├── .cruft.json ├── .dockerignore ├── .editorconfig ├── .envs ├── .ci │ ├── .django │ └── .postgres ├── .local │ ├── .django │ └── .postgres └── .production │ └── .django-example ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── new-issue.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── test.backend.yml │ └── test.frontend.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTORS.txt ├── LICENSE ├── README.md ├── ami ├── __init__.py ├── base │ ├── admin.py │ ├── fields.py │ ├── filters.py │ ├── models.py │ ├── pagination.py │ ├── permissions.py │ ├── schemas.py │ ├── serializers.py │ └── views.py ├── conftest.py ├── contrib │ ├── __init__.py │ ├── middleware.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ ├── 0004_alter_options_ordering_domain.py │ │ └── __init__.py ├── exports │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── base.py │ ├── format_types.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── registry.py │ ├── serializers.py │ ├── signals.py │ ├── tests.py │ ├── utils.py │ └── views.py ├── jobs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── management │ │ └── commands │ │ │ └── update_stale_jobs.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_job_config.py │ │ ├── 0003_job_pipeline.py │ │ ├── 0004_job_delay.py │ │ ├── 0005_alter_job_progress.py │ │ ├── 0006_alter_job_config_alter_job_progress.py │ │ ├── 0007_alter_job_options_alter_job_progress_and_more.py │ │ ├── 0008_alter_job_pipeline.py │ │ ├── 0009_remove_job_config.py │ │ ├── 0010_job_limit_job_shuffle.py │ │ ├── 0011_alter_job_limit.py │ │ ├── 0011_job_job_type_key.py │ │ ├── 0012_alter_job_limit.py │ │ ├── 0013_add_job_logs.py │ │ ├── 0013_merge_0011_alter_job_limit_0012_alter_job_limit.py │ │ ├── 0014_alter_job_progress.py │ │ ├── 0015_merge_20250117_2100.py │ │ ├── 0016_job_data_export_job_params_alter_job_job_type_key.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tasks.py │ ├── tests.py │ └── views.py ├── labelstudio │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── hooks.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ └── views.py ├── main │ ├── __init__.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── charts.py │ ├── fixtures │ │ ├── occurrences.json │ │ ├── quebec-vermont-taxa-20221220.json │ │ └── quebec-vermont-uk-denmark-taxa-20230914.json │ ├── management │ │ ├── commands │ │ │ ├── assign_roles.py │ │ │ ├── create_demo_project.py │ │ │ ├── fix_missing_relationships.py │ │ │ ├── fix_timestamps.py │ │ │ ├── import_source_images.py │ │ │ ├── import_taxa.py │ │ │ └── import_trapdata_project.py │ │ └── main.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_remove_identification_unique_primary_identification_and_more.py │ │ ├── 0003_alter_identification_options_and_more.py │ │ ├── 0004_identification_agreed_with_identification_and_more.py │ │ ├── 0004_search_extensions.py │ │ ├── 0005_taxon_display_name.py │ │ ├── 0006_merge_20230926_2353.py │ │ ├── 0007_sourceimageupload.py │ │ ├── 0008_rename_capture_sourceimageupload_source_image_and_more.py │ │ ├── 0009_alter_sourceimageupload_source_image.py │ │ ├── 0010_alter_sourceimagecollection_kwargs_and_more.py │ │ ├── 0011_taxon_author_taxon_bold_taxon_bin_and_more.py │ │ ├── 0012_alter_taxon_rank.py │ │ ├── 0013_sourceimage_detections_count.py │ │ ├── 0014_job_source_image_collection_job_source_image_single_and_more.py │ │ ├── 0015_delete_job.py │ │ ├── 0016_pipeline.py │ │ ├── 0017_alter_site_options_site_project.py │ │ ├── 0017_remove_pipeline_algorithms.py │ │ ├── 0018_delete_existing_pipeline_algorithms.py │ │ ├── 0018_device_project.py │ │ ├── 0019_delete_pipeline_remove_detection_detection_algorithm_and_more.py │ │ ├── 0019_s3storagesource_project.py │ │ ├── 0020_detection_detection_algorithm.py │ │ ├── 0021_merge_20231123_0307.py │ │ ├── 0022_alter_sourceimagecollection_method_alter_taxon_rank.py │ │ ├── 0023_taxon_main_taxon_orderin_4ffb7b_idx.py │ │ ├── 0024_deployment_captures_count_and_more.py │ │ ├── 0025_update_deployment_aggregates.py │ │ ├── 0026_occurrence_determination_score.py │ │ ├── 0027_update_occurrence_scores.py │ │ ├── 0028_alter_occurrence_options_alter_project_options_and_more.py │ │ ├── 0029_alter_deployment_device_and_more.py │ │ ├── 0030_identification_comment.py │ │ ├── 0031_s3storagesource_use_presigned_urls_and_more.py │ │ ├── 0032_alter_s3storagesource_public_base_url.py │ │ ├── 0033_remove_s3storagesource_use_presigned_urls_and_more.py │ │ ├── 0034_remove_taxon_parents_taxon_parents_json.py │ │ ├── 0035_alter_taxon_parents_json_alter_taxon_rank.py │ │ ├── 0036_event_calculated_fields_updated_at_and_more.py │ │ ├── 0037_alter_detection_path_and_more.py │ │ ├── 0038_alter_detection_path_alter_sourceimage_event_and_more.py │ │ ├── 0039_project_users_squashed_0043_rename_users_project_members.py │ │ ├── 0039_remove_classification_raw_output_and_more.py │ │ ├── 0040_alter_classification_logits_and_more.py │ │ ├── 0044_alter_project_options_squashed_0047_alter_project_options.py │ │ ├── 0044_merge_20250124_2333.py │ │ ├── 0045_alter_classification_algorithm.py │ │ ├── 0046_add_taxon_common_name_placeholder.py │ │ ├── 0048_alter_project_options_squashed_0051_alter_project_options.py │ │ ├── 0052_merge_20250207_1012.py │ │ ├── 0053_alter_classification_algorithm.py │ │ ├── 0054_alter_project_options_squashed_0055_alter_project_options.py │ │ ├── 0056_merge_20250218_1405.py │ │ ├── 0057_merge_20250220_0022.py │ │ ├── 0058_alter_project_options.py │ │ ├── 0059_alter_project_options.py │ │ └── __init__.py │ ├── models.py │ ├── signals.py │ └── tests.py ├── ml │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── management │ │ └── commands │ │ │ ├── create_missing_detection_images.py │ │ │ └── remove_duplicate_classifications.py │ ├── media.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_pipeline_endpoint_url.py │ │ ├── 0003_pipeline_slug.py │ │ ├── 0004_populate_pipeline_slugs.py │ │ ├── 0005_alter_pipeline_slug.py │ │ ├── 0006_alter_pipeline_endpoint_url_alter_pipeline_projects.py │ │ ├── 0007_add_processing_service.py │ │ ├── 0007_algorithmcategorymap_algorithm_category_map.py │ │ ├── 0008_algorithmcategorymap_description_and_more.py │ │ ├── 0009_algorithm_task_type.py │ │ ├── 0010_alter_algorithm_version.py │ │ ├── 0011_alter_algorithm_task_type_alter_algorithm_url_and_more.py │ │ ├── 0012_alter_algorithm_unique_together.py │ │ ├── 0013_remove_algorithm_url_remove_algorithmcategorymap_url_and_more.py │ │ ├── 0014_rename_model_keys.py │ │ ├── 0015_update_existing_intermediate_classifications.py │ │ ├── 0016_merge_20250117_2101.py │ │ ├── 0017_alter_algorithm_unique_together.py │ │ ├── 0018_add_processing_services_status_check_celery_beat_task.py │ │ ├── 0019_alter_algorithm_task_type.py │ │ ├── 0020_projectpipelineconfig_alter_pipeline_projects.py │ │ ├── 0021_pipeline_default_config.py │ │ ├── 0022_alter_pipeline_default_config.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── algorithm.py │ │ ├── pipeline.py │ │ ├── processing_service.py │ │ └── project_pipeline_config.py │ ├── schemas.py │ ├── serializers.py │ ├── tasks.py │ ├── tests.py │ └── views.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ └── js │ │ └── project.js ├── tasks.py ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── labelstudio │ │ ├── all_in_one.xml │ │ ├── binary_classification.xml │ │ ├── initial_object_detection.xml │ │ └── species_classification.xml │ ├── pages │ │ ├── about.html │ │ └── home.html │ └── users │ │ ├── user_detail.html │ │ └── user_form.html ├── tests │ ├── fixtures │ │ ├── images.py │ │ ├── main.py │ │ ├── ml.py │ │ ├── signals.py │ │ └── storage.py │ ├── test_doctests.py │ ├── test_merge_production_dotenvs_in_dotenv.py │ └── test_storage.py ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── api │ │ └── serializers.py │ ├── apps.py │ ├── context_processors.py │ ├── forms.py │ ├── managers.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_user_image.py │ │ ├── 0003_lowercase_existing_emails.py │ │ └── __init__.py │ ├── models.py │ ├── roles.py │ ├── signals.py │ ├── tasks.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_admin.py │ │ ├── test_drf_urls.py │ │ ├── test_drf_views.py │ │ ├── test_forms.py │ │ ├── test_managers.py │ │ ├── test_models.py │ │ ├── test_swagger.py │ │ ├── test_tasks.py │ │ └── test_user_auth.py │ ├── urls.py │ └── views.py └── utils │ ├── __init__.py │ ├── dates.py │ ├── fields.py │ ├── requests.py │ ├── s3.py │ ├── schemas.py │ ├── storages.py │ └── tests.py ├── compose ├── local │ ├── django │ │ ├── Dockerfile │ │ ├── celery │ │ │ ├── beat │ │ │ │ └── start │ │ │ ├── flower │ │ │ │ └── start │ │ │ └── worker │ │ │ │ └── start │ │ └── start │ ├── docs │ │ ├── Dockerfile │ │ └── start │ ├── minio │ │ ├── init.sh │ │ └── nginx.conf │ ├── postgres │ │ ├── Dockerfile │ │ ├── maintenance │ │ │ ├── _sourced │ │ │ │ ├── constants.sh │ │ │ │ ├── countdown.sh │ │ │ │ ├── messages.sh │ │ │ │ └── yes_no.sh │ │ │ ├── backup │ │ │ ├── backups │ │ │ ├── pgtune │ │ │ └── restore │ │ └── postgresql.conf │ └── ui │ │ └── Dockerfile └── production │ ├── aws │ ├── Dockerfile │ └── maintenance │ │ ├── download │ │ └── upload │ ├── django │ ├── Dockerfile │ ├── celery │ │ ├── beat │ │ │ └── start │ │ ├── flower │ │ │ └── start │ │ └── worker │ │ │ └── start │ ├── entrypoint │ └── start │ ├── nginx │ ├── Dockerfile │ └── default.conf │ └── traefik │ ├── Dockerfile │ └── traefik.yml ├── config ├── __init__.py ├── api_router.py ├── asgi.py ├── celery_app.py ├── newrelic.ini ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── production.py │ └── test.py ├── urls.py ├── websocket.py └── wsgi.py ├── data ├── README.md └── db │ └── snapshots │ └── .gitkeep ├── docker-compose.ci.yml ├── docker-compose.production.yml ├── docker-compose.staging.yml ├── docker-compose.worker.yml ├── docker-compose.yml ├── docs ├── Makefile ├── __init__.py ├── conf.py ├── diagrams │ ├── models.mermaid │ └── models.mermaid.svg ├── howto.rst ├── index.rst ├── make.bat └── users.rst ├── locale ├── README.md ├── en_US │ └── LC_MESSAGES │ │ └── django.po └── pt_BR │ └── LC_MESSAGES │ └── django.po ├── manage.py ├── merge_production_dotenvs_in_dotenv.py ├── processing_services ├── README.md ├── docker-compose.yml ├── example │ ├── Dockerfile │ ├── api │ │ ├── __init__.py │ │ ├── algorithms.py │ │ ├── api.py │ │ ├── pipelines.py │ │ ├── schemas.py │ │ ├── test.py │ │ └── utils.py │ ├── docker-compose.yml │ ├── main.py │ └── requirements.txt └── minimal │ ├── Dockerfile │ ├── api │ ├── __init__.py │ ├── algorithms.py │ ├── api.py │ ├── pipelines.py │ ├── schemas.py │ ├── test.py │ └── utils.py │ ├── docker-compose.yml │ ├── main.py │ └── requirements.txt ├── pyproject.toml ├── requirements ├── base.txt ├── local.txt └── production.txt ├── setup.cfg └── ui ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── .yarn └── releases │ └── yarn-berry.cjs ├── .yarnrc.yml ├── README.md ├── index.html ├── netlify.toml ├── package.json ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── android-chrome-512x512.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ └── Mazzard │ │ ├── mazzardsoftm-bold.otf │ │ ├── mazzardsoftm-bolditalic.otf │ │ ├── mazzardsoftm-italic.otf │ │ ├── mazzardsoftm-medium.otf │ │ ├── mazzardsoftm-mediumitalic.otf │ │ ├── mazzardsoftm-regular.otf │ │ ├── mazzardsoftm-semibold.otf │ │ └── mazzardsoftm-semibolditalic.otf ├── robots.txt └── site.webmanifest ├── src ├── app.module.scss ├── app.tsx ├── components │ ├── analytics.tsx │ ├── blueprint-collection │ │ ├── blueprint-collection.module.scss │ │ └── blueprint-collection.tsx │ ├── breadcrumbs │ │ ├── breadcrumbs.module.scss │ │ └── breadcrumbs.tsx │ ├── constants.ts │ ├── cookie-dialog │ │ ├── cookie-dialog.module.scss │ │ └── cookie-dialog.tsx │ ├── empty-state │ │ └── empty-state.tsx │ ├── error-boundary │ │ ├── error-boundary.module.scss │ │ └── error-boundary.tsx │ ├── error-state │ │ └── error-state.tsx │ ├── fetch-info │ │ ├── fetch-info.module.scss │ │ └── fetch-info.tsx │ ├── filtering │ │ ├── filter-control.tsx │ │ ├── filter-section.tsx │ │ ├── filters │ │ │ ├── algorithm-filter.tsx │ │ │ ├── collection-filter.tsx │ │ │ ├── date-filter.tsx │ │ │ ├── image-filter.tsx │ │ │ ├── pipeline-filter.tsx │ │ │ ├── score-filter.tsx │ │ │ ├── session-filter.tsx │ │ │ ├── station-filter.tsx │ │ │ ├── status-filter.tsx │ │ │ ├── taxa-list-filter.tsx │ │ │ ├── taxon-filter.tsx │ │ │ ├── type-filter.tsx │ │ │ ├── types.ts │ │ │ ├── verification-status-filter.tsx │ │ │ └── verified-by-filter.tsx │ │ └── utils.ts │ ├── form │ │ ├── delete-form │ │ │ └── delete-form.tsx │ │ ├── form-controller.tsx │ │ ├── form-field.tsx │ │ ├── layout │ │ │ ├── layout.module.scss │ │ │ └── layout.tsx │ │ └── types.ts │ ├── gallery │ │ ├── gallery.module.scss │ │ └── gallery.tsx │ ├── header │ │ ├── antenna-primary.svg │ │ ├── header.module.scss │ │ ├── header.tsx │ │ ├── user-info-dialog │ │ │ ├── user-info-dialog.module.scss │ │ │ ├── user-info-dialog.tsx │ │ │ ├── user-info-form │ │ │ │ ├── user-email-field.tsx │ │ │ │ ├── user-info-form.module.scss │ │ │ │ ├── user-info-form.tsx │ │ │ │ └── user-password-field.tsx │ │ │ └── user-info-image-upload │ │ │ │ ├── user-info-image-upload.module.scss │ │ │ │ └── user-info-image-upload.tsx │ │ └── version-info │ │ │ ├── version-info.module.scss │ │ │ └── version-info.tsx │ ├── info-page │ │ ├── code-of-conduct-page │ │ │ ├── code-of-conduct-page.tsx │ │ │ └── code-of-conduct.md │ │ ├── info-page.module.scss │ │ ├── info-page.tsx │ │ └── terms-of-service-page │ │ │ ├── terms-of-service-page.tsx │ │ │ └── terms-of-service.md │ ├── license-info │ │ └── license-info.tsx │ ├── menu │ │ ├── menu.module.scss │ │ └── menu.tsx │ ├── taxon-search │ │ ├── buildTree.ts │ │ ├── taxon-search.tsx │ │ ├── taxon-select.tsx │ │ ├── types.ts │ │ └── useTaxonSearch.ts │ ├── taxon │ │ └── taxon-ranks │ │ │ ├── taxon-ranks.module.scss │ │ │ └── taxon-ranks.tsx │ └── terms-of-service-info │ │ ├── terms-of-service-info.module.scss │ │ └── terms-of-service-info.tsx ├── data-services │ ├── constants.ts │ ├── hooks │ │ ├── algorithm │ │ │ ├── useAlgorithmDetails.ts │ │ │ └── useAlgorithms.ts │ │ ├── auth │ │ │ ├── tests │ │ │ │ ├── useAuthorizedQuery.test.ts │ │ │ │ ├── useLogin.test.ts │ │ │ │ ├── useLogout.test.ts │ │ │ │ └── useUserInfo.test.ts │ │ │ ├── useAuthorizedQuery.ts │ │ │ ├── useLogin.ts │ │ │ ├── useLogout.ts │ │ │ ├── useResetPassword.ts │ │ │ ├── useResetPasswordConfirm.ts │ │ │ ├── useSignUp.ts │ │ │ ├── useUpdateUserEmail.ts │ │ │ ├── useUpdateUserInfo.ts │ │ │ ├── useUpdateUserPassword.ts │ │ │ └── useUserInfo.ts │ │ ├── captures │ │ │ ├── useCaptureDetails.ts │ │ │ ├── useCaptures.ts │ │ │ ├── useDeleteCapture.ts │ │ │ ├── useStarCapture.ts │ │ │ └── useUploadCapture.ts │ │ ├── collections │ │ │ ├── useCollectionDetails.ts │ │ │ ├── useCollections.ts │ │ │ └── usePopulateCollection.ts │ │ ├── deployments │ │ │ ├── useCreateDeployment.ts │ │ │ ├── useDeleteDeployment.ts │ │ │ ├── useDeployments.ts │ │ │ ├── useDeploymentsDetails.ts │ │ │ ├── useSyncDeploymentSourceImages.ts │ │ │ ├── useUpdateDeployment.ts │ │ │ └── utils.ts │ │ ├── entities │ │ │ ├── types.ts │ │ │ ├── useCreateEntity.ts │ │ │ ├── useDeleteEntity.ts │ │ │ ├── useEntities.ts │ │ │ ├── useUpdateEntity.ts │ │ │ └── utils.ts │ │ ├── exports │ │ │ ├── useExportDetails.ts │ │ │ └── useExports.ts │ │ ├── identifications │ │ │ ├── types.ts │ │ │ ├── useClassificationDetails.ts │ │ │ ├── useCreateIdentification.ts │ │ │ ├── useCreateIdentifications.ts │ │ │ └── useDeleteIdentification.ts │ │ ├── jobs │ │ │ ├── useCancelJob.ts │ │ │ ├── useCreateJob.ts │ │ │ ├── useDeleteJob.ts │ │ │ ├── useJobDetails.ts │ │ │ ├── useJobs.ts │ │ │ ├── useQueueJob.ts │ │ │ └── useRetryJob.ts │ │ ├── occurrences │ │ │ ├── useOccurrenceDetails.ts │ │ │ └── useOccurrences.ts │ │ ├── pages │ │ │ ├── types.ts │ │ │ └── usePageDetails.ts │ │ ├── pipelines │ │ │ ├── usePipelineDetails.ts │ │ │ └── usePipelines.ts │ │ ├── processing-services │ │ │ ├── usePopulateProcessingService.ts │ │ │ ├── useProcessingServiceDetails.ts │ │ │ ├── useProcessingServices.ts │ │ │ └── useTestProcessingServiceConnection.ts │ │ ├── projects │ │ │ ├── useCreateProject.ts │ │ │ ├── useDeleteProject.ts │ │ │ ├── useProjectDetails.ts │ │ │ ├── useProjects.ts │ │ │ ├── useUpdateProject.ts │ │ │ └── utils.ts │ │ ├── sessions │ │ │ ├── useSessionDetails.ts │ │ │ ├── useSessionTimeline.ts │ │ │ └── useSessions.ts │ │ ├── species │ │ │ ├── useSpecies.ts │ │ │ └── useSpeciesDetails.ts │ │ ├── storage-sources │ │ │ ├── useStorageDetails.ts │ │ │ ├── useStorageSources.ts │ │ │ └── useTestStorageConnection.ts │ │ ├── taxa-lists │ │ │ └── useTaxaLists.ts │ │ └── useStatus.ts │ ├── models │ │ ├── algorithm.ts │ │ ├── capture-details.ts │ │ ├── capture.ts │ │ ├── classification-details.ts │ │ ├── collection.ts │ │ ├── deployment-details.ts │ │ ├── deployment.ts │ │ ├── entity.ts │ │ ├── export.ts │ │ ├── job-details.ts │ │ ├── job.ts │ │ ├── occurrence-details.ts │ │ ├── occurrence.ts │ │ ├── pipeline.ts │ │ ├── processing-service.ts │ │ ├── project.ts │ │ ├── session-details.ts │ │ ├── session.ts │ │ ├── species-details.ts │ │ ├── species.ts │ │ ├── storage.ts │ │ ├── taxa-list.ts │ │ ├── taxa.ts │ │ └── timeline-tick.ts │ ├── types.ts │ └── utils.ts ├── design-system │ ├── components │ │ ├── box │ │ │ ├── box.module.scss │ │ │ └── box.tsx │ │ ├── bulk-action-bar │ │ │ ├── bulk-action-bar.module.scss │ │ │ └── bulk-action-bar.tsx │ │ ├── button │ │ │ ├── button.module.scss │ │ │ └── button.tsx │ │ ├── card │ │ │ ├── card.module.scss │ │ │ └── card.tsx │ │ ├── checkbox │ │ │ ├── checkbox.module.scss │ │ │ └── checkbox.tsx │ │ ├── collections-picker.tsx │ │ ├── combo-box │ │ │ ├── combo-box-simple │ │ │ │ └── combo-box-simple.tsx │ │ │ └── styles.module.scss │ │ ├── dialog │ │ │ ├── dialog.module.scss │ │ │ └── dialog.tsx │ │ ├── file-input │ │ │ ├── file-input.module.scss │ │ │ ├── file-input.tsx │ │ │ └── types.ts │ │ ├── form-stepper │ │ │ ├── form-stepper.module.scss │ │ │ └── form-stepper.tsx │ │ ├── icon-button │ │ │ ├── icon-button.module.scss │ │ │ └── icon-button.tsx │ │ ├── icon │ │ │ ├── assets │ │ │ │ ├── batch-id.svg │ │ │ │ ├── checkmark.svg │ │ │ │ ├── close.svg │ │ │ │ ├── deployments.svg │ │ │ │ ├── detections.svg │ │ │ │ ├── download.svg │ │ │ │ ├── filters.svg │ │ │ │ ├── gallery-view.svg │ │ │ │ ├── identifiers.svg │ │ │ │ ├── images.svg │ │ │ │ ├── info.svg │ │ │ │ ├── members.svg │ │ │ │ ├── occurrences.svg │ │ │ │ ├── overview.svg │ │ │ │ ├── photograph.svg │ │ │ │ ├── play-button.svg │ │ │ │ ├── radix │ │ │ │ │ ├── check.svg │ │ │ │ │ ├── circle-backslash.svg │ │ │ │ │ ├── clock.svg │ │ │ │ │ ├── cross.svg │ │ │ │ │ ├── error.svg │ │ │ │ │ ├── external-link.svg │ │ │ │ │ ├── heart-filled.svg │ │ │ │ │ ├── heart.svg │ │ │ │ │ ├── minus.svg │ │ │ │ │ ├── options.svg │ │ │ │ │ ├── pencil.svg │ │ │ │ │ ├── plus.svg │ │ │ │ │ ├── question-mark.svg │ │ │ │ │ ├── search.svg │ │ │ │ │ ├── toggle-down.svg │ │ │ │ │ ├── toggle-left.svg │ │ │ │ │ ├── toggle-right.svg │ │ │ │ │ ├── trash.svg │ │ │ │ │ └── update.svg │ │ │ │ ├── sessions.svg │ │ │ │ ├── settings.svg │ │ │ │ ├── shield-check.svg │ │ │ │ ├── sort.svg │ │ │ │ ├── species.svg │ │ │ │ └── table-view.svg │ │ │ ├── icon.module.scss │ │ │ └── icon.tsx │ │ ├── image-carousel │ │ │ ├── image-carousel.module.scss │ │ │ ├── image-carousel.tsx │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── image-upload │ │ │ ├── image-upload.module.scss │ │ │ └── image-upload.tsx │ │ ├── info-block │ │ │ ├── info-block.module.scss │ │ │ └── info-block.tsx │ │ ├── input │ │ │ ├── input.module.scss │ │ │ └── input.tsx │ │ ├── loading-spinner │ │ │ ├── loading-spinner.module.scss │ │ │ └── loading-spinner.tsx │ │ ├── navigation │ │ │ ├── navigation-bar.module.scss │ │ │ └── navigation-bar.tsx │ │ ├── page-footer │ │ │ ├── page-footer.module.scss │ │ │ └── page-footer.tsx │ │ ├── page-header │ │ │ ├── page-header.module.scss │ │ │ └── page-header.tsx │ │ ├── pagination-bar │ │ │ ├── getPageWindow.ts │ │ │ ├── info-label │ │ │ │ ├── getValueInRange.ts │ │ │ │ ├── info-label.module.scss │ │ │ │ └── info-label.tsx │ │ │ ├── page-button │ │ │ │ ├── page-button.module.scss │ │ │ │ └── page-button.tsx │ │ │ ├── pagination-bar.module.scss │ │ │ └── pagination-bar.tsx │ │ ├── plot-grid │ │ │ ├── plot-grid.module.scss │ │ │ └── plot-grid.tsx │ │ ├── plot │ │ │ ├── lazy-plot.tsx │ │ │ ├── plot.module.scss │ │ │ ├── plot.tsx │ │ │ └── types.ts │ │ ├── popover │ │ │ ├── popover.module.scss │ │ │ └── popover.tsx │ │ ├── select │ │ │ ├── select.module.scss │ │ │ └── select.tsx │ │ ├── slider │ │ │ ├── dial.svg │ │ │ ├── styles.module.scss │ │ │ └── timestamp-slider.tsx │ │ ├── status │ │ │ ├── status-bar.tsx │ │ │ ├── status-marker │ │ │ │ ├── status-marker.module.scss │ │ │ │ └── status-marker.tsx │ │ │ └── types.ts │ │ ├── table │ │ │ ├── basic-table-cell │ │ │ │ ├── basic-table-cell.module.scss │ │ │ │ └── basic-table-cell.tsx │ │ │ ├── column-settings │ │ │ │ ├── column-settings.module.scss │ │ │ │ └── column-settings.tsx │ │ │ ├── image-table-cell │ │ │ │ ├── image-table-cell.module.scss │ │ │ │ └── image-table-cell.tsx │ │ │ ├── status-table-cell │ │ │ │ ├── status-table-cell.module.scss │ │ │ │ └── status-table-cell.tsx │ │ │ ├── table-header │ │ │ │ ├── table-header.module.scss │ │ │ │ └── table-header.tsx │ │ │ ├── table │ │ │ │ ├── sticky-header-table.tsx │ │ │ │ ├── table.module.scss │ │ │ │ ├── table.tsx │ │ │ │ ├── useScrollFader.ts │ │ │ │ └── vh-sticky-table-header │ │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── tabs │ │ │ ├── tabs.module.scss │ │ │ └── tabs.tsx │ │ ├── toggle-group │ │ │ ├── toggle-group.module.scss │ │ │ └── toggle-group.tsx │ │ ├── tooltip │ │ │ ├── basic-tooltip.tsx │ │ │ ├── tooltip.module.scss │ │ │ └── tooltip.tsx │ │ └── wizard │ │ │ ├── status-bullet │ │ │ ├── status-bullet.module.scss │ │ │ └── status-bullet.tsx │ │ │ ├── wizard.module.scss │ │ │ └── wizard.tsx │ ├── map │ │ ├── config.ts │ │ ├── editable-map │ │ │ ├── editable-map.tsx │ │ │ └── editable-marker.tsx │ │ ├── minimap-control.tsx │ │ ├── multi-marker-map │ │ │ └── multi-marker-map.tsx │ │ ├── pin.svg │ │ ├── styles.module.scss │ │ └── types.ts │ └── variables │ │ ├── colors.scss │ │ ├── typography.scss │ │ └── variables.scss ├── index.css ├── index.tsx ├── pages │ ├── algorithm-details │ │ ├── algorithm-details-dialog.tsx │ │ └── styles.module.scss │ ├── auth │ │ ├── auth.module.scss │ │ ├── auth.tsx │ │ ├── login.tsx │ │ ├── reset-password-confirm.tsx │ │ ├── reset-password.tsx │ │ └── sign-up.tsx │ ├── collection-details │ │ ├── capture-columns.tsx │ │ ├── capture-gallery.tsx │ │ └── collection-details.tsx │ ├── deployment-details │ │ ├── delete-deployment-dialog.tsx │ │ ├── deployment-details-dialog.tsx │ │ ├── deployment-details-form │ │ │ ├── config.ts │ │ │ ├── deployment-details-form.tsx │ │ │ ├── section-example-captures │ │ │ │ ├── section-example-captures.module.scss │ │ │ │ ├── section-example-captures.tsx │ │ │ │ └── useCaptureError.ts │ │ │ ├── section-general │ │ │ │ └── section-general.tsx │ │ │ ├── section-location │ │ │ │ ├── geo-search │ │ │ │ │ ├── geo-search.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ └── useGeoSearch.ts │ │ │ │ ├── location-map │ │ │ │ │ ├── location-map.module.scss │ │ │ │ │ └── location-map.tsx │ │ │ │ └── section-location.tsx │ │ │ ├── section-source-images │ │ │ │ ├── actions │ │ │ │ │ ├── styles.module.scss │ │ │ │ │ └── sync-source-images.tsx │ │ │ │ └── section-source-images.tsx │ │ │ └── types.ts │ │ ├── deployment-details-info.tsx │ │ ├── new-deployment-dialog.tsx │ │ └── styles.module.scss │ ├── deployments │ │ ├── deployment-columns.tsx │ │ ├── deployments.module.scss │ │ └── deployments.tsx │ ├── export-details │ │ ├── export-details-dialog.tsx │ │ └── styles.module.scss │ ├── job-details │ │ ├── job-actions │ │ │ ├── cancel-job.tsx │ │ │ ├── queue-job.tsx │ │ │ └── retry-job.tsx │ │ ├── job-details-form │ │ │ ├── job-details-form.tsx │ │ │ └── pipelines-picker.tsx │ │ ├── job-details.module.scss │ │ ├── job-details.tsx │ │ ├── job-stage-label │ │ │ ├── job-stage-label.module.scss │ │ │ └── job-stage-label.tsx │ │ └── new-job-dialog.tsx │ ├── jobs │ │ ├── delete-jobs-dialog.tsx │ │ ├── jobs-columns.tsx │ │ ├── jobs.module.scss │ │ └── jobs.tsx │ ├── occurrence-details │ │ ├── agree │ │ │ └── agree.tsx │ │ ├── identification-card │ │ │ ├── human-identification.tsx │ │ │ ├── machine-avatar.svg │ │ │ └── machine-prediction.tsx │ │ ├── occurrence-details.module.scss │ │ ├── occurrence-details.tsx │ │ ├── reject-id │ │ │ ├── constants.ts │ │ │ ├── id-button.tsx │ │ │ ├── id-quick-actions.module.scss │ │ │ ├── id-quick-actions.tsx │ │ │ ├── useRecentOptions.ts │ │ │ └── utils.ts │ │ ├── status-label │ │ │ ├── status-label.module.scss │ │ │ └── status-label.tsx │ │ ├── suggest-id │ │ │ ├── suggest-id-popover.tsx │ │ │ └── suggest-id.tsx │ │ └── taxonomy-info │ │ │ ├── taxonomy-info.module.scss │ │ │ └── taxonomy-info.tsx │ ├── occurrences │ │ ├── occurrence-actions.tsx │ │ ├── occurrence-columns.tsx │ │ ├── occurrence-gallery.tsx │ │ ├── occurrence-navigation.tsx │ │ ├── occurrences.module.scss │ │ └── occurrences.tsx │ ├── pipeline-details │ │ ├── pipeline-algorithms.tsx │ │ ├── pipeline-details-dialog.tsx │ │ ├── pipeline-stages.tsx │ │ └── styles.module.scss │ ├── processing-service-details │ │ ├── processing-service-details-dialog.tsx │ │ ├── processing-service-pipelines.tsx │ │ └── styles.module.scss │ ├── project-details │ │ ├── delete-project-dialog.tsx │ │ ├── new-project-dialog.tsx │ │ ├── project-details-form.tsx │ │ └── styles.module.scss │ ├── project │ │ ├── algorithms │ │ │ ├── algorithms-columns.tsx │ │ │ └── algorithms.tsx │ │ ├── collections │ │ │ ├── collection-actions.tsx │ │ │ ├── collection-columns.tsx │ │ │ ├── collections.tsx │ │ │ └── constants.tsx │ │ ├── entities │ │ │ ├── delete-entity-dialog.tsx │ │ │ ├── details-form │ │ │ │ ├── collection-details-form.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── entity-details-form.tsx │ │ │ │ ├── export-details-form.tsx │ │ │ │ ├── processing-service-details-form.tsx │ │ │ │ ├── storage-details-form.tsx │ │ │ │ └── types.ts │ │ │ ├── devices.tsx │ │ │ ├── entities-columns.tsx │ │ │ ├── entities-picker.tsx │ │ │ ├── entities.tsx │ │ │ ├── entity-details-dialog.tsx │ │ │ ├── new-entity-dialog.tsx │ │ │ ├── sites.tsx │ │ │ └── styles.module.scss │ │ ├── exports │ │ │ ├── exports-columns.tsx │ │ │ └── exports.tsx │ │ ├── general │ │ │ └── general.tsx │ │ ├── pipelines │ │ │ ├── pipelines-columns.tsx │ │ │ └── pipelines.tsx │ │ ├── processing-services │ │ │ ├── connection-status.tsx │ │ │ ├── processing-services-actions.tsx │ │ │ ├── processing-services-columns.tsx │ │ │ ├── processing-services.module.scss │ │ │ ├── processing-services.tsx │ │ │ └── status-info │ │ │ │ ├── status-info.module.scss │ │ │ │ ├── status-info.tsx │ │ │ │ └── types.ts │ │ ├── project.tsx │ │ ├── sidebar │ │ │ ├── sidebar.tsx │ │ │ └── useSidebarSections.tsx │ │ ├── storage │ │ │ ├── connection-status.tsx │ │ │ ├── status-info │ │ │ │ ├── status-info.module.scss │ │ │ │ ├── status-info.tsx │ │ │ │ └── types.ts │ │ │ ├── storage-columns.tsx │ │ │ ├── storage.module.scss │ │ │ └── storage.tsx │ │ └── summary │ │ │ ├── deployments-map.tsx │ │ │ └── summary.tsx │ ├── projects │ │ ├── project-gallery.tsx │ │ └── projects.tsx │ ├── session-details │ │ ├── playback │ │ │ ├── activity-plot │ │ │ │ ├── activity-plot.tsx │ │ │ │ ├── lazy-activity-plot.module.scss │ │ │ │ ├── lazy-activity-plot.tsx │ │ │ │ ├── types.ts │ │ │ │ └── useDynamicPlotWidth.ts │ │ │ ├── capture-details │ │ │ │ ├── capture-details.module.scss │ │ │ │ ├── capture-details.tsx │ │ │ │ └── capture-job │ │ │ │ │ ├── capture-job-dialog.tsx │ │ │ │ │ ├── capture-job.tsx │ │ │ │ │ └── process-now.tsx │ │ │ ├── capture-navigation │ │ │ │ ├── capture-navigation.module.scss │ │ │ │ └── capture-navigation.tsx │ │ │ ├── frame │ │ │ │ ├── frame.module.scss │ │ │ │ ├── frame.tsx │ │ │ │ └── types.ts │ │ │ ├── playback.module.scss │ │ │ ├── playback.tsx │ │ │ ├── session-captures-slider │ │ │ │ └── session-captures-slider.tsx │ │ │ ├── threshold-slider │ │ │ │ └── threshold-slider.tsx │ │ │ ├── useActiveCapture.ts │ │ │ ├── useActiveOccurrences.ts │ │ │ └── utils.tsx │ │ ├── session-details.module.scss │ │ ├── session-details.tsx │ │ └── session-info │ │ │ ├── session-info.module.scss │ │ │ └── session-info.tsx │ ├── sessions │ │ ├── session-columns.tsx │ │ ├── session-gallery.tsx │ │ └── sessions.tsx │ ├── species-details │ │ ├── species-details.module.scss │ │ └── species-details.tsx │ └── species │ │ ├── species-columns.tsx │ │ ├── species-gallery.tsx │ │ └── species.tsx ├── setupTests.ts └── utils │ ├── breadcrumbContext.tsx │ ├── constants.ts │ ├── cookieConsent │ ├── constants.ts │ ├── cookieConsentContext.tsx │ └── types.ts │ ├── date │ ├── getCompactDatespanString │ │ ├── getCompactDatespanString.test.ts │ │ └── getCompactDatespanString.ts │ ├── getCompactTimespanString │ │ └── getCompactTimespanString.ts │ ├── getFormatedDateString │ │ └── getFormatedDateString.ts │ ├── getFormatedDateTimeString │ │ └── getFormatedDateTimeString.ts │ └── getFormatedTimeString │ │ └── getFormatedTimeString.ts │ ├── formContext │ ├── formContext.tsx │ └── types.ts │ ├── getAppRoute.ts │ ├── isEmpty │ ├── isEmpty.test.ts │ └── isEmpty.ts │ ├── language.ts │ ├── numberFormats │ ├── index.ts │ └── numberFormats.test.ts │ ├── parseServerError │ ├── parseServerError.test.ts │ └── parseServerError.ts │ ├── snakeCaseToSentenceCase.ts │ ├── testHelpers.tsx │ ├── useClientSideSort.ts │ ├── useColumnSettings.tsx │ ├── useDebounce.ts │ ├── useFilters.ts │ ├── useFormError.ts │ ├── useNavItems.ts │ ├── usePageBreadcrumb.ts │ ├── usePageTitle.ts │ ├── usePagination.ts │ ├── useSelectedView.ts │ ├── useSort.ts │ ├── useSyncSectionStatus.ts │ ├── useWindowSize.ts │ ├── user │ ├── constants.ts │ ├── types.ts │ ├── userContext.test.tsx │ ├── userContext.tsx │ └── userInfoContext.tsx │ └── userPreferences │ ├── constants.ts │ ├── types.ts │ └── userPreferencesContext.tsx ├── tailwind.config.js ├── tsconfig.json ├── vite-env.d.ts ├── vite.config.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .gitattributes 3 | .github 4 | .gitignore 5 | .gitlab-ci.yml 6 | .idea 7 | .pre-commit-config.yaml 8 | .readthedocs.yml 9 | .travis.yml 10 | .git 11 | ui 12 | ami/media 13 | backups 14 | venv 15 | .venv 16 | .env 17 | .envs 18 | .envs/* 19 | node_modules 20 | data 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml,xml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | 25 | [default.conf] 26 | indent_style = space 27 | indent_size = 2 28 | -------------------------------------------------------------------------------- /.envs/.ci/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | DJANGO_SETTINGS_MODULE="config.settings.local" 5 | 6 | # Redis 7 | # ------------------------------------------------------------------------------ 8 | REDIS_URL=redis://redis:6379/0 9 | 10 | DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:4000 11 | 12 | MINIO_ENDPOINT=http://minio:9000 13 | MINIO_ROOT_USER=amistorage 14 | MINIO_ROOT_PASSWORD=amistorage 15 | MINIO_DEFAULT_BUCKET=ami-ci 16 | MINIO_STORAGE_USE_HTTPS=False 17 | MINIO_TEST_BUCKET=ami-test-ci 18 | MINIO_BROWSER_REDIRECT_URL=http://minio:9001 19 | -------------------------------------------------------------------------------- /.envs/.ci/.postgres: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=postgres 2 | POSTGRES_PORT=5432 3 | POSTGRES_DB=ami-ci 4 | POSTGRES_USER=4JXkOnTAeDmDyIapSRrGEE 5 | POSTGRES_PASSWORD=d4xojpnJU3OzPQ0apSCLP1oHR1TYvyMzAlF5KpE9HFL6MPlnbDibwI 6 | -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=postgres 2 | POSTGRES_PORT=5432 3 | POSTGRES_DB=ami 4 | POSTGRES_USER=xekSryPnqczJXkOnTAeDmDyIapSRrGEE 5 | POSTGRES_PASSWORD=iMRQjJEGflj5xojpnJU3OzPQ0apSCLP1oHR1TYvyMzAlF5KpE9HFL6MPlnbDibwI 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | - package-ecosystem: "npm" 9 | directory: "/frontend/" 10 | schedule: 11 | interval: "monthly" 12 | 13 | - package-ecosystem: "docker" 14 | directory: "/frontend/" 15 | schedule: 16 | interval: "monthly" 17 | 18 | - package-ecosystem: "docker" 19 | directory: "/backend/" 20 | schedule: 21 | interval: "monthly" 22 | 23 | - package-ecosystem: "docker" 24 | directory: "/" 25 | schedule: 26 | interval: "monthly" 27 | 28 | - package-ecosystem: "pip" 29 | directory: "/backend/" 30 | schedule: 31 | interval: "monthly" 32 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: '3.11' 12 | 13 | # Build documentation in the docs/ directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: requirements/local.txt 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "batisteo.vscode-django", 4 | "ms-python.black-formatter", 5 | "dbaeumer.vscode-eslint", 6 | "ms-python.isort", 7 | "esbenp.prettier-vscode", 8 | "ms-python.python", 9 | "foxundermoon.shell-format", 10 | "ms-azuretools.vscode-docker" 11 | 12 | ], 13 | "unwantedRecommendations": [ 14 | 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Rolnick Lab 2 | -------------------------------------------------------------------------------- /ami/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) 3 | -------------------------------------------------------------------------------- /ami/base/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/base/admin.py -------------------------------------------------------------------------------- /ami/base/filters.py: -------------------------------------------------------------------------------- 1 | from django.db.models import F, OrderBy 2 | from rest_framework.filters import OrderingFilter 3 | 4 | 5 | class NullsLastOrderingFilter(OrderingFilter): 6 | def get_ordering(self, request, queryset, view): 7 | values = super().get_ordering(request, queryset, view) 8 | if not values: 9 | return values 10 | return [OrderBy(F(value.lstrip("-")), descending=value.startswith("-"), nulls_last=True) for value in values] 11 | -------------------------------------------------------------------------------- /ami/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ami.users.models import User 4 | from ami.users.tests.factories import UserFactory 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def media_storage(settings, tmpdir): 9 | settings.MEDIA_ROOT = tmpdir.strpath 10 | 11 | 12 | @pytest.fixture 13 | def user(db) -> User: 14 | return UserFactory() 15 | -------------------------------------------------------------------------------- /ami/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /ami/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /ami/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /ami/contrib/sites/migrations/0004_alter_options_ordering_domain.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-02-04 14:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sites", "0003_set_site_domain_and_name"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="site", 15 | options={ 16 | "ordering": ["domain"], 17 | "verbose_name": "site", 18 | "verbose_name_plural": "sites", 19 | }, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ami/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /ami/exports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/exports/__init__.py -------------------------------------------------------------------------------- /ami/exports/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExportsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "ami.exports" 7 | 8 | def ready(self): 9 | import ami.exports.signals # noqa: F401 10 | -------------------------------------------------------------------------------- /ami/exports/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/exports/migrations/__init__.py -------------------------------------------------------------------------------- /ami/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/jobs/__init__.py -------------------------------------------------------------------------------- /ami/jobs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class JobsConfig(AppConfig): 6 | name = "ami.jobs" 7 | verbose_name = _("Jobs") 8 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0003_job_pipeline.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-02 01:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0016_pipeline"), 10 | ("jobs", "0002_alter_job_config"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="job", 16 | name="pipeline", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="jobs", 22 | to="main.pipeline", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0004_job_delay.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-02 02:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0003_job_pipeline"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="job", 14 | name="delay", 15 | field=models.IntegerField( 16 | default=0, help_text="Delay before running the job", verbose_name="Delay in seconds" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0005_alter_job_progress.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-03 02:46 2 | 3 | import ami.jobs.models 4 | from django.db import migrations 5 | import django_pydantic_field.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("jobs", "0004_job_delay"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="job", 16 | name="progress", 17 | field=django_pydantic_field.fields.PydanticSchemaField( 18 | config=None, 19 | default={"stages": [], "summary": {"progress": 0.0, "status": "CREATED"}}, 20 | schema=ami.jobs.models.JobProgress, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0008_alter_job_pipeline.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-10 01:33 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("ml", "0001_initial"), 10 | ("jobs", "0007_alter_job_options_alter_job_progress_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="job", 16 | name="pipeline", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="jobs", 22 | to="ml.pipeline", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0009_remove_job_config.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-10 01:52 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0008_alter_job_pipeline"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="job", 14 | name="config", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0011_alter_job_limit.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-11-03 23:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0010_job_limit_job_shuffle"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="job", 14 | name="limit", 15 | field=models.IntegerField( 16 | blank=True, 17 | default=None, 18 | help_text="Limit the number of images to process", 19 | null=True, 20 | verbose_name="Limit", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0012_alter_job_limit.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-11-11 17:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0011_job_job_type_key"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="job", 14 | name="limit", 15 | field=models.IntegerField( 16 | blank=True, 17 | default=None, 18 | help_text="Limit the number of images to process", 19 | null=True, 20 | verbose_name="Limit", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0013_merge_0011_alter_job_limit_0012_alter_job_limit.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-12-17 22:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0011_alter_job_limit"), 9 | ("jobs", "0012_alter_job_limit"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0014_alter_job_progress.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-12-17 20:13 2 | 3 | import ami.jobs.models 4 | from django.db import migrations 5 | import django_pydantic_field.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("jobs", "0013_add_job_logs"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="job", 16 | name="progress", 17 | field=django_pydantic_field.fields.PydanticSchemaField( 18 | config=None, 19 | default={"errors": [], "logs": [], "stages": [], "summary": {"progress": 0.0, "status": "CREATED"}}, 20 | schema=ami.jobs.models.JobProgress, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/jobs/migrations/0015_merge_20250117_2100.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-01-17 21:00 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("jobs", "0013_merge_0011_alter_job_limit_0012_alter_job_limit"), 9 | ("jobs", "0014_alter_job_progress"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/jobs/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/jobs/migrations/__init__.py -------------------------------------------------------------------------------- /ami/labelstudio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/labelstudio/__init__.py -------------------------------------------------------------------------------- /ami/labelstudio/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class LabelStudioConfig(AppConfig): 6 | name = "ami.labelstudio" 7 | verbose_name = _("Label Studio Integration") 8 | -------------------------------------------------------------------------------- /ami/labelstudio/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/labelstudio/migrations/__init__.py -------------------------------------------------------------------------------- /ami/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/__init__.py -------------------------------------------------------------------------------- /ami/main/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.db.models.signals import post_migrate 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class MainConfig(AppConfig): 7 | name = "ami.main" 8 | verbose_name = _("Main") 9 | 10 | def ready(self): 11 | import ami.main.signals # noqa: F401 12 | from ami.tests.fixtures.signals import initialize_demo_project 13 | from ami.users.signals import create_roles 14 | 15 | post_migrate.connect(initialize_demo_project, sender=self) 16 | post_migrate.connect(create_roles, sender=self) 17 | -------------------------------------------------------------------------------- /ami/main/management/commands/import_source_images.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError # noqa 2 | 3 | from ...models import Deployment 4 | 5 | 6 | class Command(BaseCommand): 7 | r"""Import source images from s3 bucket configured for Deployment.""" 8 | 9 | help = "Import trap data from a Deployment's data source" 10 | 11 | def add_arguments(self, parser): 12 | parser.add_argument("deployment_id", type=int) 13 | 14 | def handle(self, *args, **options): 15 | deployment_id = options["deployment_id"] 16 | deployment = Deployment.objects.get(id=deployment_id) 17 | created = deployment.import_captures() 18 | msg = f"Imported {len(created)} source images for {deployment}" 19 | self.stdout.write(self.style.SUCCESS(msg)) 20 | -------------------------------------------------------------------------------- /ami/main/management/main.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/management/main.py -------------------------------------------------------------------------------- /ami/main/migrations/0002_remove_identification_unique_primary_identification_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-21 20:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveConstraint( 13 | model_name="identification", 14 | name="unique_primary_identification", 15 | ), 16 | migrations.AddConstraint( 17 | model_name="identification", 18 | constraint=models.UniqueConstraint( 19 | condition=models.Q(("primary", True)), fields=("occurrence",), name="unique_primary_identification" 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /ami/main/migrations/0004_search_extensions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-22 18:06 2 | 3 | from django.db import migrations 4 | from django.contrib.postgres.operations import TrigramExtension 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0003_alter_identification_options_and_more"), 10 | ] 11 | 12 | operations = [TrigramExtension()] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0005_taxon_display_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-22 21:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0004_search_extensions"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="taxon", 14 | name="display_name", 15 | field=models.CharField( 16 | blank=True, max_length=255, null=True, unique=True, verbose_name="Cached display name" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ami/main/migrations/0006_merge_20230926_2353.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-09-26 23:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0004_identification_agreed_with_identification_and_more"), 9 | ("main", "0005_taxon_display_name"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0009_alter_sourceimageupload_source_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-10-16 23:43 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0008_rename_capture_sourceimageupload_source_image_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="sourceimageupload", 15 | name="source_image", 16 | field=models.OneToOneField( 17 | blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="main.sourceimage" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /ami/main/migrations/0013_sourceimage_detections_count.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-10-31 17:17 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0012_alter_taxon_rank"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="sourceimage", 15 | name="detections_count", 16 | field=models.IntegerField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ami/main/migrations/0015_delete_job.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-02 00:50 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0014_job_source_image_collection_job_source_image_single_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.DeleteModel( 13 | name="Job", 14 | ), 15 | ] 16 | -------------------------------------------------------------------------------- /ami/main/migrations/0017_alter_site_options_site_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-23 02:04 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0016_pipeline"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="site", 15 | options={"verbose_name": "Research Site"}, 16 | ), 17 | migrations.AddField( 18 | model_name="site", 19 | name="project", 20 | field=models.ForeignKey( 21 | null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="sites", to="main.project" 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /ami/main/migrations/0017_remove_pipeline_algorithms.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-10 01:33 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0016_pipeline"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="pipeline", 14 | name="algorithms", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/main/migrations/0018_delete_existing_pipeline_algorithms.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-10 01:33 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("ml", "0001_initial"), 10 | ("main", "0017_remove_pipeline_algorithms"), 11 | ("jobs", "0008_alter_job_pipeline"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RunSQL("", ""), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/main/migrations/0018_device_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-23 02:09 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0017_alter_site_options_site_project"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="device", 15 | name="project", 16 | field=models.ForeignKey( 17 | null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="devices", to="main.project" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /ami/main/migrations/0019_s3storagesource_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-23 02:21 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0018_device_project"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="s3storagesource", 15 | name="project", 16 | field=models.ForeignKey( 17 | null=True, 18 | on_delete=django.db.models.deletion.SET_NULL, 19 | related_name="storage_sources", 20 | to="main.project", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/main/migrations/0020_detection_detection_algorithm.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-10 01:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("ml", "0001_initial"), 10 | ("main", "0019_delete_pipeline_remove_detection_detection_algorithm_and_more"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="detection", 16 | name="detection_algorithm", 17 | field=models.ForeignKey( 18 | blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="ml.algorithm" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ami/main/migrations/0021_merge_20231123_0307.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-23 03:07 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0019_s3storagesource_project"), 9 | ("main", "0020_detection_detection_algorithm"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0023_taxon_main_taxon_orderin_4ffb7b_idx.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-29 23:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0022_alter_sourceimagecollection_method_alter_taxon_rank"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddIndex( 13 | model_name="taxon", 14 | index=models.Index(fields=["ordering", "name"], name="main_taxon_orderin_4ffb7b_idx"), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/main/migrations/0026_occurrence_determination_score.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-12-02 01:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0025_update_deployment_aggregates"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="occurrence", 14 | name="determination_score", 15 | field=models.FloatField(blank=True, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/main/migrations/0027_update_occurrence_scores.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-12-02 01:08 2 | 3 | from django.db import migrations 4 | 5 | 6 | # Call save on all occurrences to update their scores 7 | def update_occurrence_scores(apps, schema_editor): 8 | Occurrence = apps.get_model("main", "Occurrence") 9 | # from ami.main.models import Occurrence 10 | 11 | for occurrence in Occurrence.objects.all(): 12 | occurrence.save() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | dependencies = [ 17 | ("main", "0026_occurrence_determination_score"), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(update_occurrence_scores, migrations.RunPython.noop), 22 | ] 23 | -------------------------------------------------------------------------------- /ami/main/migrations/0030_identification_comment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2024-04-16 18:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0029_alter_deployment_device_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="identification", 14 | name="comment", 15 | field=models.TextField(blank=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/main/migrations/0032_alter_s3storagesource_public_base_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2024-05-07 20:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0031_s3storagesource_use_presigned_urls_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="s3storagesource", 14 | name="public_base_url", 15 | field=models.CharField(blank=True, max_length=255, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/main/migrations/0033_remove_s3storagesource_use_presigned_urls_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-07-09 19:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0032_alter_s3storagesource_public_base_url"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="s3storagesource", 14 | name="use_presigned_urls", 15 | ), 16 | migrations.AlterField( 17 | model_name="sourceimage", 18 | name="public_base_url", 19 | field=models.CharField(blank=True, max_length=255, null=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /ami/main/migrations/0044_alter_project_options_squashed_0047_alter_project_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-01-27 00:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | replaces = [ 8 | ("main", "0044_alter_project_options"), 9 | ("main", "0045_alter_project_options"), 10 | ("main", "0046_alter_project_options"), 11 | ("main", "0047_alter_project_options"), 12 | ] 13 | 14 | dependencies = [ 15 | ("main", "0039_project_users_squashed_0043_rename_users_project_members"), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterModelOptions( 20 | name="project", 21 | options={"ordering": ["-priority", "created_at"]}, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/main/migrations/0044_merge_20250124_2333.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-01-24 23:33 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0039_project_users_squashed_0043_rename_users_project_members"), 9 | ("main", "0040_alter_classification_logits_and_more"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0045_alter_classification_algorithm.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-16 16:00 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0044_merge_20250124_2333"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="classification", 15 | name="algorithm", 16 | field=models.ForeignKey( 17 | null=True, 18 | on_delete=django.db.models.deletion.SET_NULL, 19 | related_name="classifications", 20 | to="ml.algorithm", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/main/migrations/0052_merge_20250207_1012.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-07 10:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0044_merge_20250124_2333"), 9 | ("main", "0048_alter_project_options_squashed_0051_alter_project_options"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0053_alter_classification_algorithm.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-10 06:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("main", "0052_merge_20250207_1012"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="classification", 15 | name="algorithm", 16 | field=models.ForeignKey( 17 | null=True, 18 | on_delete=django.db.models.deletion.SET_NULL, 19 | related_name="classifications", 20 | to="ml.algorithm", 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/main/migrations/0056_merge_20250218_1405.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-18 14:05 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0045_alter_classification_algorithm"), 9 | ("main", "0054_alter_project_options_squashed_0055_alter_project_options"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/0057_merge_20250220_0022.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-20 00:22 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0046_add_taxon_common_name_placeholder"), 9 | ("main", "0056_merge_20250218_1405"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/main/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/migrations/__init__.py -------------------------------------------------------------------------------- /ami/ml/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/ml/__init__.py -------------------------------------------------------------------------------- /ami/ml/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class MLConfig(AppConfig): 6 | name = "ami.ml" 7 | verbose_name = _("Machine Learning") 8 | -------------------------------------------------------------------------------- /ami/ml/migrations/0002_pipeline_endpoint_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-22 02:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="pipeline", 14 | name="endpoint_url", 15 | field=models.URLField(blank=True, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/ml/migrations/0003_pipeline_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-22 03:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0002_pipeline_endpoint_url"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="pipeline", 14 | name="slug", 15 | field=models.SlugField(max_length=255, null=True, unique=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/ml/migrations/0004_populate_pipeline_slugs.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-22 03:03 2 | 3 | from django.db import migrations 4 | 5 | from django.utils.text import slugify 6 | 7 | 8 | def populate_pipeline_slugs(apps, schema_editor): 9 | Pipeline = apps.get_model("ml", "Pipeline") 10 | for pipeline in Pipeline.objects.all(): 11 | pipeline.slug = slugify(pipeline.name) 12 | pipeline.save() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | dependencies = [ 17 | ("ml", "0003_pipeline_slug"), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(populate_pipeline_slugs, migrations.RunPython.noop), 22 | ] 23 | -------------------------------------------------------------------------------- /ami/ml/migrations/0005_alter_pipeline_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-11-22 03:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0004_populate_pipeline_slugs"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="pipeline", 14 | name="slug", 15 | field=models.SlugField(default="2023-11-22 03:07:46.287011", max_length=255, unique=True), 16 | preserve_default=False, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ami/ml/migrations/0006_alter_pipeline_endpoint_url_alter_pipeline_projects.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-10-11 15:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("main", "0038_alter_detection_path_alter_sourceimage_event_and_more"), 9 | ("ml", "0005_alter_pipeline_slug"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="pipeline", 15 | name="endpoint_url", 16 | field=models.CharField(blank=True, max_length=1024, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="pipeline", 20 | name="projects", 21 | field=models.ManyToManyField(blank=True, related_name="pipelines", to="main.project"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /ami/ml/migrations/0010_alter_algorithm_version.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-12-05 01:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0009_algorithm_task_type"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="algorithm", 14 | name="version", 15 | field=models.IntegerField( 16 | default=1, help_text="An internal, sortable and incrementable version number for the model." 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /ami/ml/migrations/0012_alter_algorithm_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-12-05 04:20 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0011_alter_algorithm_task_type_alter_algorithm_url_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="algorithm", 14 | unique_together=set(), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/ml/migrations/0016_merge_20250117_2101.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-01-17 21:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0007_add_processing_service"), 9 | ("ml", "0015_update_existing_intermediate_classifications"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /ami/ml/migrations/0017_alter_algorithm_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2025-02-12 19:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("ml", "0016_merge_20250117_2101"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterUniqueTogether( 13 | name="algorithm", 14 | unique_together={("name", "version")}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /ami/ml/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/ml/migrations/__init__.py -------------------------------------------------------------------------------- /ami/ml/models/__init__.py: -------------------------------------------------------------------------------- 1 | from ami.ml.models.algorithm import Algorithm, AlgorithmCategoryMap 2 | from ami.ml.models.pipeline import Pipeline 3 | from ami.ml.models.processing_service import ProcessingService 4 | from ami.ml.models.project_pipeline_config import ProjectPipelineConfig 5 | 6 | __all__ = [ 7 | "Algorithm", 8 | "AlgorithmCategoryMap", 9 | "Pipeline", 10 | "ProcessingService", 11 | "ProjectPipelineConfig", 12 | ] 13 | -------------------------------------------------------------------------------- /ami/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /ami/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/static/fonts/.gitkeep -------------------------------------------------------------------------------- /ami/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /ami/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /ami/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Forbidden (403) 5 | {% endblock title %} 6 | {% block content %} 7 |

Forbidden (403)

8 |

9 | {% if exception %} 10 | {{ exception }} 11 | {% else %} 12 | You're not allowed to access this page. 13 | {% endif %} 14 |

15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /ami/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Page not found 5 | {% endblock title %} 6 | {% block content %} 7 |

Page not found

8 |

9 | {% if exception %} 10 | {{ exception }} 11 | {% else %} 12 | This is not the page you were looking for. 13 | {% endif %} 14 |

15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /ami/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Server Error 5 | {% endblock title %} 6 | {% block content %} 7 |

Ooops!!! 500

8 |

Looks like something went wrong!

9 |

10 | We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing. 11 |

12 | {% endblock content %} 13 | -------------------------------------------------------------------------------- /ami/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %} 6 | {% translate "Account Inactive" %} 7 | {% endblock head_title %} 8 | {% block inner %} 9 |

{% translate "Account Inactive" %}

10 |

{% translate "This account is inactive." %}

11 | {% endblock inner %} 12 | -------------------------------------------------------------------------------- /ami/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% block head_title %} 5 | {% endblock head_title %} 6 | {% endblock title %} 7 | {% block content %} 8 |
9 |
10 | {% block inner %} 11 | {% endblock inner %} 12 |
13 |
14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /ami/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %} 6 | {% translate "Sign Out" %} 7 | {% endblock head_title %} 8 | {% block inner %} 9 |

{% translate "Sign Out" %}

10 |

{% translate "Are you sure you want to sign out?" %}

11 |
12 | {% csrf_token %} 13 | {% if redirect_field_value %} 14 | 17 | {% endif %} 18 | 19 |
20 | {% endblock inner %} 21 | -------------------------------------------------------------------------------- /ami/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %} 7 | {% translate "Change Password" %} 8 | {% endblock head_title %} 9 | {% block inner %} 10 |

{% translate "Change Password" %}

11 |
14 | {% csrf_token %} 15 | {{ form|crispy }} 16 | 17 |
18 | {% endblock inner %} 19 | -------------------------------------------------------------------------------- /ami/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %} 7 | {% translate "Password Reset" %} 8 | {% endblock head_title %} 9 | {% block inner %} 10 |

{% translate "Password Reset" %}

11 | {% if user.is_authenticated %} 12 | {% include "account/snippets/already_logged_in.html" %} 13 | {% endif %} 14 |

15 | {% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %} 16 |

17 | {% endblock inner %} 18 | -------------------------------------------------------------------------------- /ami/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %} 6 | {% translate "Change Password" %} 7 | {% endblock head_title %} 8 | {% block inner %} 9 |

{% translate "Change Password" %}

10 |

{% translate "Your password is now changed." %}

11 | {% endblock inner %} 12 | -------------------------------------------------------------------------------- /ami/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %} 7 | {% translate "Set Password" %} 8 | {% endblock head_title %} 9 | {% block inner %} 10 |

{% translate "Set Password" %}

11 |
14 | {% csrf_token %} 15 | {{ form|crispy }} 16 | 20 |
21 | {% endblock inner %} 22 | -------------------------------------------------------------------------------- /ami/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %} 6 | {% translate "Sign Up Closed" %} 7 | {% endblock head_title %} 8 | {% block inner %} 9 |

{% translate "Sign Up Closed" %}

10 |

{% translate "We are sorry, but the sign up is currently closed." %}

11 | {% endblock inner %} 12 | -------------------------------------------------------------------------------- /ami/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %} 6 | {% translate "Verify Your E-mail Address" %} 7 | {% endblock head_title %} 8 | {% block inner %} 9 |

{% translate "Verify Your E-mail Address" %}

10 |

11 | {% blocktranslate %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %} 12 |

13 | {% endblock inner %} 14 | -------------------------------------------------------------------------------- /ami/templates/labelstudio/binary_classification.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Deployment: 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ami/templates/labelstudio/initial_object_detection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /ami/templates/labelstudio/species_classification.xml: -------------------------------------------------------------------------------- 1 | 2 | Deployment: 3 | 4 | 5 | 6 | {{ label_config.taxonomy_choices_xml|safe }} 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /ami/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /ami/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /ami/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_tags %} 4 | 5 | {% block title %} 6 | {{ user.name }} 7 | {% endblock title %} 8 | {% block content %} 9 |

{{ user.name }}

10 | 13 | {% csrf_token %} 14 | {{ form|crispy }} 15 |
16 |
17 | 18 |
19 |
20 | 21 | {% endblock content %} 22 | -------------------------------------------------------------------------------- /ami/tests/fixtures/signals.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | from ami.main.models import Project 6 | 7 | from .main import create_complete_test_project, create_local_admin_user, update_site_settings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def initialize_demo_project(sender, **kwargs): 13 | """ 14 | Signal handler to create a demo project after `migrate` is run. 15 | """ 16 | if not Project.objects.exists(): 17 | update_site_settings(domain=settings.EXTERNAL_HOSTNAME) 18 | create_complete_test_project() 19 | create_local_admin_user() 20 | -------------------------------------------------------------------------------- /ami/tests/test_doctests.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import pkgutil 3 | 4 | import ami as root_package 5 | 6 | 7 | def load_tests(loader, tests, ignore): 8 | modules = pkgutil.walk_packages(root_package.__path__, root_package.__name__ + ".") 9 | for _, module_name, _ in modules: 10 | try: 11 | suite = doctest.DocTestSuite(module_name) 12 | except ValueError: 13 | # Presumably a "no docstrings" error. That's OK. 14 | pass 15 | except ModuleNotFoundError: 16 | pass 17 | else: 18 | tests.addTests(suite) 19 | return tests 20 | -------------------------------------------------------------------------------- /ami/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/__init__.py -------------------------------------------------------------------------------- /ami/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "ami.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import ami.users.signals # noqa: F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /ami/users/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def allauth_settings(request): 5 | """Expose some settings from django-allauth in templates.""" 6 | return { 7 | "ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION, 8 | } 9 | -------------------------------------------------------------------------------- /ami/users/migrations/0002_user_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-09-08 02:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("users", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="image", 15 | field=models.ImageField(blank=True, null=True, upload_to="users"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /ami/users/migrations/0003_lowercase_existing_emails.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-09-19 17:54 2 | from django.db import migrations 3 | 4 | 5 | def lowercase_emails(apps, schema_editor): 6 | User = apps.get_model("users", "User") 7 | for user in User.objects.all(): 8 | lowercase_email = user.email.lower() 9 | if user.email != lowercase_email: 10 | user.email = lowercase_email 11 | user.save(update_fields=["email"]) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ("users", "0002_user_image"), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(lowercase_emails), 22 | ] 23 | -------------------------------------------------------------------------------- /ami/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/migrations/__init__.py -------------------------------------------------------------------------------- /ami/users/tasks.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from config import celery_app 4 | 5 | User = get_user_model() 6 | 7 | 8 | @celery_app.task() 9 | def get_users_count(): 10 | """A pointless Celery task to demonstrate usage.""" 11 | return User.objects.count() 12 | -------------------------------------------------------------------------------- /ami/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/tests/__init__.py -------------------------------------------------------------------------------- /ami/users/tests/test_drf_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import resolve, reverse 2 | 3 | from ami.users.models import User 4 | 5 | 6 | def test_user_detail(user: User): 7 | assert reverse("api:user-detail", kwargs={"id": user.pk}) == f"/api/v2/users/{user.pk}/" 8 | assert resolve(f"/api/v2/users/{user.pk}/").view_name == "api:user-detail" 9 | 10 | 11 | def test_user_list(): 12 | assert reverse("api:user-list") == "/api/v2/users/" 13 | assert resolve("/api/v2/users/").view_name == "api:user-list" 14 | 15 | 16 | def test_user_me(): 17 | assert reverse("api:user-me") == "/api/v2/users/me/" 18 | assert resolve("/api/v2/users/me/").view_name == "api:user-me" 19 | -------------------------------------------------------------------------------- /ami/users/tests/test_drf_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from djoser.views import UserViewSet 3 | from rest_framework.test import APIRequestFactory 4 | 5 | from ami.users.models import User 6 | 7 | 8 | # Djoser has its own tests for the UserViewSet, so we only need to test our own code. 9 | class TestUserViewSet: 10 | @pytest.fixture 11 | def api_rf(self) -> APIRequestFactory: 12 | return APIRequestFactory() 13 | 14 | def test_get_queryset(self, user: User, api_rf: APIRequestFactory): 15 | view = UserViewSet() 16 | view.action = "list" 17 | request = api_rf.get("/fake-url/") 18 | request.user = user 19 | 20 | view.request = request 21 | 22 | assert user in view.get_queryset() 23 | -------------------------------------------------------------------------------- /ami/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from ami.users.models import User 2 | 3 | 4 | def test_user_get_absolute_url(user: User): 5 | assert user.get_absolute_url() == f"/api/v2/users/{user.pk}/" 6 | -------------------------------------------------------------------------------- /ami/users/tests/test_swagger.py: -------------------------------------------------------------------------------- 1 | import pytest # noqa 2 | from django.urls import reverse 3 | 4 | 5 | def test_swagger_accessible_by_admin(admin_client): 6 | url = reverse("api-docs") 7 | response = admin_client.get(url) 8 | assert response.status_code == 200 9 | 10 | 11 | # @TODO re-enable once we configure authentication 12 | # @pytest.mark.django_db 13 | # def test_swagger_ui_not_accessible_by_normal_user(client): 14 | # url = reverse("api-docs") 15 | # response = client.get(url) 16 | # assert response.status_code == 403 17 | 18 | 19 | def test_api_schema_generated_successfully(admin_client): 20 | url = reverse("api-schema") 21 | response = admin_client.get(url) 22 | assert response.status_code == 200 23 | -------------------------------------------------------------------------------- /ami/users/tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from celery.result import EagerResult 3 | 4 | from ami.users.tasks import get_users_count 5 | from ami.users.tests.factories import UserFactory 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | def test_user_count(settings): 11 | """A basic test to execute the get_users_count Celery task.""" 12 | UserFactory.create_batch(3) 13 | settings.CELERY_TASK_ALWAYS_EAGER = True 14 | task_result = get_users_count.delay() 15 | assert isinstance(task_result, EagerResult) 16 | assert task_result.result == 3 17 | -------------------------------------------------------------------------------- /ami/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ami.users.views import user_detail_view, user_redirect_view, user_update_view 4 | 5 | app_name = "users" 6 | urlpatterns = [ 7 | path("~redirect/", view=user_redirect_view, name="redirect"), 8 | path("~update/", view=user_update_view, name="update"), 9 | path("/", view=user_detail_view, name="detail"), 10 | ] 11 | -------------------------------------------------------------------------------- /ami/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import dates, s3 2 | 3 | __all__ = ["dates", "s3"] 4 | -------------------------------------------------------------------------------- /ami/utils/fields.py: -------------------------------------------------------------------------------- 1 | from django.forms import BooleanField 2 | from rest_framework.request import Request 3 | 4 | 5 | def url_boolean_param(request: Request, param: str, default: bool = False) -> bool: 6 | """ 7 | The presence of the parameter in the query string with no value indicates True. 8 | 9 | If the parameter is present and has a value, it is parsed as a BooleanField. 10 | Which means that "true", "True", "1", and "yes" are all True, and everything 11 | else is False. 12 | """ 13 | try: 14 | value = request.query_params[param] 15 | except KeyError: 16 | value = False 17 | else: 18 | if value == "": 19 | value = True 20 | else: 21 | value = BooleanField(required=False).clean(value) 22 | 23 | return value or default 24 | -------------------------------------------------------------------------------- /ami/utils/storages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from storages.backends.s3boto3 import S3Boto3Storage 4 | 5 | IMAGE_FILE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "tiff", "tif"] 6 | 7 | 8 | class StaticRootS3Boto3Storage(S3Boto3Storage): 9 | location = "static" 10 | default_acl = "public-read" 11 | 12 | 13 | class MediaRootS3Boto3Storage(S3Boto3Storage): 14 | location = "media" 15 | file_overwrite = False 16 | 17 | 18 | @dataclass 19 | class ConnectionTestResult: 20 | connection_successful: bool 21 | prefix_exists: bool 22 | latency: float 23 | total_time: float 24 | error_code: str | None 25 | error_message: str | None 26 | files_checked: int 27 | first_file_found: str | None 28 | full_uri: str | None 29 | -------------------------------------------------------------------------------- /compose/local/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | rm -f './celerybeat.pid' 8 | exec watchfiles --filter python celery.__main__.main --args '-A config.celery_app beat -l INFO' 9 | -------------------------------------------------------------------------------- /compose/local/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | exec watchfiles --filter python celery.__main__.main \ 7 | --args \ 8 | "-A config.celery_app -b \"${CELERY_BROKER_URL}\" flower --db=\"${CELERY_FLOWER_DB-/data/flower.db}\"" 9 | -------------------------------------------------------------------------------- /compose/local/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | exec watchfiles --filter python celery.__main__.main --args '-A config.celery_app worker -l INFO' 8 | -------------------------------------------------------------------------------- /compose/local/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | exec uvicorn config.asgi:application --host 0.0.0.0 --reload --reload-include '*.html' 10 | -------------------------------------------------------------------------------- /compose/local/docs/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | exec make livehtml 8 | -------------------------------------------------------------------------------- /compose/local/minio/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Create a default bucket 4 | # /usr/bin/mc set alias minio "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" 5 | /usr/bin/mc config host add local "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" 6 | /usr/bin/mc mb local/"${MINIO_DEFAULT_BUCKET}" --ignore-existing 7 | /usr/bin/mc mb local/"${MINIO_TEST_BUCKET}" --ignore-existing 8 | 9 | # Give it public read access 10 | /usr/bin/mc anonymous set public local/"${MINIO_DEFAULT_BUCKET}" 11 | /usr/bin/mc anonymous set public local/"${MINIO_TEST_BUCKET}" 12 | 13 | exit 0 14 | -------------------------------------------------------------------------------- /compose/local/minio/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | server { 9 | listen 9000 default; 10 | server_name _; 11 | 12 | location / { 13 | # Set Host header to mitigate MinIO's signature discrepancy issue 14 | # e.g. "SignatureDoesNotMatch: The request signature we calculated 15 | # does not match the signature you provided. Check your key and signing method." 16 | proxy_set_header Host minio:9000; 17 | proxy_pass http://minio:9000; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /compose/local/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:16 2 | # FROM esgn/pgtuned:latest 3 | 4 | COPY ./compose/local/postgres/maintenance /usr/local/bin/maintenance 5 | RUN chmod +x /usr/local/bin/maintenance/* 6 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 7 | && rmdir /usr/local/bin/maintenance 8 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /compose/local/ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Node version should always match the version in `ui/.nvmrc` 2 | FROM node:18 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Configure git to trust the /app directory 8 | RUN git config --global --add safe.directory /app 9 | 10 | # Don't try to open a browser 11 | ENV BROWSER=none 12 | 13 | # Expose the port the app runs on 14 | EXPOSE 4000 15 | 16 | # Check for changed app dependencies on every start 17 | CMD ["sh", "-c", "yarn install && yarn start --debug --host 0.0.0.0 --port 4000"] 18 | -------------------------------------------------------------------------------- /compose/production/aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM garland/aws-cli-docker:1.16.140 2 | 3 | COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance 4 | COPY ./compose/local/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced 5 | 6 | RUN chmod +x /usr/local/bin/maintenance/* 7 | 8 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 9 | && rmdir /usr/local/bin/maintenance 10 | -------------------------------------------------------------------------------- /compose/production/aws/maintenance/download: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Download a file from your Amazon S3 bucket to the postgres /backups folder 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f production.yml run --rm awscli <1> 7 | 8 | set -o errexit 9 | set -o pipefail 10 | set -o nounset 11 | 12 | working_dir="$(dirname ${0})" 13 | source "${working_dir}/_sourced/constants.sh" 14 | source "${working_dir}/_sourced/messages.sh" 15 | 16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}" 17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}" 18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}" 19 | 20 | 21 | aws s3 cp s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH}/${1} ${BACKUP_DIR_PATH}/${1} 22 | 23 | message_success "Finished downloading ${1}." 24 | -------------------------------------------------------------------------------- /compose/production/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | exec celery -A config.celery_app beat -l INFO 9 | -------------------------------------------------------------------------------- /compose/production/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | exec celery \ 8 | -A config.celery_app \ 9 | -b "${CELERY_BROKER_URL}" \ 10 | flower \ 11 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 12 | --db="${CELERY_FLOWER_DB:-/data/flower.db}" 13 | -------------------------------------------------------------------------------- /compose/production/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | exec newrelic-admin run-program celery -A config.celery_app worker -l INFO 8 | -------------------------------------------------------------------------------- /compose/production/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | python /app/manage.py collectstatic --noinput 8 | 9 | exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker 10 | -------------------------------------------------------------------------------- /compose/production/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17.8-alpine 2 | COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf 3 | -------------------------------------------------------------------------------- /compose/production/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | location /media/ { 5 | alias /usr/share/nginx/media/; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compose/production/traefik/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM traefik:2.10.3 2 | RUN mkdir -p /etc/traefik/acme \ 3 | && touch /etc/traefik/acme/acme.json \ 4 | && chmod 600 /etc/traefik/acme/acme.json 5 | COPY ./compose/production/traefik/traefik.yml /etc/traefik 6 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery_app import app as celery_app 4 | 5 | __all__ = ("celery_app",) 6 | -------------------------------------------------------------------------------- /config/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 7 | 8 | app = Celery("ami") 9 | 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | 16 | # Load task modules from all registered Django app configs. 17 | app.autodiscover_tasks() 18 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/config/settings/__init__.py -------------------------------------------------------------------------------- /config/websocket.py: -------------------------------------------------------------------------------- 1 | async def websocket_application(scope, receive, send): 2 | while True: 3 | event = await receive() 4 | 5 | if event["type"] == "websocket.connect": 6 | await send({"type": "websocket.accept"}) 7 | 8 | if event["type"] == "websocket.disconnect": 9 | break 10 | 11 | if event["type"] == "websocket.receive": 12 | if event["text"] == "ping": 13 | await send({"type": "websocket.send", "text": "pong!"}) 14 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | This data folder is not kept under version control. 2 | 3 | However it must exist for `docker compose up` to work on its own. 4 | 5 | Use this folder for database snapshots, local test images, backups, etc. 6 | -------------------------------------------------------------------------------- /data/db/snapshots/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/data/db/snapshots/.gitkeep -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory 2 | -------------------------------------------------------------------------------- /docs/diagrams/models.mermaid: -------------------------------------------------------------------------------- 1 | classDiagram 2 | Project -- Deployment 3 | Site -- Deployment 4 | DeviceType -- Deployment 5 | Deployment -- Event 6 | Project -- Site 7 | Event -- SourceImage 8 | Event -- SourceImageCollection 9 | SourceImageCollection -- SourceImage 10 | User -- Identification 11 | Identification -- Determination 12 | Pipeline -- Algorithms 13 | Job --> Detections : bbox, image embedding 14 | Job --> Predictions : classification scores 15 | Detections --> Occurrence : tracking 16 | Occurrence -- Determination 17 | Job <-- SourceImageCollection 18 | Job <-- Pipeline 19 | Project -- TaxaList 20 | TaxaList -- Determination 21 | Predictions -- Determination 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Automated Monitoring of Insects ML Platform documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Automated Monitoring of Insects ML Platform's documentation! 7 | ====================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | howto 14 | users 15 | 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/users.rst: -------------------------------------------------------------------------------- 1 | .. _users: 2 | 3 | Users 4 | ====================================================================== 5 | 6 | Starting a new project, it’s highly recommended to set up a custom user model, 7 | even if the default User model is sufficient for you. 8 | 9 | This model behaves identically to the default user model, 10 | but you’ll be able to customize it in the future if the need arises. 11 | 12 | .. automodule:: ami.users.models 13 | :members: 14 | :noindex: 15 | 16 | -------------------------------------------------------------------------------- /locale/en_US/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Translations for the Automated Monitoring of Insects ML Platform project 2 | # Copyright (C) 2023 Rolnick Lab 3 | # Rolnick Lab , 2023. 4 | # 5 | #, fuzzy 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1.0\n" 9 | "Language: en-US\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | -------------------------------------------------------------------------------- /merge_production_dotenvs_in_dotenv.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections.abc import Sequence 3 | from pathlib import Path 4 | 5 | BASE_DIR = Path(__file__).parent.resolve() 6 | PRODUCTION_DOTENVS_DIR = BASE_DIR / ".envs" / ".production" 7 | PRODUCTION_DOTENV_FILES = [ 8 | PRODUCTION_DOTENVS_DIR / ".django", 9 | PRODUCTION_DOTENVS_DIR / ".postgres", 10 | ] 11 | DOTENV_FILE = BASE_DIR / ".env" 12 | 13 | 14 | def merge( 15 | output_file: Path, 16 | files_to_merge: Sequence[Path], 17 | ) -> None: 18 | merged_content = "" 19 | for merge_file in files_to_merge: 20 | merged_content += merge_file.read_text() 21 | merged_content += os.linesep 22 | output_file.write_text(merged_content) 23 | 24 | 25 | if __name__ == "__main__": 26 | merge(DOTENV_FILE, PRODUCTION_DOTENV_FILES) 27 | -------------------------------------------------------------------------------- /processing_services/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ml_backend_minimal: 3 | build: 4 | context: ./minimal 5 | volumes: 6 | - ./minimal/:/app:z 7 | ports: 8 | - "2000:2000" 9 | extra_hosts: 10 | - minio:host-gateway 11 | networks: 12 | - antenna_network 13 | 14 | ml_backend_example: 15 | build: 16 | context: ./example 17 | volumes: 18 | - ./example/:/app:z 19 | ports: 20 | - "2003:2000" 21 | extra_hosts: 22 | - minio:host-gateway 23 | networks: 24 | - antenna_network 25 | 26 | networks: 27 | antenna_network: 28 | name: antenna_network 29 | -------------------------------------------------------------------------------- /processing_services/example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | # Set up ml backend FastAPI 4 | WORKDIR /app 5 | COPY . /app 6 | RUN pip install -r ./requirements.txt 7 | CMD ["python", "/app/main.py"] 8 | -------------------------------------------------------------------------------- /processing_services/example/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/processing_services/example/api/__init__.py -------------------------------------------------------------------------------- /processing_services/example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ml_backend_example: 3 | build: 4 | context: . 5 | volumes: 6 | - ./:/app:z 7 | ports: 8 | - "2003:2000" 9 | extra_hosts: 10 | - minio:host-gateway 11 | networks: 12 | - antenna_network 13 | deploy: 14 | resources: 15 | reservations: 16 | devices: 17 | - driver: nvidia 18 | count: 1 19 | capabilities: [gpu] 20 | 21 | networks: 22 | antenna_network: 23 | name: antenna_network 24 | -------------------------------------------------------------------------------- /processing_services/example/main.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import uvicorn 3 | 4 | uvicorn.run("api.api:app", host="0.0.0.0", port=2000, reload=True) 5 | -------------------------------------------------------------------------------- /processing_services/example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pydantic 4 | Pillow 5 | requests 6 | transformers==4.50.3 7 | torch==2.6.0 8 | torchvision==0.21.0 9 | -------------------------------------------------------------------------------- /processing_services/minimal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN pip install -r ./requirements.txt 8 | 9 | CMD ["python", "/app/main.py"] 10 | -------------------------------------------------------------------------------- /processing_services/minimal/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/processing_services/minimal/api/__init__.py -------------------------------------------------------------------------------- /processing_services/minimal/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ml_backend_minimal: 3 | build: 4 | context: . 5 | volumes: 6 | - ./:/app:z 7 | ports: 8 | - "2000:2000" 9 | extra_hosts: 10 | - minio:host-gateway 11 | networks: 12 | - antenna_network 13 | 14 | networks: 15 | antenna_network: 16 | name: antenna_network 17 | -------------------------------------------------------------------------------- /processing_services/minimal/main.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import uvicorn 3 | 4 | uvicorn.run("api.api:app", host="0.0.0.0", port=2000, reload=True) 5 | -------------------------------------------------------------------------------- /processing_services/minimal/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pydantic 4 | Pillow 5 | requests 6 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # PRECAUTION: avoid production dependencies that aren't in development 2 | 3 | -r base.txt 4 | 5 | 6 | # Django 7 | # ------------------------------------------------------------------------------ 8 | # django-anymail[mailgun]==10.0 # https://github.com/anymail/django-anymail 9 | # django-anymail[sendgrid]==10.0 # https://github.com/anymail/django-anymail 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # flake8 and pycodestyle don't support pyproject.toml 2 | # https://github.com/PyCQA/flake8/issues/234 3 | # https://github.com/PyCQA/pycodestyle/issues/813 4 | [flake8] 5 | max-line-length = 119 6 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv 7 | 8 | [pycodestyle] 9 | max-line-length = 119 10 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv 11 | -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "rules": { 11 | "no-console": 1, 12 | "@typescript-eslint/no-unused-vars": 1, 13 | "@typescript-eslint/no-empty-function": 0, 14 | "@typescript-eslint/no-explicit-any": 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/.gitattributes: -------------------------------------------------------------------------------- 1 | .yarn/releases/** binary 2 | .yarn/plugins/** binary 3 | 4 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # sass 22 | /.sass-cache 23 | 24 | # Yarn 3 files 25 | .pnp.* 26 | .yarn/* 27 | !.yarn/patches 28 | !.yarn/plugins 29 | !.yarn/releases 30 | !.yarn/sdks 31 | !.yarn/versions 32 | -------------------------------------------------------------------------------- /ui/.nvmrc: -------------------------------------------------------------------------------- 1 | v18.12.0 2 | 3 | # This should always match the version in `local/ui/Dockerfile` 4 | -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /ui/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-berry.cjs 4 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /ui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #47518f 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon-16x16.png -------------------------------------------------------------------------------- /ui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon-32x32.png -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-bold.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-bolditalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-bolditalic.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-italic.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-medium.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-mediumitalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-mediumitalic.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-regular.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-semibold.otf -------------------------------------------------------------------------------- /ui/public/fonts/Mazzard/mazzardsoftm-semibolditalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-semibolditalic.otf -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Antenna Data Platform", 3 | "short_name": "Antenna", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone", 19 | "start_url": "." 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/components/breadcrumbs/breadcrumbs.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .breadcrumbs { 5 | display: flex; 6 | align-items: center; 7 | 8 | > :not(:last-child) { 9 | margin-right: 16px; 10 | } 11 | } 12 | 13 | .breadcrumb { 14 | display: block; 15 | @include paragraph-x-small(); 16 | font-weight: 600; 17 | color: $color-neutral-700; 18 | 19 | span { 20 | &:last-child { 21 | display: none; 22 | } 23 | } 24 | 25 | &.link { 26 | color: $color-primary-1-600; 27 | } 28 | } 29 | 30 | @media only screen and (max-width: $small-screen-breakpoint) { 31 | .breadcrumb:not(:first-child):not(:last-child) { 32 | span { 33 | &:first-child { 34 | display: none; 35 | } 36 | &:last-child { 37 | display: inline; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/components/constants.ts: -------------------------------------------------------------------------------- 1 | export const BREAKPOINTS = { 2 | SM: 576, 3 | MD: 768, 4 | LG: 1024, 5 | XL: 1280, 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/components/error-boundary/error-boundary.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .wrapper { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | gap: 16px; 10 | width: 100%; 11 | height: 100%; 12 | padding: 32px; 13 | background-color: $color-generic-white; 14 | @include paragraph-medium(); 15 | color: $color-neutral-700; 16 | box-sizing: border-box; 17 | } 18 | 19 | .iconWrapper { 20 | display: flex; 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/components/fetch-info/fetch-info.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | @import 'src/design-system/variables/variables.scss'; 4 | 5 | .wrapper { 6 | height: 32px; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | z-index: 1; 11 | 12 | :not(:last-child) { 13 | margin-right: 6px; 14 | } 15 | 16 | span { 17 | @include paragraph-x-small(); 18 | font-weight: 600; 19 | color: $color-neutral-300; 20 | } 21 | } 22 | 23 | @media only screen and (max-width: $small-screen-breakpoint) { 24 | .wrapper { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/fetch-info/fetch-info.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSpinner } from 'design-system/components/loading-spinner/loading-spinner' 2 | import { STRING, translate } from 'utils/language' 3 | import styles from './fetch-info.module.scss' 4 | 5 | enum FetchInfoType { 6 | Loading = 'loading', 7 | Updating = 'updating', 8 | } 9 | 10 | const messages: { [key in FetchInfoType]: string } = { 11 | [FetchInfoType.Loading]: translate(STRING.LOADING_DATA), 12 | [FetchInfoType.Updating]: translate(STRING.UPDATING_DATA), 13 | } 14 | 15 | export const FetchInfo = ({ isLoading }: { isLoading?: boolean }) => { 16 | const type = isLoading ? FetchInfoType.Loading : FetchInfoType.Updating 17 | 18 | return ( 19 |
20 | 21 | {messages[type]}... 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/filtering/filters/image-filter.tsx: -------------------------------------------------------------------------------- 1 | import { useCaptureDetails } from 'data-services/hooks/captures/useCaptureDetails' 2 | import { FilterProps } from './types' 3 | 4 | export const ImageFilter = ({ value }: FilterProps) => { 5 | const { capture, isLoading } = useCaptureDetails(value) 6 | 7 | const label = (() => { 8 | if (capture) { 9 | return `#${capture.id}` 10 | } 11 | if (value && isLoading) { 12 | return 'Loading...' 13 | } 14 | return 'All images' 15 | })() 16 | 17 | return ( 18 |
19 | {label} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/components/filtering/filters/session-filter.tsx: -------------------------------------------------------------------------------- 1 | import { useSessionDetails } from 'data-services/hooks/sessions/useSessionDetails' 2 | import { FilterProps } from './types' 3 | 4 | export const SessionFilter = ({ value }: FilterProps) => { 5 | const { session, isLoading } = useSessionDetails(value) 6 | 7 | const label = (() => { 8 | if (session) { 9 | return session.label 10 | } 11 | if (value && isLoading) { 12 | return 'Loading...' 13 | } 14 | return 'All sessions' 15 | })() 16 | 17 | return ( 18 |
19 | {label} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/components/filtering/filters/taxa-list-filter.tsx: -------------------------------------------------------------------------------- 1 | import { TaxaList } from 'data-services/models/taxa-list' 2 | import { Select } from 'nova-ui-kit' 3 | import { FilterProps } from './types' 4 | 5 | export const TaxaListFilter = ({ data = [], value, onAdd }: FilterProps) => { 6 | const taxaLists = data as TaxaList[] 7 | 8 | return ( 9 | 14 | 15 | 16 | 17 | 18 | {taxaLists.map((list) => ( 19 | 20 | {list.name} 21 | 22 | ))} 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/components/filtering/filters/type-filter.tsx: -------------------------------------------------------------------------------- 1 | import { Job, SERVER_JOB_TYPES } from 'data-services/models/job' 2 | import { Select } from 'nova-ui-kit' 3 | import { FilterProps } from './types' 4 | 5 | const OPTIONS = SERVER_JOB_TYPES.map((key) => { 6 | const typeInfo = Job.getJobTypeInfo(key) 7 | 8 | return { 9 | ...typeInfo, 10 | } 11 | }) 12 | 13 | export const TypeFilter = ({ value, onAdd }: FilterProps) => ( 14 | 15 | 16 | 17 | 18 | 19 | {OPTIONS.map((option) => ( 20 | 21 | {option.label} 22 | 23 | ))} 24 | 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /ui/src/components/filtering/filters/types.ts: -------------------------------------------------------------------------------- 1 | export interface FilterProps { 2 | data?: any 3 | error?: string 4 | onAdd: (value: string) => void 5 | onClear: () => void 6 | value: string | undefined 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/components/filtering/utils.ts: -------------------------------------------------------------------------------- 1 | // Help functions to handle boolean filters (search param values are defined as strings and need to be converted) 2 | export const stringToBoolean = (string?: string) => { 3 | switch (string?.toLowerCase()) { 4 | case 'true': 5 | case '1': 6 | return true 7 | case 'false': 8 | case '0': 9 | return false 10 | default: 11 | return undefined 12 | } 13 | } 14 | 15 | export const booleanToString = (value?: boolean) => 16 | value !== undefined ? `${value}` : '' 17 | 18 | // Help function to decide if a filter section should be open or not on page load 19 | export const someActive = ( 20 | fields: string[], 21 | activeFilters: { field: string }[] 22 | ) => activeFilters.some(({ field }) => fields.includes(field)) 23 | -------------------------------------------------------------------------------- /ui/src/components/form/types.ts: -------------------------------------------------------------------------------- 1 | export interface FieldConfig { 2 | label: string 3 | description?: string 4 | rules?: { 5 | required?: boolean 6 | minLength?: number 7 | maxLength?: number 8 | min?: number 9 | max?: number 10 | validate?: (value: any) => string | undefined 11 | } 12 | } 13 | 14 | export type FormConfig = { 15 | [name: string]: FieldConfig 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/components/header/user-info-dialog/user-info-form/user-info-form.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .miniForm { 5 | border-radius: 8px; 6 | border: 1px solid $color-neutral-100; 7 | overflow: hidden; 8 | } 9 | 10 | .miniFormContent { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 16px; 14 | padding: 16px; 15 | } 16 | 17 | .miniFormActions { 18 | display: flex; 19 | justify-content: flex-end; 20 | gap: 16px; 21 | padding-top: 16px; 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .container { 5 | width: 100%; 6 | height: 0; 7 | padding-bottom: 100%; 8 | background-color: $color-primary-2-50; 9 | position: relative; 10 | } 11 | 12 | .content { 13 | position: absolute; 14 | width: 100%; 15 | height: 100%; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | gap: 8px; 21 | @include paragraph-small(); 22 | color: $color-neutral-300; 23 | 24 | img { 25 | width: 100%; 26 | height: 100%; 27 | object-fit: cover; 28 | } 29 | } 30 | 31 | .overlay { 32 | position: absolute; 33 | width: 100%; 34 | height: 100%; 35 | vertical-align: middle; 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/components/header/version-info/version-info.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/variables.scss'; 2 | @import 'src/design-system/variables/colors.scss'; 3 | @import 'src/design-system/variables/typography.scss'; 4 | 5 | .wrapper { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | gap: 16px; 10 | } 11 | 12 | .badge { 13 | padding: 6px 16px 4px; 14 | border-radius: 4px; 15 | background-color: $color-success-100; 16 | color: $color-success-700; 17 | text-align: center; 18 | @include label(); 19 | } 20 | 21 | .version { 22 | @include paragraph-x-small(); 23 | color: $color-neutral-700; 24 | white-space: nowrap; 25 | } 26 | 27 | .deprecated { 28 | .badge { 29 | background-color: $color-destructive-100; 30 | color: $color-destructive-600; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/components/info-page/code-of-conduct-page/code-of-conduct-page.tsx: -------------------------------------------------------------------------------- 1 | import { InfoPage } from '../info-page' 2 | import markdown from './code-of-conduct.md' 3 | 4 | export const CodeOfConductPage = () => ( 5 | 6 | ) 7 | -------------------------------------------------------------------------------- /ui/src/components/info-page/terms-of-service-page/terms-of-service-page.tsx: -------------------------------------------------------------------------------- 1 | import { InfoPage } from '../info-page' 2 | import markdown from './terms-of-service.md' 3 | 4 | export const TermsOfServicePage = () => ( 5 | 6 | ) 7 | -------------------------------------------------------------------------------- /ui/src/components/license-info/license-info.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | const LINK = 'https://creativecommons.org/licenses/by-nc/4.0/legalcode' 4 | 5 | interface LicenseInfoProps { 6 | style?: CSSProperties 7 | } 8 | 9 | export const LicenseInfo = ({ style }: LicenseInfoProps) => { 10 | // TODO: Check licence given the current project 11 | 12 | return ( 13 |

14 | These images are licensed under{' '} 15 | 16 | CC BY-NC 4.0 17 | 18 |

19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/components/taxon-search/buildTree.ts: -------------------------------------------------------------------------------- 1 | import { Node, TreeItem } from './types' 2 | 3 | export const buildTree = (data: Node[]) => { 4 | const tree: TreeItem[] = [] 5 | const treeMap: { [key: string]: TreeItem } = {} 6 | 7 | data.forEach((node: Node) => (treeMap[node.id] = { ...node, children: [] })) 8 | 9 | data.forEach((node: Node) => { 10 | if (node.parentId && treeMap[node.parentId]) { 11 | treeMap[node.parentId].children.push(treeMap[node.id]) 12 | } else { 13 | tree.push(treeMap[node.id]) 14 | } 15 | }) 16 | 17 | return tree 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/components/taxon-search/types.ts: -------------------------------------------------------------------------------- 1 | export type Node = { 2 | id: string 3 | label: string 4 | details?: string 5 | parentId?: string 6 | } 7 | 8 | export type TreeItem = Node & { children: TreeItem[] } 9 | -------------------------------------------------------------------------------- /ui/src/components/taxon/taxon-ranks/taxon-ranks.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .ranks { 5 | display: flex; 6 | flex-wrap: wrap; 7 | align-items: center; 8 | column-gap: 6px; 9 | margin-top: 8px; 10 | 11 | &.compact { 12 | flex-wrap: nowrap; 13 | } 14 | } 15 | 16 | .rank { 17 | display: block; 18 | @include paragraph-x-small(); 19 | color: $color-neutral-600; 20 | white-space: nowrap; 21 | 22 | &.divider { 23 | color: $color-neutral-300; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/terms-of-service-info/terms-of-service-info.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | @import 'src/design-system/variables/variables.scss'; 4 | 5 | .wrapper { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | @include paragraph-x-small(); 10 | padding: 8px 16px; 11 | border-radius: 4px; 12 | background-color: $color-success-100; 13 | color: $color-success-700; 14 | margin-bottom: 16px; 15 | 16 | p { 17 | @include paragraph-small(); 18 | margin: 0; 19 | } 20 | 21 | a { 22 | font-weight: 600; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/auth/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_ROUTES, API_URL } from 'data-services/constants' 4 | import { useUser } from 'utils/user/userContext' 5 | 6 | export const useLogin = ({ onSuccess }: { onSuccess?: () => void } = {}) => { 7 | const { setToken } = useUser() 8 | const { mutate, isLoading, isSuccess, error } = useMutation({ 9 | mutationFn: (data: { email: string; password: string }) => 10 | axios 11 | .post<{ auth_token: string }>(`${API_URL}/${API_ROUTES.LOGIN}/`, data) 12 | .then((res) => res.data.auth_token), 13 | onSuccess: (token) => { 14 | setToken(token) 15 | onSuccess?.() 16 | }, 17 | }) 18 | 19 | return { login: mutate, isLoading, isSuccess, error } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/auth/useLogout.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_ROUTES, API_URL } from 'data-services/constants' 4 | import { getAuthHeader } from 'data-services/utils' 5 | import { useUser } from 'utils/user/userContext' 6 | 7 | export const useLogout = () => { 8 | const { clearToken, user } = useUser() 9 | const { mutate, isLoading, isSuccess, error } = useMutation({ 10 | mutationFn: () => 11 | axios.post(`${API_URL}/${API_ROUTES.LOGOUT}/`, undefined, { 12 | headers: getAuthHeader(user), 13 | }), 14 | onSuccess: clearToken, 15 | onError: clearToken, 16 | }) 17 | 18 | return { logout: mutate, isLoading, isSuccess, error } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/auth/useResetPassword.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_ROUTES, API_URL } from 'data-services/constants' 4 | 5 | export const useResetPassword = (onSuccess?: () => void) => { 6 | const { mutate, isLoading, isSuccess, error } = useMutation({ 7 | mutationFn: (data: { email: string }) => 8 | axios 9 | .post(`${API_URL}/${API_ROUTES.RESET_PASSWORD}/`, data) 10 | .then((res) => res.data.auth_token), 11 | onSuccess, 12 | }) 13 | 14 | return { resetPassword: mutate, isLoading, isSuccess, error } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/auth/useResetPasswordConfirm.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_ROUTES, API_URL } from 'data-services/constants' 4 | 5 | export const useResetPasswordConfirm = (onSuccess?: () => void) => { 6 | const { mutate, isLoading, isSuccess, error } = useMutation({ 7 | mutationFn: (data: { 8 | new_password: string 9 | token?: string 10 | uid?: string 11 | }) => 12 | axios 13 | .post(`${API_URL}/${API_ROUTES.RESET_PASSWORD_CONFIRM}/`, data) 14 | .then((res) => res.data.auth_token), 15 | onSuccess, 16 | }) 17 | 18 | return { resetPasswordConfirm: mutate, isLoading, isSuccess, error } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/auth/useSignUp.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_ROUTES, API_URL } from 'data-services/constants' 4 | 5 | export const useSignUp = ({ onSuccess }: { onSuccess?: () => void } = {}) => { 6 | const { mutate, isLoading, isSuccess, error } = useMutation({ 7 | mutationFn: (data: { email: string; password: string }) => 8 | axios 9 | .post(`${API_URL}/${API_ROUTES.USERS}/`, data) 10 | .then((res) => res.data), 11 | onSuccess, 12 | }) 13 | 14 | return { signUp: mutate, isLoading, isSuccess, error } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/entities/types.ts: -------------------------------------------------------------------------------- 1 | export interface EntityFieldValues { 2 | description?: string 3 | name: string 4 | projectId: string 5 | customFields?: { [key: string]: string | number | object | undefined } 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/entities/useDeleteEntity.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query' 2 | import axios from 'axios' 3 | import { API_URL } from 'data-services/constants' 4 | import { getAuthHeader } from 'data-services/utils' 5 | import { useUser } from 'utils/user/userContext' 6 | 7 | export const useDeleteEntity = (collection: string, onSuccess?: () => void) => { 8 | const { user } = useUser() 9 | const queryClient = useQueryClient() 10 | 11 | const { mutateAsync, isLoading, isSuccess, error } = useMutation({ 12 | mutationFn: (id: string) => 13 | axios.delete(`${API_URL}/${collection}/${id}/`, { 14 | headers: getAuthHeader(user), 15 | }), 16 | onSuccess: () => { 17 | queryClient.invalidateQueries([collection]) 18 | onSuccess?.() 19 | }, 20 | }) 21 | 22 | return { deleteEntity: mutateAsync, isLoading, isSuccess, error } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/entities/utils.ts: -------------------------------------------------------------------------------- 1 | import { EntityFieldValues } from './types' 2 | 3 | export const convertToServerFieldValues = (fieldValues: EntityFieldValues) => { 4 | const { description, name, projectId, customFields } = fieldValues 5 | 6 | return { 7 | ...(description ? { description } : {}), 8 | ...(name ? { name } : {}), 9 | project: projectId, 10 | ...(customFields ?? {}), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/identifications/types.ts: -------------------------------------------------------------------------------- 1 | export interface IdentificationFieldValues { 2 | agreeWith?: { 3 | identificationId?: string 4 | predictionId?: string 5 | } 6 | occurrenceId: string 7 | taxonId: string 8 | comment?: string 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/pages/types.ts: -------------------------------------------------------------------------------- 1 | export interface Page { 2 | id: number 3 | name: string 4 | slug: string 5 | } 6 | 7 | export interface PageDetails extends Page { 8 | html: string 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/pages/usePageDetails.ts: -------------------------------------------------------------------------------- 1 | import { API_ROUTES, API_URL } from 'data-services/constants' 2 | import { useAuthorizedQuery } from '../auth/useAuthorizedQuery' 3 | import { PageDetails } from './types' 4 | 5 | export const usePageDetails = ( 6 | slug: string 7 | ): { 8 | page?: PageDetails 9 | isLoading: boolean 10 | isFetching: boolean 11 | error?: unknown 12 | } => { 13 | const { data, isLoading, isFetching, error } = 14 | useAuthorizedQuery({ 15 | queryKey: [API_ROUTES.PAGES, slug], 16 | url: `${API_URL}/${API_ROUTES.PAGES}/${slug}/`, 17 | }) 18 | 19 | return { 20 | page: data, 21 | isLoading, 22 | isFetching, 23 | error, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/data-services/hooks/projects/utils.ts: -------------------------------------------------------------------------------- 1 | export const convertToServerFormData = (fieldValues: any) => { 2 | const data = new FormData() 3 | 4 | if (fieldValues.name) { 5 | data.append('name', fieldValues.name) 6 | } 7 | if (fieldValues.description) { 8 | data.append('description', fieldValues.description) 9 | } 10 | if (fieldValues.image) { 11 | data.append('image', fieldValues.image, fieldValues.image.name) 12 | } else if (fieldValues.image === null) { 13 | data.append('image', '') 14 | } 15 | 16 | return data 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/data-services/models/taxa-list.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ServerEntity } from 'data-services/models/entity' 2 | 3 | export type ServerTaxaList = ServerEntity & { 4 | taxa: string // URL to taxa API endpoint (filtered by this TaxaList) 5 | projects: number[] // Array of project IDs 6 | } 7 | 8 | export class TaxaList extends Entity { 9 | protected readonly _taxaList: ServerTaxaList 10 | 11 | public constructor(taxaList: ServerTaxaList) { 12 | super(taxaList) // Call the parent class constructor 13 | this._taxaList = taxaList 14 | } 15 | 16 | get taxaUrl(): string { 17 | return this._taxaList.taxa 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/data-services/types.ts: -------------------------------------------------------------------------------- 1 | export interface FetchParams { 2 | projectId?: string 3 | pagination?: { page: number; perPage: number } 4 | sort?: { field: string; order: 'asc' | 'desc' } 5 | filters?: { field: string; value?: string; error?: string }[] 6 | } 7 | 8 | export interface APIValidationError { 9 | detail: string 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/design-system/components/box/box.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/variables.scss'; 3 | 4 | .box { 5 | background: $color-generic-white; 6 | width: 100%; 7 | border-radius: 6px; 8 | border: 1px solid $color-neutral-100; 9 | overflow-x: auto; 10 | } 11 | 12 | .boxContent { 13 | padding: 32px; 14 | display: inline-flex; 15 | align-items: center; 16 | justify-content: center; 17 | min-width: 100%; 18 | height: 100%; 19 | box-sizing: border-box; 20 | } 21 | 22 | @media only screen and (max-width: $small-screen-breakpoint) { 23 | .boxContent { 24 | padding: 16px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/design-system/components/box/box.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import styles from './box.module.scss' 3 | 4 | export const Box = ({ children }: { children: ReactNode }) => ( 5 |
6 |
{children}
7 |
8 | ) 9 | -------------------------------------------------------------------------------- /ui/src/design-system/components/bulk-action-bar/bulk-action-bar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .wrapper { 5 | bottom: 72px; 6 | position: fixed; 7 | border-radius: 32px; 8 | height: 64px; 9 | padding: 0 32px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | gap: 16px; 14 | background-color: $color-generic-white; 15 | box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1); 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/design-system/components/bulk-action-bar/bulk-action-bar.tsx: -------------------------------------------------------------------------------- 1 | import { XIcon } from 'lucide-react' 2 | import { Button } from 'nova-ui-kit' 3 | import { ReactNode } from 'react' 4 | import styles from './bulk-action-bar.module.scss' 5 | 6 | interface BulkActionBarProps { 7 | children: ReactNode 8 | selectedItems: string[] 9 | onClear: () => void 10 | } 11 | 12 | export const BulkActionBar = ({ 13 | children, 14 | selectedItems, 15 | onClear, 16 | }: BulkActionBarProps) => ( 17 |
18 | 19 | {selectedItems.length} selected 20 | 21 | {children} 22 | 25 |
26 | ) 27 | -------------------------------------------------------------------------------- /ui/src/design-system/components/file-input/file-input.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .container { 5 | display: flex; 6 | align-items: center; 7 | justify-content: flex-start; 8 | margin-top: 8px; 9 | gap: 16px; 10 | } 11 | 12 | .fileInput { 13 | width: 0.1px; 14 | height: 0.1px; 15 | opacity: 0; 16 | overflow: hidden; 17 | position: absolute; 18 | z-index: -1; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/design-system/components/file-input/types.ts: -------------------------------------------------------------------------------- 1 | export enum FileInputAccept { 2 | All = 'all', 3 | Images = 'images', 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/occurrences.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/photograph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/play-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/circle-backslash.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/heart-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/options.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/toggle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/toggle-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/toggle-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/radix/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/assets/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/design-system/components/icon/icon.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .wrapper { 5 | display: inline-flex; 6 | align-items: center; 7 | justify-content: center; 8 | transition: transform 400ms ease; 9 | 10 | &.light { 11 | color: $color-generic-white; 12 | } 13 | 14 | &.dark { 15 | color: $color-neutral-700; 16 | } 17 | 18 | &.neutral { 19 | color: $color-neutral-300; 20 | } 21 | 22 | &.primary { 23 | color: $color-primary-1-600; 24 | } 25 | 26 | &.success { 27 | color: $color-success-700; 28 | } 29 | 30 | &.error { 31 | color: $color-destructive-600; 32 | } 33 | 34 | &.fixedSized { 35 | > * { 36 | width: 100%; 37 | height: 100%; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/design-system/components/image-carousel/types.ts: -------------------------------------------------------------------------------- 1 | export enum CarouselTheme { 2 | Default = 'default', 3 | Light = 'light', 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/design-system/components/image-carousel/utils.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { CSSProperties } from 'react' 3 | 4 | export const getImageBoxStyles = ( 5 | width: string | number = 100 6 | ): CSSProperties => ({ 7 | width: _.isNumber(width) ? `${width}px` : width, 8 | }) 9 | 10 | export const getPlaceholderStyles = (ratio = 1): CSSProperties => ({ 11 | paddingBottom: `${(1 / ratio) * 100}%`, 12 | }) 13 | -------------------------------------------------------------------------------- /ui/src/design-system/components/image-upload/image-upload.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .container { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | background-color: $color-primary-2-50; 9 | 10 | img { 11 | max-width: 100%; 12 | } 13 | } 14 | 15 | .info { 16 | @include paragraph-small(); 17 | color: $color-neutral-300; 18 | margin: 96px 32px; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/design-system/components/info-block/info-block.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .field { 5 | margin: 0; 6 | 7 | &:not(:last-child) { 8 | margin-bottom: 16px; 9 | } 10 | } 11 | 12 | .fieldLabel { 13 | display: block; 14 | @include label(); 15 | font-weight: 600; 16 | color: $color-neutral-300; 17 | } 18 | 19 | .fieldValue { 20 | display: block; 21 | @include paragraph-medium(); 22 | color: $color-neutral-700; 23 | 24 | &.link { 25 | color: $color-primary-1-600; 26 | font-weight: 600; 27 | } 28 | 29 | &.bubble { 30 | @include bubble-label(); 31 | display: inline-flex; 32 | margin-top: 4px; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/design-system/components/loading-spinner/loading-spinner.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | 3 | .loadingSpinner { 4 | display: inline-block; 5 | border-style: solid; 6 | border-color: $color-primary-2-500; 7 | border-top-color: transparent; 8 | border-radius: 50%; 9 | animation: spin 0.7s ease-in-out infinite; 10 | } 11 | 12 | @keyframes spin { 13 | to { 14 | -webkit-transform: rotate(360deg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/design-system/components/loading-spinner/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import styles from './loading-spinner.module.scss' 2 | 3 | export const LoadingSpinner = ({ size = 56 }: { size?: number }) => { 4 | return ( 5 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/design-system/components/page-footer/page-footer.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | 4 | .wrapper { 5 | position: fixed; 6 | bottom: 0; 7 | left: 0; 8 | width: 100%; 9 | background-color: $color-generic-white; 10 | border-top: 1px solid $color-neutral-100; 11 | z-index: 2; 12 | } 13 | 14 | .content { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | width: 100%; 19 | height: 64px; 20 | padding: 0 100px; 21 | box-sizing: border-box; 22 | } 23 | 24 | @media only screen and (max-width: $medium-screen-breakpoint) { 25 | .content { 26 | padding: 0 32px; 27 | } 28 | } 29 | 30 | @media only screen and (max-width: $small-screen-breakpoint) { 31 | .content { 32 | padding: 0 16px; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/design-system/components/page-footer/page-footer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import styles from './page-footer.module.scss' 3 | 4 | interface PageFooterProps { 5 | hide?: boolean 6 | children?: ReactNode 7 | } 8 | 9 | export const PageFooter = ({ hide, children }: PageFooterProps) => { 10 | if (!children || hide) { 11 | return null 12 | } 13 | 14 | return ( 15 |
16 |
{children}
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/design-system/components/pagination-bar/getPageWindow.ts: -------------------------------------------------------------------------------- 1 | const WINDOW_SIZE = 2 2 | 3 | export const getPageWindow = (currentPage: number, numPages: number) => { 4 | const pages = Array.from(Array(numPages).keys()) 5 | const startIndex = currentPage - WINDOW_SIZE 6 | const endIndex = currentPage + WINDOW_SIZE + 1 7 | 8 | const offset = (() => { 9 | if (startIndex < 0) { 10 | return -startIndex 11 | } 12 | if (endIndex >= pages.length) { 13 | return pages.length - endIndex 14 | } 15 | return 0 16 | })() 17 | 18 | return pages.slice(startIndex + offset, endIndex + offset) 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/design-system/components/pagination-bar/info-label/getValueInRange.ts: -------------------------------------------------------------------------------- 1 | export const getValueInRange = (args: { 2 | value: number 3 | min: number 4 | max: number 5 | }) => Math.min(args.max, Math.max(args.min, args.value)) 6 | -------------------------------------------------------------------------------- /ui/src/design-system/components/pagination-bar/info-label/info-label.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/colors.scss'; 2 | @import 'src/design-system/variables/typography.scss'; 3 | @import 'src/design-system/variables/variables.scss'; 4 | 5 | .infoLabel { 6 | display: block; 7 | @include paragraph-small(); 8 | font-weight: 600; 9 | color: $color-neutral-300; 10 | } 11 | 12 | @media only screen and (max-width: $small-screen-breakpoint) { 13 | .infoLabel { 14 | display: none; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/design-system/components/pagination-bar/page-button/page-button.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/design-system/variables/typography.scss'; 2 | 3 | .pageButton { 4 | @include paragraph-small(); 5 | font-weight: 600; 6 | padding: 0 8px; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/design-system/components/pagination-bar/page-button/page-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonTheme } from '../../button/button' 2 | import styles from './page-button.module.scss' 3 | 4 | interface PageButtonProps { 5 | page: number 6 | active?: boolean 7 | onClick: () => void 8 | } 9 | 10 | export const PageButton = ({ page, active, onClick }: PageButtonProps) => ( 11 |