├── .dockerignore ├── .flake8 ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .readthedocs.yaml ├── README.md ├── alyx ├── Makefile ├── actions │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ ├── actions.cullmethod.json │ │ ├── actions.cullreason.json │ │ ├── actions.proceduretype.json │ │ └── actions.watertype.json │ ├── management │ │ └── commands │ │ │ ├── check_water_admin.py │ │ │ └── send_pending_notifications.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181015_0914.py │ │ ├── 0003_auto_20181119_0930.py │ │ ├── 0004_auto_20181122_1923.py │ │ ├── 0005_auto_20190124_1025.py │ │ ├── 0006_cull_cullmethod_cullreason.py │ │ ├── 0007_session_qc.py │ │ ├── 0008_alter_session_qc.py │ │ ├── 0009_ephyssession.py │ │ ├── 0010_session_extended_qc.py │ │ ├── 0011_auto_20200317_1055.py │ │ ├── 0012_change_qc_enum.py │ │ ├── 0013_auto_20201119_0953.py │ │ ├── 0014_session_auto_datetime.py │ │ ├── 0015_auto_20210624_1253.py │ │ ├── 0016_chronicrecording.py │ │ ├── 0017_alter_chronicrecording_subject_and_more.py │ │ ├── 0018_session_projects_alter_session_project.py │ │ ├── 0019_imagingsession.py │ │ ├── 0020_alter_notification_notification_type_and_more.py │ │ ├── 0021_alter_session_extended_qc.py │ │ ├── 0022_project_to_projects.py │ │ ├── 0023_remove_session_project.py │ │ ├── 0024_surgery_implant_weight.py │ │ ├── 0025_move_implant_weight.py │ │ ├── 0026_alter_surgery_implant_weight.py │ │ └── __init__.py │ ├── models.py │ ├── notifications.py │ ├── serializers.py │ ├── static │ │ ├── ref_weighings_female.csv │ │ └── ref_weighings_male.csv │ ├── tests.py │ ├── tests_admin.py │ ├── tests_rest.py │ ├── urls.py │ ├── views.py │ └── water_control.py ├── alyx │ ├── __init__.py │ ├── base.py │ ├── settings_ci.py │ ├── settings_lab_template.py │ ├── settings_secret_template.py │ ├── settings_template.py │ ├── test_base.py │ ├── urls.py │ └── wsgi.py ├── data │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ ├── data.dataformat.json │ │ ├── data.datarepositorytype.json │ │ └── data.datasettype.json │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── files.py │ │ │ └── transfers_integration.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181015_0914.py │ │ ├── 0003_auto_20190315_1716.py │ │ ├── 0004_dataset_filesize_int64.py │ │ ├── 0005_dataset_collection.py │ │ ├── 0006_dataset_hash_version.py │ │ ├── 0007_dataset_auto_datetime.py │ │ ├── 0008_dataset_revision_tag.py │ │ ├── 0009_auto_20210624_1253.py │ │ ├── 0010_alter_dataset_created_by_and_more.py │ │ ├── 0011_alter_datasettype_filename_pattern.py │ │ ├── 0012_alter_datasettype_filename_pattern_and_more.py │ │ ├── 0013_dataset_content_type_dataset_object_id.py │ │ ├── 0014_alter_datasettype_filename_pattern.py │ │ ├── 0015_alter_datasettype_filename_pattern.py │ │ ├── 0016_alter_dataset_collection_alter_revision_name.py │ │ ├── 0017_alter_dataset_object_id.py │ │ ├── 0018_alter_dataset_collection_alter_revision_name.py │ │ ├── 0019_dataset_qc.py │ │ ├── 0020_alter_datarepository_timezone.py │ │ ├── 0021_alter_dataset_collection_alter_dataset_hash_and_more.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── tests_rest.py │ ├── transfers.py │ ├── urls.py │ └── views.py ├── experiments │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ ├── allen_structure_tree.csv │ │ ├── experiments.brainregion.json │ │ ├── experiments.coordinatesystem.json │ │ ├── experiments.imagingtype.json │ │ ├── experiments.probemodel.json │ │ └── load_allen.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_channels_20200402.py │ │ ├── 0003_probe_insertion_serial_20200504_1132.py │ │ ├── 0004_auto_20200519_2129.py │ │ ├── 0005_trajectoryestimate_datetime.py │ │ ├── 0006_auto_20201119_0953.py │ │ ├── 0007_probeinsertion_auto_datetime.py │ │ ├── 0008_probeinsertion_datasets.py │ │ ├── 0009_auto_20210624_1253.py │ │ ├── 0010_probeinsertion_chronic_recording.py │ │ ├── 0011_chronic_insertion.py │ │ ├── 0012_fov_imagingstack_imagingtype_alter_channel_lateral_and_more.py │ │ ├── 0013_remove_trajectoryestimate_unique_trajectory_per_provenance_and_more.py │ │ ├── 0014_alter_probeinsertion_chronic_insertion.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── tests_rest.py │ ├── urls.py │ └── views.py ├── jobs │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── tasks.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200629_0935.py │ │ ├── 0003_auto_20200705_2046.py │ │ ├── 0004_auto_20200731_0936.py │ │ ├── 0005_auto_20201119_0953.py │ │ ├── 0006_alter_task_status.py │ │ ├── 0007_remove_task_unique_task_name_per_session_and_more.py │ │ ├── 0008_task_data_repository.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── templatetags │ │ ├── __init__.py │ │ └── jobs_template_tags.py │ ├── tests.py │ ├── tests_admin.py │ ├── urls.py │ └── views.py ├── manage.py ├── misc │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ ├── misc.cagetype.json │ │ ├── misc.enrichment.json │ │ ├── misc.food.json │ │ └── misc.lab.json │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── backup.py │ │ │ ├── download_gsheets.py │ │ │ ├── dump.py │ │ │ ├── homeoffice.py │ │ │ ├── migrate_ucl.py │ │ │ ├── one_cache.py │ │ │ ├── queries │ │ │ ├── breeding_pairs.sql │ │ │ ├── genotype_tests.sql │ │ │ ├── lines.sql │ │ │ ├── litters.sql │ │ │ ├── subject.sql │ │ │ └── subjectrequest.sql │ │ │ ├── report.py │ │ │ ├── reset_db.py │ │ │ ├── set_db_permissions.py │ │ │ ├── set_user_permissions.py │ │ │ ├── total_reset.py │ │ │ └── validate_subjects.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181119_0930.py │ │ ├── 0003_auto_20190124_1025.py │ │ ├── 0004_auto_20190309_1750.py │ │ ├── 0005_lab_repositories.py │ │ ├── 0006_labmember_allowed_users.py │ │ ├── 0007_auto_20200120_1223.py │ │ ├── 0008_auto_20210624_1253.py │ │ ├── 0009_auto_20211122_1535.py │ │ ├── 0010_alter_lab_timezone.py │ │ ├── 0011_alter_lab_name.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── tests_rest.py │ ├── urls.py │ └── views.py ├── rm_all_db_migrations.sh ├── static │ ├── ref_weighings_female.csv │ └── ref_weighings_male.csv ├── subjects │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ └── subjects.source.json │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── update_zygosities.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181015_1026.py │ │ ├── 0003_auto_20190124_1025.py │ │ ├── 0004_remove_project_repositories.py │ │ ├── 0005_auto_20191224_1035.py │ │ ├── 0006_auto_20200317_1055.py │ │ ├── 0007_auto_20200921_1346.py │ │ ├── 0008_auto_20201015_1320.py │ │ ├── 0009_auto_20201103_1434.py │ │ ├── 0010_auto_20210624_1253.py │ │ ├── 0011_alter_subject_nickname.py │ │ ├── 0012_alter_subject_nickname.py │ │ ├── 0013_remove_subject_implant_weight.py │ │ ├── 0014_delete_adverse_effect_delete_cull_subject_and_more.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── static │ │ └── subjects │ │ │ └── assets │ │ │ ├── css │ │ │ ├── application.css │ │ │ ├── docs.css │ │ │ ├── toolkit-inverse.css │ │ │ └── toolkit-light.css │ │ │ ├── fonts │ │ │ ├── toolkit-entypo.eot │ │ │ ├── toolkit-entypo.ttf │ │ │ ├── toolkit-entypo.woff │ │ │ └── toolkit-entypo.woff2 │ │ │ ├── img │ │ │ └── avatar-mdo.png │ │ │ └── js │ │ │ ├── application.js │ │ │ ├── chart.js │ │ │ ├── jquery.min.js │ │ │ ├── tablesorter.min.js │ │ │ └── toolkit.js │ ├── tests.py │ ├── tests_admin.py │ ├── tests_rest.py │ ├── urls.py │ ├── views.py │ └── zygosities.py └── templates │ ├── admin │ ├── base.html │ ├── change_form.html │ ├── change_list.html │ ├── index.html │ └── search_form.html │ ├── base.html │ ├── error_500.html │ ├── includes │ └── navbar.html │ ├── index.html │ ├── rest_framework │ └── api.html │ ├── subject.html │ ├── subject_history.html │ ├── subjects_list.html │ ├── tasks.html │ ├── training.html │ └── water_history.html ├── data ├── README.md └── all_dumped_anon.json.gz ├── docs ├── Makefile ├── README.md ├── _static │ ├── 001-alyx.conf │ ├── Full_Alyx_ERD.png │ ├── actions_ERD.png │ ├── data_ERD.png │ ├── misc_ERD.png │ └── subjects_ERD.png ├── api.rst ├── conf.py ├── considerations.rst ├── gettingstarted.md ├── index.rst ├── models.rst ├── motivation.rst ├── requirements.txt └── webserving.md ├── requirements.txt ├── requirements_frozen.txt ├── scripts ├── auto-update.sh ├── check-backup.sh ├── delete_migrations.sh ├── deployment_examples │ ├── 01_backup_ibl.sh │ ├── 02c_globus_sync_PT.sh │ ├── 03_dev_reset.sh │ ├── 99_purge_duplicate_backups.py │ ├── alyx-docker │ │ ├── 000-default-conf-alyx-dev │ │ ├── 000-default-conf-alyx-prod │ │ ├── 000-default-conf-openalyx │ │ ├── Dockerfile.base │ │ ├── Dockerfile.ibl │ │ ├── README.md │ │ ├── apache-conf-alyx-dev │ │ ├── apache-conf-alyx-prod │ │ ├── apache-conf-openalyx │ │ └── ibl_alyx_bootstrap.sh │ ├── docker │ │ ├── Dockerfile │ │ ├── README │ │ ├── alyx-entrypoint.sh │ │ ├── docker-compose.yml │ │ └── dot-env.example │ ├── ibl_patcher_sync.py │ └── public_data_release.py ├── dump-init-fixtures.sh ├── load-init-fixtures.sh ├── oneoff │ ├── 2019-07-30-datasettypes.py │ ├── 2019-08-30-patch_register.py │ ├── 2019-09-30-UCL_IBL_compare.py │ ├── 2019-10-04-DatasetTypes_DataMigration.py │ ├── 2024-03-16-update_test_fixture.py │ └── 2025-01-01-water_fixture_rename.py ├── sanitize-init-fixtures.sh.py ├── sync_ucl │ ├── pk_sync │ │ ├── Alyx_update_PK_from_UCL.py │ │ └── Alyx_update_PK_from_UCL.sh │ ├── prune_cortexlab.py │ └── ucl_post_json_import.py └── templates │ ├── .pgpass_template │ ├── dump_db.sh │ └── load_db.sh ├── setup.cfg ├── setup.py └── utils └── user-uuid ├── dump.sh ├── load.sh └── replace_user_ids.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore .git and .cache folders 2 | .git 3 | .cache 4 | .github 5 | alyxvenv/ 6 | venv/ 7 | tables/ 8 | uploaded/ -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | migrations 4 | __pycache__ 5 | manage.py 6 | settings.py 7 | .github 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *~ 4 | *! 5 | alyx-backups 6 | uploaded 7 | tables 8 | docs/_build 9 | docs/_templates 10 | *.tsv 11 | bin 12 | experimental 13 | *.asv 14 | all_dumped_anon.json 15 | *.pkl 16 | *.patch 17 | *.sql 18 | *.html.py 19 | *.orig 20 | notes.txt 21 | build 22 | *venv 23 | .idea/* 24 | .cache/ 25 | alyx/alyx_full.sql.gz 26 | .vscode/ 27 | alyx/data/management/commands/*ibl* 28 | alyx/ibl_reports 29 | alyx/templates/ibl_reports 30 | alyx/alyx/settings_secret.py 31 | alyx/alyx/settings_lab.py 32 | alyx/alyx/settings.py 33 | alyx/static/*/* 34 | scripts/deployment_examples/docker-apache/settings* 35 | 36 | alyx/.idea/ 37 | 38 | alyx.log 39 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: requirements.txt 30 | - requirements: docs/requirements.txt 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alyx 2 | 3 | [![Github Actions](https://github.com/cortex-lab/alyx/actions/workflows/main.yml/badge.svg)](https://github.com/cortex-lab/alyx/actions/) 4 | [![Coverage Status](https://coveralls.io/repos/github/cortex-lab/alyx/badge.svg?branch=github_action)](https://coveralls.io/github/cortex-lab/alyx?branch=master) 5 | 6 | Database for experimental neuroscience laboratories 7 | 8 | Documentation: [Installation and getting started](http://alyx.readthedocs.io), [Alyx usage guide](https://docs.google.com/document/d/1cx3XLZiZRh3lUzhhR_p65BggEqTKpXHUDkUDagvf9Kc/edit?usp=sharing) 9 | 10 | 11 | ## Installation 12 | Alyx has only been tested on Ubuntu (16.04 / 18.04 / 20.04), the latest is recommended. There are no guarantees that 13 | this setup will work on other systems. Assumptions made are that you have sudo permissions under an account named 14 | 15 | [The getting started](docs/gettingstarted.md) section of the documentation details the steps for 16 | - installing the Python/Django environment 17 | - serving a local database 18 | - registering local data 19 | - accessing local data using [ONE](https://one.internationalbrainlab.org) 20 | 21 | ## Contribution 22 | 23 | * Development happens on the **dev** branch 24 | * alyx is sync with the **master** branch 25 | * alyx-dev is sync with the **dev** branch 26 | * Migrations files are provided by the repository 27 | * Continuous integration is setup, to run tests locally: 28 | - `./manage.py test -n` test without migrations (faster) 29 | - `./manage.py test` test with migrations (recommended if model changes) 30 | - NB: When running tests ensure `DEBUG = True` in the settings.py file (specifically `SECURE_SSL_REDIRECT = True` causes REST tests to fail) 31 | 32 | ```shell 33 | ./manage.py test -n 34 | ``` 35 | -------------------------------------------------------------------------------- /alyx/Makefile: -------------------------------------------------------------------------------- 1 | all: serve 2 | 3 | serve: 4 | python manage.py runserver 5 | 6 | clean-pyc: 7 | find . -name '*.pyc' -exec rm -f {} + 8 | find . -name '*.pyo' -exec rm -f {} + 9 | find . -name '*~' -exec rm -f {} + 10 | find . -name '__pycache__' -exec rm -fr {} + 11 | 12 | clean: clean-pyc 13 | 14 | rmmigrations: clean 15 | rm */migrations/0*.py 16 | 17 | migrate: 18 | python manage.py makemigrations equipment misc subjects actions data imaging behavior electrophysiology 19 | python manage.py migrate 20 | 21 | test: 22 | python manage.py test -k -n 23 | flake8 24 | -------------------------------------------------------------------------------- /alyx/actions/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'actions.apps.ActionsConfig' 2 | -------------------------------------------------------------------------------- /alyx/actions/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ActionsConfig(AppConfig): 5 | name = 'actions' 6 | verbose_name = 'Action admin' 7 | -------------------------------------------------------------------------------- /alyx/actions/fixtures/actions.cullmethod.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "actions.cullmethod", 4 | "pk": "01a5a8d8-a233-48e2-b2a7-ac0580034ac8", 5 | "fields": { 6 | "name": "Cervical Dislocation", 7 | "json": null, 8 | "description": "" 9 | } 10 | }, 11 | { 12 | "model": "actions.cullmethod", 13 | "pk": "0694060a-1797-4529-8e59-5f39423866f3", 14 | "fields": { 15 | "name": "Overdose", 16 | "json": null, 17 | "description": "" 18 | } 19 | }, 20 | { 21 | "model": "actions.cullmethod", 22 | "pk": "13407b51-a6ba-4176-9dea-b852741f8eb4", 23 | "fields": { 24 | "name": "CO2", 25 | "json": null, 26 | "description": "" 27 | } 28 | }, 29 | { 30 | "model": "actions.cullmethod", 31 | "pk": "4e166f8c-6c22-4b7a-8dd3-d8b810d02981", 32 | "fields": { 33 | "name": "Decapitation", 34 | "json": null, 35 | "description": "" 36 | } 37 | }, 38 | { 39 | "model": "actions.cullmethod", 40 | "pk": "6988037c-4c9e-46a4-94f7-e2509741089e", 41 | "fields": { 42 | "name": "Cooling", 43 | "json": null, 44 | "description": "" 45 | } 46 | }, 47 | { 48 | "model": "actions.cullmethod", 49 | "pk": "a10c2b09-f52a-4a11-8333-f95cb2ee478f", 50 | "fields": { 51 | "name": "Concussion of the Brain", 52 | "json": null, 53 | "description": "" 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /alyx/actions/fixtures/actions.cullreason.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "actions.cullreason", 4 | "pk": "0d8d52d8-e989-4193-9c33-8c997271e02f", 5 | "fields": { 6 | "name": "infection or illness", 7 | "json": null, 8 | "description": "Known or indeterminate" 9 | } 10 | }, 11 | { 12 | "model": "actions.cullreason", 13 | "pk": "13ed7ed6-734d-470d-81ad-f51f2f6baee5", 14 | "fields": { 15 | "name": "regular experiment end", 16 | "json": null, 17 | "description": "" 18 | } 19 | }, 20 | { 21 | "model": "actions.cullreason", 22 | "pk": "1ca73836-6a5c-4939-93a6-75c7fdb0a3e3", 23 | "fields": { 24 | "name": "acute injury", 25 | "json": null, 26 | "description": "headplate loss, indeterminate, etc" 27 | } 28 | }, 29 | { 30 | "model": "actions.cullreason", 31 | "pk": "4bae1398-4beb-4399-b5fc-d7ffb6fdd261", 32 | "fields": { 33 | "name": "underweight", 34 | "json": null, 35 | "description": "" 36 | } 37 | }, 38 | { 39 | "model": "actions.cullreason", 40 | "pk": "f235d64a-cca1-490d-b741-2cc6cceba53b", 41 | "fields": { 42 | "name": "issue during surgery", 43 | "json": null, 44 | "description": "" 45 | } 46 | }, 47 | { 48 | "model": "actions.cullreason", 49 | "pk": "f450fb3f-d9cf-4e81-ba6a-1bde38aaaf6d", 50 | "fields": { 51 | "name": "time limit reached", 52 | "json": null, 53 | "description": "age, protocol etc... please describe in cull note" 54 | } 55 | }, 56 | { 57 | "model": "actions.cullreason", 58 | "pk": "f997bdad-3fb4-400a-85fa-0fa8463f2227", 59 | "fields": { 60 | "name": "benign experimental impediments", 61 | "json": null, 62 | "description": "cataract, bone regrowth, mouse didn't learn task" 63 | } 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /alyx/actions/fixtures/actions.watertype.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "actions.watertype", 4 | "pk": "2127f637-0770-4639-8c22-3d73a94eecc3", 5 | "fields": { 6 | "json": null, 7 | "name": "Water" 8 | } 9 | }, 10 | { 11 | "model": "actions.watertype", 12 | "pk": "31043cd4-7351-4aff-b69e-34a50b69cc75", 13 | "fields": { 14 | "json": null, 15 | "name": "Water 15% Sucrose" 16 | } 17 | }, 18 | { 19 | "model": "actions.watertype", 20 | "pk": "511feb0c-cc35-4c42-a7b9-db704833ef16", 21 | "fields": { 22 | "json": null, 23 | "name": "Hydrogel 5% Citric Acid" 24 | } 25 | }, 26 | { 27 | "model": "actions.watertype", 28 | "pk": "5267f912-8abf-470f-8593-40a02c35bd60", 29 | "fields": { 30 | "json": null, 31 | "name": "Water 10% Sucrose" 32 | } 33 | }, 34 | { 35 | "model": "actions.watertype", 36 | "pk": "c1135c08-ef13-4dea-a0ea-3f88877eebd1", 37 | "fields": { 38 | "json": null, 39 | "name": "Water 2% Citric Acid" 40 | } 41 | }, 42 | { 43 | "model": "actions.watertype", 44 | "pk": "c68ed3b4-8a3d-47e2-a010-de7b9c027439", 45 | "fields": { 46 | "json": null, 47 | "name": "Hydrogel" 48 | } 49 | }, 50 | { 51 | "model": "actions.watertype", 52 | "pk": "dba3e45a-fb95-4a6d-9140-2b704c5b300e", 53 | "fields": { 54 | "json": null, 55 | "name": "Water 1% Citric Acid" 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /alyx/actions/management/commands/check_water_admin.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from actions.models import WaterRestriction 3 | from actions.notifications import check_water_administration, check_weighed 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Check all water administrations and weighings." 8 | 9 | def add_arguments(self, parser): 10 | pass 11 | 12 | def handle(self, *args, **options): 13 | wrs = WaterRestriction.objects.select_related('subject'). \ 14 | filter( 15 | subject__death_date__isnull=True, 16 | start_time__isnull=False, 17 | end_time__isnull=True). \ 18 | order_by('subject__responsible_user__username', 'subject__nickname') 19 | for wr in wrs: 20 | check_water_administration(wr.subject) 21 | check_weighed(wr.subject) 22 | -------------------------------------------------------------------------------- /alyx/actions/management/commands/send_pending_notifications.py: -------------------------------------------------------------------------------- 1 | from django.core.management import BaseCommand 2 | from actions.models import send_pending_emails 3 | 4 | 5 | class Command(BaseCommand): 6 | help = "Send pending notifications." 7 | 8 | def handle(self, *args, **options): 9 | send_pending_emails() 10 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0003_auto_20181119_0930.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-11-19 09:30 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('actions', '0002_auto_20181015_0914'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='taskprotocol', 17 | name='lab', 18 | ), 19 | migrations.RemoveField( 20 | model_name='taskprotocol', 21 | name='location', 22 | ), 23 | migrations.RemoveField( 24 | model_name='taskprotocol', 25 | name='procedures', 26 | ), 27 | migrations.RemoveField( 28 | model_name='taskprotocol', 29 | name='subject', 30 | ), 31 | migrations.RemoveField( 32 | model_name='taskprotocol', 33 | name='users', 34 | ), 35 | migrations.RemoveField( 36 | model_name='waterrestriction', 37 | name='adlib_drink', 38 | ), 39 | migrations.AddField( 40 | model_name='wateradministration', 41 | name='adlib', 42 | field=models.BooleanField(default=False), 43 | ), 44 | migrations.RemoveField( 45 | model_name='session', 46 | name='task_protocol', 47 | ), 48 | migrations.AddField( 49 | model_name='session', 50 | name='task_protocol', 51 | field=models.CharField(blank=True, default='', max_length=1023), 52 | ), 53 | migrations.AlterField( 54 | model_name='wateradministration', 55 | name='session', 56 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wateradmin_session_related', to='actions.Session'), 57 | ), 58 | migrations.AlterField( 59 | model_name='wateradministration', 60 | name='water_administered', 61 | field=models.FloatField(blank=True, help_text='Water administered, in milliliters', null=True, validators=[django.core.validators.MinValueValidator(limit_value=0)]), 62 | ), 63 | migrations.DeleteModel( 64 | name='TaskProtocol', 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0004_auto_20181122_1923.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-11-22 19:23 2 | 3 | import actions.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('actions', '0003_auto_20181119_0930'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='waterrestriction', 17 | name='water_type', 18 | field=models.ForeignKey(blank=True, default=actions.models._default_water_type, help_text='Default Water Type when creating water admin', null=True, on_delete=django.db.models.deletion.SET_NULL, to='actions.WaterType'), 19 | ), 20 | migrations.AlterField( 21 | model_name='wateradministration', 22 | name='water_type', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='actions.WaterType'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0007_session_qc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-07 19:51 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0006_cull_cullmethod_cullreason'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='session', 16 | name='qc', 17 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Quality control JSON field', null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0008_alter_session_qc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-01-17 21:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0007_session_qc'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='session', 15 | name='qc', 16 | ), 17 | migrations.AddField( 18 | model_name='session', 19 | name='qc', 20 | field=models.IntegerField(choices=[(50, 'CRITICAL'), (40, 'ERROR'), (30, 'WARNING'), (20, 'NOT_SET'), (10, 'PASS')], default=20), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0009_ephyssession.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-02-12 11:53 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0008_alter_session_qc'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='EphysSession', 15 | fields=[ 16 | ], 17 | options={ 18 | 'proxy': True, 19 | 'indexes': [], 20 | 'constraints': [], 21 | }, 22 | bases=('actions.session',), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0010_session_extended_qc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-03-09 15:13 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0009_ephyssession'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='session', 16 | name='extended_qc', 17 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Structured data about session QC,formatted in a user-defined way', null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0011_auto_20200317_1055.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-03-17 10:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0010_session_extended_qc'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='session', 16 | name='project', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='subjects.Project', verbose_name='Session Project'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0012_change_qc_enum.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-08-20 16:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def update_qc_not_set_value(apps, schema_editor): 7 | Session = apps.get_model('actions', 'Session') 8 | Session.objects.filter(qc=20).update(qc=0) 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('actions', '0011_auto_20200317_1055'), 15 | ] 16 | 17 | operations = [ 18 | migrations.AlterField( 19 | model_name='session', 20 | name='qc', 21 | field=models.IntegerField(choices=[(50, 'CRITICAL'), (40, 'FAIL'), (30, 'WARNING'), (0, 'NOT_SET'), (10, 'PASS')], default=0), 22 | ), 23 | migrations.RunPython(update_qc_not_set_value), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0013_auto_20201119_0953.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-19 09:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0012_change_qc_enum'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='session', 15 | name='qc', 16 | field=models.IntegerField(choices=[(50, 'CRITICAL'), (40, 'FAIL'), (30, 'WARNING'), (0, 'NOT_SET'), (10, 'PASS')], default=0, help_text='50: CRITICAL / 40: FAIL / 30: WARNING / 0: NOT_SET / 10: PASS'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0014_session_auto_datetime.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2021-02-09 17:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0013_auto_20201119_0953'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='session', 15 | name='auto_datetime', 16 | field=models.DateTimeField(auto_now=True, null=True, verbose_name='last updated'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0016_chronicrecording.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-07-01 20:40 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('subjects', '0010_auto_20210624_1253'), 14 | ('misc', '0008_auto_20210624_1253'), 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ('actions', '0015_auto_20210624_1253'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='ChronicRecording', 22 | fields=[ 23 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 24 | ('name', models.CharField(blank=True, help_text='Long name', max_length=255)), 25 | ('json', models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True)), 26 | ('narrative', models.TextField(blank=True)), 27 | ('start_time', models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True)), 28 | ('end_time', models.DateTimeField(blank=True, null=True)), 29 | ('lab', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='misc.lab')), 30 | ('location', models.ForeignKey(blank=True, help_text='The physical location at which the action was performed', null=True, on_delete=django.db.models.deletion.SET_NULL, to='misc.lablocation')), 31 | ('procedures', models.ManyToManyField(blank=True, help_text='The procedure(s) performed', to='actions.ProcedureType')), 32 | ('subject', models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='actions_chronicrecordings', to='subjects.subject')), 33 | ('users', models.ManyToManyField(blank=True, help_text='The user(s) involved in this action', to=settings.AUTH_USER_MODEL)), 34 | ], 35 | options={ 36 | 'abstract': False, 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0017_alter_chronicrecording_subject_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 15:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('subjects', '0010_auto_20210624_1253'), 11 | ('actions', '0016_chronicrecording'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='chronicrecording', 17 | name='subject', 18 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 19 | ), 20 | migrations.AlterField( 21 | model_name='otheraction', 22 | name='subject', 23 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 24 | ), 25 | migrations.AlterField( 26 | model_name='session', 27 | name='subject', 28 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 29 | ), 30 | migrations.AlterField( 31 | model_name='surgery', 32 | name='subject', 33 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 34 | ), 35 | migrations.AlterField( 36 | model_name='virusinjection', 37 | name='subject', 38 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 39 | ), 40 | migrations.AlterField( 41 | model_name='waterrestriction', 42 | name='subject', 43 | field=models.ForeignKey(help_text='The subject on which this action was performed', on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)ss', to='subjects.subject'), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0018_session_projects_alter_session_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-06-23 08:10 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | def labelprojm2m(apps, schema_editor): 8 | # We can't import the Session model directly as it may be a newer 9 | # version than this migration expects. We use the historical version. 10 | Session = apps.get_model('actions', 'Session') 11 | # if the deprecated project field doesn't exist anymore, return 12 | if not 'project' in [f.name for f in Session._meta.fields]: 13 | return 14 | for session in Session.objects.filter(project__isnull=False): 15 | session.projects.set([session.project]) 16 | session.save() 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [ 22 | ('subjects', '0010_auto_20210624_1253'), 23 | ('actions', '0017_alter_chronicrecording_subject_and_more'), 24 | ] 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='session', 29 | name='projects', 30 | field=models.ManyToManyField(blank=True, to='subjects.project', verbose_name='Session Projects'), 31 | ), 32 | migrations.AlterField( 33 | model_name='session', 34 | name='project', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oldproject', to='subjects.project', verbose_name='Session Project'), 36 | ), 37 | migrations.RunPython(labelprojm2m), 38 | ] 39 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0019_imagingsession.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-07-10 11:52 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0018_session_projects_alter_session_project'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='ImagingSession', 15 | fields=[ 16 | ], 17 | options={ 18 | 'proxy': True, 19 | 'indexes': [], 20 | 'constraints': [], 21 | }, 22 | bases=('actions.session',), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0020_alter_notification_notification_type_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-07-24 18:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0019_imagingsession'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='notification_type', 16 | field=models.CharField(choices=[('responsible_user_change', 'responsible user has changed'), ('mouse_underweight', 'mouse is underweight'), ('mouse_water', 'water to give to mouse'), ('mouse_training', 'check training days'), ('mouse_not_weighed', 'no weight entered for date')], max_length=32), 17 | ), 18 | migrations.AlterField( 19 | model_name='notificationrule', 20 | name='notification_type', 21 | field=models.CharField(choices=[('responsible_user_change', 'responsible user has changed'), ('mouse_underweight', 'mouse is underweight'), ('mouse_water', 'water to give to mouse'), ('mouse_training', 'check training days'), ('mouse_not_weighed', 'no weight entered for date')], max_length=32), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0021_alter_session_extended_qc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-12 13:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0020_alter_notification_notification_type_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='session', 15 | name='extended_qc', 16 | field=models.JSONField(blank=True, help_text='Structured data about session QC, formatted in a user-defined way', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0022_project_to_projects.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-14 14:28 2 | import logging 3 | 4 | from django.db import migrations 5 | from django.db.models import F, Q 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def project2projects(apps, schema_editor): 11 | """ 12 | Find sessions where the project field (singular) value is not in the projects (plural) many-to-many 13 | field and updates them. 14 | 15 | Tested on local instance. 16 | """ 17 | Session = apps.get_model('actions', 'Session') 18 | sessions = Session.objects.exclude(Q(project__isnull=True) | Q(projects=F('project'))) 19 | 20 | # Check query worked 21 | # from iblutil.util import ensure_list 22 | # for session in sessions.values('pk', 'project', 'projects'): 23 | # assert session['project'] not in ensure_list(session['projects']) 24 | 25 | for session in sessions: 26 | session.projects.add(session.project) 27 | # session.project = None 28 | # session.save() # No need to save 29 | 30 | assert Session.objects.exclude(Q(project__isnull=True) | Q(projects=F('project'))).count() == 0 31 | logger.info(f'project -> projects: {sessions.count():,g} sessions updated') 32 | 33 | 34 | class Migration(migrations.Migration): 35 | 36 | dependencies = [ 37 | ('actions', '0021_alter_session_extended_qc'), 38 | ] 39 | 40 | operations = [migrations.RunPython(project2projects)] 41 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0023_remove_session_project.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-14 14:54 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('actions', '0022_project_to_projects'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='session', 15 | name='project', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0024_surgery_implant_weight.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-15 13:53 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0023_remove_session_project'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='surgery', 16 | name='implant_weight', 17 | field=models.FloatField(blank=True, default=0, help_text='Implant weight in grams', validators=[django.core.validators.MinValueValidator(0)]), 18 | preserve_default=False, 19 | ), 20 | migrations.AddConstraint( 21 | model_name='surgery', 22 | constraint=models.CheckConstraint(check=models.Q(('implant_weight__gte', 0)), name='implant_weight_gte_0'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/actions/migrations/0026_alter_surgery_implant_weight.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2024-11-19 14:14 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0025_move_implant_weight'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='surgery', 16 | name='implant_weight', 17 | field=models.FloatField(help_text='Implant weight in grams', validators=[django.core.validators.MinValueValidator(0)]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/actions/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/actions/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/actions/static/ref_weighings_female.csv: -------------------------------------------------------------------------------- 1 | 4,14.10,1.80 5,16.90,1.20 6,17.50,1.00 7,18.20,1.10 8,18.70,1.20 9,19.30,1.20 10,19.80,1.30 11,20.20,1.40 12,20.70,1.40 13,21.70,1.50 14,22.00,1.60 15,22.30,1.70 16,22.60,1.80 17,22.60,2.00 18,23.00,2.10 19,23.50,2.30 20,23.60,2.30 21,23.60,2.30 22,23.60,2.30 23,23.60,2.30 24,23.60,2.30 25,23.60,2.30 26,23.60,2.30 27,23.60,2.30 28,23.60,2.30 29,23.60,2.30 30,23.60,2.30 31,23.60,2.30 32,23.60,2.30 33,23.60,2.30 34,23.60,2.30 35,23.60,2.30 36,23.60,2.30 37,23.60,2.30 38,23.60,2.30 39,23.60,2.30 40,23.60,2.30 41,23.60,2.30 42,23.60,2.30 43,23.60,2.30 44,23.60,2.30 45,23.60,2.30 46,23.60,2.30 47,23.60,2.30 48,23.60,2.30 49,23.60,2.30 50,23.60,2.30 51,23.60,2.30 52,23.60,2.30 53,23.60,2.30 54,23.60,2.30 55,23.60,2.30 56,23.60,2.30 57,23.60,2.30 58,23.60,2.30 59,23.60,2.30 60,23.60,2.30 61,23.60,2.30 62,23.60,2.30 63,23.60,2.30 64,23.60,2.30 65,23.60,2.30 66,23.60,2.30 67,23.60,2.30 68,23.60,2.30 69,23.60,2.30 70,23.60,2.30 71,23.60,2.30 72,23.60,2.30 73,23.60,2.30 74,23.60,2.30 75,23.60,2.30 76,23.60,2.30 77,23.60,2.30 78,23.60,2.30 79,23.60,2.30 80,23.60,2.30 81,23.60,2.30 82,23.60,2.30 83,23.60,2.30 84,23.60,2.30 85,23.60,2.30 86,23.60,2.30 87,23.60,2.30 88,23.60,2.30 89,23.60,2.30 90,23.60,2.30 91,23.60,2.30 92,23.60,2.30 93,23.60,2.30 94,23.60,2.30 95,23.60,2.30 96,23.60,2.30 -------------------------------------------------------------------------------- /alyx/actions/static/ref_weighings_male.csv: -------------------------------------------------------------------------------- 1 | 4,15.70,2.20 5,19.40,1.80 6,21.10,1.50 7,22.90,1.50 8,24.00,1.50 9,25.00,1.60 10,25.60,1.70 11,26.70,1.70 12,27.70,1.70 13,28.40,1.90 14,29.10,1.90 15,29.70,2.20 16,30.10,2.10 17,30.70,2.20 18,31.10,2.30 19,31.40,2.40 20,31.80,2.50 21,31.80,2.50 22,31.80,2.50 23,31.80,2.50 24,31.80,2.50 25,31.80,2.50 26,31.80,2.50 27,31.80,2.50 28,31.80,2.50 29,31.80,2.50 30,31.80,2.50 31,31.80,2.50 32,31.80,2.50 33,31.80,2.50 34,31.80,2.50 35,31.80,2.50 36,31.80,2.50 37,31.80,2.50 38,31.80,2.50 39,31.80,2.50 40,31.80,2.50 41,31.80,2.50 42,31.80,2.50 43,31.80,2.50 44,31.80,2.50 45,31.80,2.50 46,31.80,2.50 47,31.80,2.50 48,31.80,2.50 49,31.80,2.50 50,31.80,2.50 51,31.80,2.50 52,31.80,2.50 53,31.80,2.50 54,31.80,2.50 55,31.80,2.50 56,31.80,2.50 57,31.80,2.50 58,31.80,2.50 59,31.80,2.50 60,31.80,2.50 61,31.80,2.50 62,31.80,2.50 63,31.80,2.50 64,31.80,2.50 65,31.80,2.50 66,31.80,2.50 67,31.80,2.50 68,31.80,2.50 69,31.80,2.50 70,31.80,2.50 71,31.80,2.50 72,31.80,2.50 73,31.80,2.50 74,31.80,2.50 75,31.80,2.50 76,31.80,2.50 77,31.80,2.50 78,31.80,2.50 79,31.80,2.50 80,31.80,2.50 81,31.80,2.50 82,31.80,2.50 83,31.80,2.50 84,31.80,2.50 85,31.80,2.50 86,31.80,2.50 87,31.80,2.50 88,31.80,2.50 89,31.80,2.50 90,31.80,2.50 91,31.80,2.50 92,31.80,2.50 93,31.80,2.50 94,31.80,2.50 95,31.80,2.50 96,31.80,2.50 97,31.80,2.50 98,31.80,2.50 99,31.80,2.50 100,31.80,2.50 101,31.80,2.50 102,31.80,2.50 103,31.80,2.50 104,31.80,2.50 105,31.80,2.50 106,31.80,2.50 107,31.80,2.50 108,31.80,2.50 109,31.80,2.50 110,31.80,2.50 111,31.80,2.50 112,31.80,2.50 113,31.80,2.50 114,31.80,2.50 115,31.80,2.50 116,31.80,2.50 117,31.80,2.50 118,31.80,2.50 119,31.80,2.50 120,31.80,2.50 121,31.80,2.50 122,31.80,2.50 123,31.80,2.50 124,31.80,2.50 125,31.80,2.50 126,31.80,2.50 127,31.80,2.50 128,31.80,2.50 129,31.80,2.50 130,31.80,2.50 131,31.80,2.50 132,31.80,2.50 133,31.80,2.50 134,31.80,2.50 135,31.80,2.50 -------------------------------------------------------------------------------- /alyx/actions/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | import actions.views as av 3 | 4 | urlpatterns = [ 5 | path('admin-actions/weighing-plot/', 6 | av.weighing_plot, 7 | name='weighing-plot', 8 | ), 9 | 10 | path('admin-actions/water-history/', 11 | av.WaterHistoryListView.as_view(), 12 | name='water-history', 13 | ), 14 | 15 | path('admin-actions/training/', 16 | av.TrainingListView.as_view(), 17 | name='training', 18 | ), 19 | 20 | path('admin-actions/training/', 21 | av.TrainingListView.as_view(), 22 | name='training', 23 | ), 24 | 25 | path('admin-actions/subject-history/', 26 | av.SubjectHistoryListView.as_view(), 27 | name='subject-history', 28 | ), 29 | 30 | path('locations', av.LabLocationList.as_view(), name="location-list"), 31 | 32 | path('locations/', av.LabLocationAPIDetail.as_view(), 33 | name="location-detail"), 34 | 35 | path('procedures', av.ProcedureTypeList.as_view(), name="procedures-list"), 36 | 37 | path('sessions', av.SessionAPIList.as_view(), name="session-list"), 38 | 39 | path('sessions/', av.SessionAPIDetail.as_view(), 40 | name="session-detail"), 41 | 42 | path('surgeries', av.SurgeriesList.as_view(), name='surgeries-list'), 43 | 44 | path('surgeries/', av.SurgeriesAPIDetail.as_view(), name='surgeries-detail'), 45 | 46 | path('water-administrations', av.WaterAdministrationAPIListCreate.as_view(), 47 | name="water-administration-create"), 48 | 49 | path('water-administrations/', av.WaterAdministrationAPIDetail.as_view(), 50 | name="water-administration-detail"), 51 | 52 | path('water-requirement/', av.WaterRequirement.as_view(), 53 | name='water-requirement'), 54 | 55 | path('water-restriction', av.WaterRestrictionList.as_view(), 56 | name='water-restriction-list'), 57 | 58 | path('water-type', av.WaterTypeList.as_view(), 59 | name="watertype-list"), 60 | 61 | path('water-type/', av.WaterTypeList.as_view(), 62 | name="watertype-detail"), 63 | 64 | path('weighings', av.WeighingAPIListCreate.as_view(), 65 | name="weighing-create"), 66 | 67 | path('weighings/', av.WeighingAPIDetail.as_view(), 68 | name="weighing-detail"), 69 | 70 | 71 | ] 72 | -------------------------------------------------------------------------------- /alyx/alyx/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = __version__ = '3.2.2' 2 | -------------------------------------------------------------------------------- /alyx/alyx/settings_lab_template.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | # ALYX-SPECIFIC 4 | ALLOWED_HOSTS = ['localhost', '127.0.0.1'] 5 | LANGUAGE_CODE = 'en-us' 6 | TIME_ZONE = 'Europe/London' # NB: Changing the timezone here requires migrations 7 | GLOBUS_CLIENT_ID = '525cc543-8ccb-4d11-8036-af332da5eafd' 8 | SUBJECT_REQUEST_EMAIL_FROM = 'alyx@internationalbrainlab.org' 9 | DEFAULT_SOURCE = 'IBL' 10 | DEFAULT_PROTOCOL = '1' 11 | SUPERUSERS = ('root',) 12 | STOCK_MANAGERS = ('root',) 13 | WEIGHT_THRESHOLD = 0.8 # Absolute minimum weight threshold (red line in plots) 14 | DEFAULT_LAB_NAME = 'defaultlab' 15 | WATER_RESTRICTIONS_EDITABLE = False # if set to True, all users can edit water restrictions 16 | DEFAULT_LAB_PK = '4027da48-7be3-43ec-a222-f75dffe36872' 17 | SESSION_REPO_URL = \ 18 | "http://ibl.flatironinstitute.org/{lab}/Subjects/{subject}/{date}/{number:03d}/" 19 | NARRATIVE_TEMPLATES = { 20 | 'Headplate implant': dedent(''' 21 | == General == 22 | 23 | Start time (hh:mm): ___:___ 24 | End time (hh:mm): ___:___ 25 | 26 | Bregma-Lambda : _______ (mm) 27 | 28 | == Drugs == (copy paste as many times as needed; select IV, SC or IP) 29 | __________________( IV / SC / IP ) Admin. time (hh:mm) ___:___ 30 | 31 | == Coordinates == (copy paste as many times as needed; select B or L) 32 | (B / L) - Region: AP: _______ ML: ______ (mm) 33 | Region: _____________________________ 34 | 35 | == Notes == 36 | 37 | '''), 38 | } 39 | -------------------------------------------------------------------------------- /alyx/alyx/settings_secret_template.py: -------------------------------------------------------------------------------- 1 | # You should edit this file to match your settings and copy it to 2 | # "settings_secret.py". 3 | 4 | # SECURITY WARNING: keep the secret key used in production secret! 5 | SECRET_KEY = '%SECRET_KEY%' 6 | 7 | S3_ACCESS = {} # should include the keys (access_key, secret_key, region) 8 | 9 | # Database 10 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 15 | 'NAME': '%DBNAME%', 16 | 'USER': '%DBUSER%', 17 | 'PASSWORD': '%DBPASSWORD%', 18 | 'HOST': '127.0.0.1', 19 | 'PORT': '5432', 20 | } 21 | } 22 | 23 | EMAIL_HOST = 'mail.superserver.net' 24 | EMAIL_HOST_USER = 'alyx@awesomedomain.org' 25 | EMAIL_HOST_PASSWORD = 'UnbreakablePassword' 26 | EMAIL_PORT = 587 27 | EMAIL_USE_TLS = True 28 | -------------------------------------------------------------------------------- /alyx/alyx/test_base.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.test import TestCase 4 | from django.test import Client 5 | from alyx.base import _custom_filter_parser 6 | 7 | 8 | class BaseCustomFilterTest(TestCase): 9 | 10 | def test_parser(self): 11 | fixtures = [ 12 | ('gnagna,["NYU-21", "SH014"]', {"gnagna": ["NYU-21", "SH014"]}), # list 13 | ("gnagna,['NYU-21', 'SH014']", {"gnagna": ["NYU-21", "SH014"]}), # list 14 | ('fieldname,None', {"fieldname": None}), # None 15 | ('fieldname,true', {"fieldname": True}), # True insensitive 16 | ('fieldname,True', {"fieldname": True}), # True 17 | ('fieldname,False', {"fieldname": False}), # False 18 | ('fieldname,NYU', {"fieldname": "NYU"}), # string 19 | ('fieldname,14.2', {"fieldname": 14.2}), # float 20 | ('fieldname,142', {"fieldname": 142}), # integer 21 | ('f0,["toto"],f1,["tata"]', {"f0": ["toto"], "f1": ['tata']}), 22 | ('f0,val0,f1,("tata")', {"f0": "val0", "f1": 'tata'}), 23 | ('f0,val0,f1,("tata",)', {"f0": "val0", "f1": ('tata',)}) 24 | ] 25 | 26 | for fix in fixtures: 27 | self.assertEqual(_custom_filter_parser(fix[0]), fix[1]) 28 | 29 | def value_error_on_duplicate_field(): 30 | _custom_filter_parser('toto,abc,toto,1') 31 | self.assertRaises(ValueError, value_error_on_duplicate_field) 32 | 33 | 34 | def setup_admin_subject_user(obj): 35 | """Set up a user with permissions to access the admin site and a subject.""" 36 | from misc.models import LabMember, Lab 37 | from subjects.models import Subject 38 | from misc.management.commands.set_user_permissions import Command 39 | 40 | obj.client = Client() 41 | obj.user = LabMember.objects.create_user( 42 | username='foo', password='bar123', email='foo@example.com') 43 | obj.user.is_staff = obj.user.is_active = True # for change permissions 44 | obj.user.save() 45 | 46 | Command().handle() # set user group permissions 47 | obj.client.login(username='foo', password='bar123') 48 | try: 49 | obj.lab = Lab.objects.get(name='cortexlab') 50 | except Lab.DoesNotExist: 51 | obj.Lab = Lab.objects.create(name='cortexlab') 52 | obj.subject = Subject.objects.create( 53 | nickname='aQt', birth_date=date(2025, 1, 1), lab=obj.lab, actual_severity=2) 54 | -------------------------------------------------------------------------------- /alyx/alyx/urls.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from django.conf.urls import include 3 | from django.urls import path 4 | from django.contrib import admin 5 | from django.shortcuts import render 6 | from rest_framework.authtoken import views as authv 7 | from rest_framework.documentation import include_docs_urls 8 | 9 | admin.site.site_header = 'Alyx' 10 | 11 | urlpatterns = [ 12 | path('', include('misc.urls')), 13 | path('', include('experiments.urls')), 14 | path('', include('jobs.urls')), 15 | path('', include('actions.urls')), 16 | path('', include('data.urls')), 17 | path('', include('subjects.urls')), 18 | path('admin/doc/', include('django.contrib.admindocs.urls')), 19 | path('admin/', admin.site.urls), 20 | path('auth/', include('rest_framework.urls', namespace='rest_framework')), 21 | path('auth-token', authv.obtain_auth_token), 22 | path('docs/', include_docs_urls(title='Alyx REST API documentation')), 23 | ] 24 | 25 | # this is an optional app 26 | try: 27 | urlpatterns += [path('ibl_reports/', include('ibl_reports.urls')), ] 28 | except ModuleNotFoundError: 29 | pass 30 | 31 | 32 | def handler500(request): 33 | ctx = { 34 | 'request': request.path, 35 | 'traceback': traceback.format_exc() 36 | } 37 | return render(request, 'error_500.html', ctx) 38 | -------------------------------------------------------------------------------- /alyx/alyx/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for alyx project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alyx.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /alyx/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/data/__init__.py -------------------------------------------------------------------------------- /alyx/data/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DataConfig(AppConfig): 5 | name = 'data' 6 | -------------------------------------------------------------------------------- /alyx/data/fixtures/data.datarepositorytype.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "data.datarepositorytype", 4 | "pk": "8047d768-c04a-440e-91aa-f221719c4bc5", 5 | "fields": { 6 | "json": "", 7 | "name": "Fileserver" 8 | } 9 | }, 10 | { 11 | "model": "data.datarepositorytype", 12 | "pk": "bd237c2c-d114-49fa-adc8-cfaca4fc2f87", 13 | "fields": { 14 | "json": "", 15 | "name": "Cardboard box" 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/data/management/__init__.py -------------------------------------------------------------------------------- /alyx/data/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/data/management/commands/__init__.py -------------------------------------------------------------------------------- /alyx/data/migrations/0003_auto_20190315_1716.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2019-03-15 17:16 2 | 3 | from django.conf import settings 4 | import django.contrib.postgres.fields.jsonb 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('subjects', '0003_auto_20190124_1025'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('data', '0002_auto_20181015_0914'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Download', 21 | fields=[ 22 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 23 | ('name', models.CharField(blank=True, help_text='Long name', max_length=255)), 24 | ('json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True)), 25 | ('first_download', models.DateTimeField(auto_now_add=True)), 26 | ('last_download', models.DateTimeField(auto_now=True)), 27 | ('count', models.IntegerField(default=0)), 28 | ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Dataset')), 29 | ('projects', models.ManyToManyField(blank=True, to='subjects.Project')), 30 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | migrations.AlterUniqueTogether( 34 | name='filerecord', 35 | unique_together={('data_repository', 'relative_path')}, 36 | ), 37 | migrations.AlterUniqueTogether( 38 | name='download', 39 | unique_together={('user', 'dataset')}, 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /alyx/data/migrations/0004_dataset_filesize_int64.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-07-31 23:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0003_auto_20190315_1716'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='dataset', 15 | name='file_size', 16 | field=models.BigIntegerField(blank=True, help_text='Size in bytes', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/migrations/0005_dataset_collection.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-11-15 06:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0004_dataset_filesize_int64'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='dataset', 15 | name='collection', 16 | field=models.CharField(blank=True, help_text='file subcollection or subfolder', max_length=255, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/migrations/0006_dataset_hash_version.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-01-17 15:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0005_dataset_collection'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='dataset', 15 | name='hash', 16 | field=models.CharField(blank=True, help_text='Hash of the data buffer, SHA-1 is 40 hex chars, while md5is 32 hex chars', max_length=64, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='dataset', 20 | name='version', 21 | field=models.CharField(blank=True, help_text='version of the algorithm generating the file', max_length=64, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /alyx/data/migrations/0007_dataset_auto_datetime.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2021-02-09 17:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0006_dataset_hash_version'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='dataset', 15 | name='auto_datetime', 16 | field=models.DateTimeField(auto_now=True, null=True, verbose_name='last updated'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/migrations/0010_alter_dataset_created_by_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-03-30 15:46 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('actions', '0017_alter_chronicrecording_subject_and_more'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('data', '0009_auto_20210624_1253'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='dataset', 19 | name='created_by', 20 | field=models.ForeignKey(blank=True, help_text='The creator of the data.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_created_by_related', to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AlterField( 23 | model_name='dataset', 24 | name='provenance_directory', 25 | field=models.ForeignKey(blank=True, help_text='link to directory containing intermediate results', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_provenance_related', to='data.dataset'), 26 | ), 27 | migrations.AlterField( 28 | model_name='dataset', 29 | name='session', 30 | field=models.ForeignKey(blank=True, help_text='The Session to which this data belongs', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_session_related', to='actions.session'), 31 | ), 32 | migrations.AlterField( 33 | model_name='datasettype', 34 | name='created_by', 35 | field=models.ForeignKey(blank=True, help_text='The creator of the data.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_created_by_related', to=settings.AUTH_USER_MODEL), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /alyx/data/migrations/0011_alter_datasettype_filename_pattern.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.6 on 2022-06-24 15:14 2 | 3 | import alyx.base 4 | from django.db import migrations 5 | 6 | 7 | def str2null(apps, _): 8 | DatasetType = apps.get_model('data', 'DatasetType') 9 | for dstype in DatasetType.objects.filter(filename_pattern=''): 10 | dstype.save() 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ('data', '0010_alter_dataset_created_by_and_more'), 17 | ] 18 | 19 | operations = [ 20 | migrations.AlterField( 21 | model_name='datasettype', 22 | name='filename_pattern', 23 | field=alyx.base.CharNullField(blank=True, help_text="File name pattern (with wildcards) for this file in ALF naming convention. E.g. 'spikes.times.*' or '*.timestamps.*', or 'spikes.*.*' for a DataCollection, which would include all files starting with the word 'spikes'.", max_length=255, null=True, unique=True), 24 | ), 25 | migrations.RunPython(str2null), 26 | ] 27 | 28 | -------------------------------------------------------------------------------- /alyx/data/migrations/0012_alter_datasettype_filename_pattern_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.6 on 2022-07-07 17:39 2 | 3 | import alyx.base 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('data', '0011_alter_datasettype_filename_pattern'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='datasettype', 16 | name='filename_pattern', 17 | field=alyx.base.CharNullField(blank=True, help_text="File name pattern (with wildcards) for this file in ALF naming convention. E.g. 'spikes.times.*' or '*.timestamps.*', or 'spikes.*.*' for a DataCollection, which would include all files starting with the word 'spikes'. NB: Case-insensitive matching.If null, the name field must match the object.attribute part of the filename.", max_length=255, null=True, unique=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='datasettype', 21 | name='name', 22 | field=models.CharField(blank=True, help_text="Short identifying nickname, e.g. 'spikes.times'", max_length=255, unique=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/data/migrations/0013_dataset_content_type_dataset_object_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-01-31 12:34 2 | 3 | from django.db import migrations, transaction, models 4 | import django.db.models.deletion 5 | 6 | 7 | def forwards(apps, _): 8 | """Go through the datasets and assign the session field to the content_object field""" 9 | Dataset = apps.get_model('data', 'Dataset') 10 | with transaction.atomic(): 11 | for dataset in Dataset.objects.filter(session__isnull=False).iterator(): 12 | if dataset.content_object is None: 13 | dataset.content_object = dataset.session 14 | dataset.save() 15 | 16 | 17 | def backwards(apps, _): 18 | Dataset = apps.get_model('data', 'Dataset') 19 | with transaction.atomic(): 20 | for dataset in Dataset.objects.filter(session__isnull=False).iterator(): 21 | if dataset.content_object is not None: 22 | dataset.content_object = None 23 | dataset.save() 24 | 25 | 26 | class Migration(migrations.Migration): 27 | 28 | dependencies = [ 29 | ('contenttypes', '0002_remove_content_type_name'), 30 | ('data', '0012_alter_datasettype_filename_pattern_and_more'), 31 | ] 32 | 33 | operations = [ 34 | migrations.AddField( 35 | model_name='dataset', 36 | name='content_type', 37 | field=models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), 38 | preserve_default=False, 39 | ), 40 | migrations.AddField( 41 | model_name='dataset', 42 | name='object_id', 43 | field=models.UUIDField( 44 | null=True, blank=True, help_text='UUID, an object of content_type with this ID must already exist to attach a note.'), 45 | preserve_default=False, 46 | ), 47 | # migrations.RunPython(forwards, backwards) 48 | ] 49 | -------------------------------------------------------------------------------- /alyx/data/migrations/0014_alter_datasettype_filename_pattern.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 15:32 2 | # NB: The previous migrations (0011 and 0012) somehow failed to set the filename_pattern field as 3 | # nullable. Migrations 0014 and 0015 reverse and remake this change which apparently fixed this 4 | # issue. 5 | 6 | import alyx.base 7 | from django.db import migrations, transaction 8 | 9 | PATTERN = '$$$' 10 | 11 | 12 | def fix_null_fields(apps, _): 13 | """Populate null filename_pattern fields before making column not null""" 14 | DatasetType = apps.get_model('data', 'DatasetType') 15 | assert not DatasetType.objects.filter(filename_pattern__startswith=PATTERN).count() 16 | with transaction.atomic(): 17 | for dtype in DatasetType.objects.filter(filename_pattern__isnull=True).iterator(): 18 | dtype.filename_pattern = PATTERN + dtype.name 19 | dtype.save() 20 | 21 | 22 | def null_fields(apps, _): 23 | """Reset previously null filename_pattern fields""" 24 | DatasetType = apps.get_model('data', 'DatasetType') 25 | with transaction.atomic(): 26 | for dtype in DatasetType.objects.filter(filename_pattern__startswith=PATTERN).iterator(): 27 | dtype.filename_pattern = None 28 | dtype.save() 29 | 30 | 31 | class Migration(migrations.Migration): 32 | 33 | dependencies = [ 34 | ('data', '0013_dataset_content_type_dataset_object_id'), 35 | ] 36 | 37 | operations = [ 38 | migrations.RunPython(fix_null_fields, null_fields), 39 | migrations.AlterField( 40 | model_name='datasettype', 41 | name='filename_pattern', 42 | field=alyx.base.CharNullField(blank=True, help_text="File name pattern (with wildcards) for this file in ALF naming convention. E.g. 'spikes.times.*' or '*.timestamps.*', or 'spikes.*.*' for a DataCollection, which would include all files starting with the word 'spikes'. NB: Case-insensitive matching.If null, the name field must match the object.attribute part of the filename.", max_length=255, unique=True), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /alyx/data/migrations/0015_alter_datasettype_filename_pattern.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.5 on 2023-02-01 15:33 2 | # NB: The previous migrations (0011 and 0012) somehow failed to set the filname_pattern field as 3 | # nullable. Migrations 0014 and 0015 reverse and remake this change which apparently fixed this 4 | # issue. 5 | 6 | import alyx.base 7 | from django.db import migrations, transaction 8 | 9 | PATTERN = '$$$' 10 | 11 | 12 | def fix_null_fields(apps, _): 13 | """Populate null filename_pattern fields before making column not null""" 14 | DatasetType = apps.get_model('data', 'DatasetType') 15 | assert not DatasetType.objects.filter(filename_pattern__startswith=PATTERN).count() 16 | with transaction.atomic(): 17 | for dtype in DatasetType.objects.filter(filename_pattern__isnull=True).iterator(): 18 | dtype.filename_pattern = PATTERN + dtype.name 19 | dtype.save() 20 | 21 | 22 | def null_fields(apps, _): 23 | """Reset previously null filename_pattern fields""" 24 | DatasetType = apps.get_model('data', 'DatasetType') 25 | with transaction.atomic(): 26 | for dtype in DatasetType.objects.filter(filename_pattern__startswith=PATTERN).iterator(): 27 | dtype.filename_pattern = None 28 | dtype.save() 29 | 30 | 31 | class Migration(migrations.Migration): 32 | 33 | dependencies = [ 34 | ('data', '0014_alter_datasettype_filename_pattern'), 35 | ] 36 | 37 | operations = [ 38 | migrations.AlterField( 39 | model_name='datasettype', 40 | name='filename_pattern', 41 | field=alyx.base.CharNullField(blank=True, help_text="File name pattern (with wildcards) for this file in ALF naming convention. E.g. 'spikes.times.*' or '*.timestamps.*', or 'spikes.*.*' for a DataCollection, which would include all files starting with the word 'spikes'. NB: Case-insensitive matching.If null, the name field must match the object.attribute part of the filename.", max_length=255, null=True, unique=True), 42 | ), 43 | migrations.RunPython(null_fields, fix_null_fields), 44 | ] 45 | -------------------------------------------------------------------------------- /alyx/data/migrations/0016_alter_dataset_collection_alter_revision_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-05-11 16:24 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('data', '0015_alter_datasettype_filename_pattern'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='dataset', 16 | name='collection', 17 | field=models.CharField(blank=True, help_text='file subcollection or subfolder', max_length=255, null=True, validators=[django.core.validators.RegexValidator('^[\\w/]+$', 'Collections must only contain letters, numbers, hyphens, underscores and forward slashes.')]), 18 | ), 19 | migrations.AlterField( 20 | model_name='revision', 21 | name='name', 22 | field=models.CharField(blank=True, help_text='Long name', max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[\\w-]+$', 'Revisions must only contain letters, numbers, hyphens, underscores and forward slashes.')]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/data/migrations/0017_alter_dataset_object_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-06-16 09:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0016_alter_dataset_collection_alter_revision_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='dataset', 15 | name='object_id', 16 | field=models.UUIDField(blank=True, help_text='UUID of an object whose type matches content_type.', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/migrations/0018_alter_dataset_collection_alter_revision_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-07-10 11:52 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('data', '0017_alter_dataset_object_id'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='dataset', 16 | name='collection', 17 | field=models.CharField(blank=True, help_text='file subcollection or subfolder', max_length=255, null=True, validators=[django.core.validators.RegexValidator('^[\\w./-]+$', 'Collections must only contain letters, numbers, hyphens, underscores and forward slashes.')]), 18 | ), 19 | migrations.AlterField( 20 | model_name='revision', 21 | name='name', 22 | field=models.CharField(blank=True, help_text='Long name', max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Revisions must only contain letters, numbers, hyphens, underscores and forward slashes.')]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/data/migrations/0019_dataset_qc.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-02-13 15:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0018_alter_dataset_collection_alter_revision_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='dataset', 15 | name='qc', 16 | field=models.IntegerField(choices=[(50, 'CRITICAL'), (40, 'FAIL'), (30, 'WARNING'), (0, 'NOT_SET'), (10, 'PASS')], default=0, help_text='50: CRITICAL / 40: FAIL / 30: WARNING / 0: NOT_SET / 10: PASS'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/data/migrations/0020_alter_datarepository_timezone.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 15:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("data", "0019_dataset_qc"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="datarepository", 15 | name="timezone", 16 | field=models.CharField( 17 | blank=True, 18 | default="Europe/London", 19 | help_text="Timezone of the server (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)", 20 | max_length=64, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /alyx/data/migrations/0021_alter_dataset_collection_alter_dataset_hash_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2024-11-01 17:53 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('data', '0020_alter_datarepository_timezone'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='dataset', 16 | name='collection', 17 | field=models.CharField(blank=True, default='', help_text='file subcollection or subfolder', max_length=255, validators=[django.core.validators.RegexValidator('^[\\w./-]+$', 'Collections must only contain letters, numbers, hyphens, underscores and forward slashes.')]), 18 | preserve_default=False, 19 | ), 20 | migrations.AlterField( 21 | model_name='dataset', 22 | name='hash', 23 | field=models.CharField(blank=True, default='', help_text='Hash of the data buffer, SHA-1 is 40 hex chars, while md5is 32 hex chars', max_length=64), 24 | preserve_default=False, 25 | ), 26 | migrations.AlterField( 27 | model_name='dataset', 28 | name='version', 29 | field=models.CharField(blank=True, default='', help_text='version of the algorithm generating the file', max_length=64), 30 | preserve_default=False, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /alyx/data/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/data/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/experiments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/experiments/__init__.py -------------------------------------------------------------------------------- /alyx/experiments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExperimentsConfig(AppConfig): 5 | name = 'experiments' 6 | -------------------------------------------------------------------------------- /alyx/experiments/fixtures/experiments.coordinatesystem.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "experiments.coordinatesystem", 4 | "pk": "3b00d02e-ea91-4a37-9427-d83de37a1b63", 5 | "fields": { 6 | "name": "Needles-Allen", 7 | "json": null, 8 | "description": "Coordinate system based on the Allen brain Atlas with an antero-posterior stretch and a dorso-ventral squeeze.\r\n\r\nOrigin Bregma, millimeters\r\nx: medio-lateral, right positive\r\ny: antero-posteror: front positive\r\nz: dorso-ventral: up positive.\r\n\r\nhttps://github.com/int-brain-lab/ibllib/blob/master/ibllib/atlas/atlas.py\r\nhttps://github.com/int-brain-lab/ibllib-matlab" 9 | } 10 | }, 11 | { 12 | "model": "experiments.coordinatesystem", 13 | "pk": "cbc64f28-6aa5-4331-a9fd-7648ffa13876", 14 | "fields": { 15 | "name": "IBL-Allen", 16 | "json": null, 17 | "description": "Coordinate system based on the Allen brain Atlas for mice.\r\n\r\nOrigin Bregma, millimeters\r\nx: medio-lateral, right positive\r\ny: antero-posteror: front positive\r\nz: dorso-ventral: up positive.\r\n\r\nThe API below works with several atlas resolutions:\r\nhttps://github.com/int-brain-lab/ibllib/blob/master/ibllib/atlas/atlas.py" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /alyx/experiments/fixtures/experiments.imagingtype.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "experiments.imagingtype", 4 | "pk": "8c009775-2bff-4de7-baca-c27dc949426e", 5 | "fields": { 6 | "json": null, 7 | "name": "mesoscope" 8 | } 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /alyx/experiments/fixtures/experiments.probemodel.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "experiments.probemodel", 4 | "pk": "057d9a20-aaa9-4f79-8b0c-c8729b4cf160", 5 | "fields": { 6 | "name": "Neuropixel 3B2", 7 | "json": null, 8 | "probe_manufacturer": "Imec", 9 | "probe_model": "3B2", 10 | "description": null 11 | } 12 | }, 13 | { 14 | "model": "experiments.probemodel", 15 | "pk": "100fe998-986c-467d-aac8-a470e3a34341", 16 | "fields": { 17 | "name": "Neurophotometrics Fiber", 18 | "json": null, 19 | "probe_manufacturer": "Neurophotometrics", 20 | "probe_model": "Fiber", 21 | "description": null 22 | } 23 | }, 24 | { 25 | "model": "experiments.probemodel", 26 | "pk": "5df5182a-5113-4e37-b690-0e6cae3abc6a", 27 | "fields": { 28 | "name": "Neuropixel 2.0 single shank", 29 | "json": null, 30 | "probe_manufacturer": "Imec", 31 | "probe_model": "NP2.1", 32 | "description": null 33 | } 34 | }, 35 | { 36 | "model": "experiments.probemodel", 37 | "pk": "6f586705-13a7-4c2a-ac4a-ef58b10d9bc4", 38 | "fields": { 39 | "name": "Neuropixel 2.0 four shank", 40 | "json": null, 41 | "probe_manufacturer": "Imec", 42 | "probe_model": "NP2.4", 43 | "description": null 44 | } 45 | }, 46 | { 47 | "model": "experiments.probemodel", 48 | "pk": "96dccdfe-7206-4b65-bb17-0743c842acd0", 49 | "fields": { 50 | "name": "Neuropixel 3A", 51 | "json": null, 52 | "probe_manufacturer": "Imec", 53 | "probe_model": "3A", 54 | "description": null 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /alyx/experiments/fixtures/load_allen.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import numpy as np 3 | 4 | from experiments.models import BrainRegion 5 | 6 | csv_file = "/var/www/alyx-dev/alyx/experiments/fixtures/allen_structure_tree.csv" 7 | 8 | with open(csv_file, newline='') as fid: 9 | data = list(csv.reader(fid)) 10 | 11 | data.pop(0) 12 | id = np.array([int(d[0]) for d in data], dtype='int32') 13 | name = np.array([d[2] for d in data], dtype='object') 14 | acronym = np.array([d[3] for d in data], dtype='object') 15 | parent = np.array([int(d[8] or '0') for d in data], dtype='int32') 16 | 17 | for d in data: 18 | id = int(d[0]) 19 | name = d[2] 20 | acronym = d[3] 21 | parent = int(d[8] or '0') 22 | br, _ = BrainRegion.objects.get_or_create(id=id, name=name, acronym=acronym) 23 | if parent != br.pk: 24 | br.parent = BrainRegion.objects.get(id=parent) 25 | br.save() 26 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0003_probe_insertion_serial_20200504_1132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-05-04 11:32 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('experiments', '0002_channels_20200402'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='probeinsertion', 16 | name='serial', 17 | field=models.CharField(blank=True, help_text='Probe serial number', max_length=255), 18 | ), 19 | migrations.AlterField( 20 | model_name='trajectoryestimate', 21 | name='phi', 22 | field=models.FloatField(help_text='Azimuth from right (degrees), anti-clockwise, [0-360]', null=True, validators=[django.core.validators.MinValueValidator(-180), django.core.validators.MaxValueValidator(360)]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0004_auto_20200519_2129.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-05-19 21:29 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('experiments', '0003_probe_insertion_serial_20200504_1132'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='brainregion', 16 | name='description', 17 | field=models.TextField(blank=True, null=True), 18 | ), 19 | migrations.AddField( 20 | model_name='brainregion', 21 | name='ontology', 22 | field=models.CharField(blank=True, max_length=64, null=True), 23 | ), 24 | migrations.AddField( 25 | model_name='trajectoryestimate', 26 | name='json', 27 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0005_trajectoryestimate_datetime.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-06-08 12:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('experiments', '0004_auto_20200519_2129'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='trajectoryestimate', 15 | name='datetime', 16 | field=models.DateTimeField(auto_now=True, verbose_name='last update'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0006_auto_20201119_0953.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-19 09:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('experiments', '0005_trajectoryestimate_datetime'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='trajectoryestimate', 15 | name='provenance', 16 | field=models.IntegerField(choices=[(70, 'Ephys aligned histology track'), (50, 'Histology track'), (30, 'Micro-manipulator'), (10, 'Planned')], default=10, help_text='70: Ephys aligned histology track / 50: Histology track / 30: Micro-manipulator / 10: Planned'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0007_probeinsertion_auto_datetime.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2021-02-09 17:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('experiments', '0006_auto_20201119_0953'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='probeinsertion', 15 | name='auto_datetime', 16 | field=models.DateTimeField(auto_now=True, null=True, verbose_name='last updated'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0008_probeinsertion_datasets.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2021-02-21 22:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0007_dataset_auto_datetime'), 10 | ('experiments', '0007_probeinsertion_auto_datetime'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='probeinsertion', 16 | name='datasets', 17 | field=models.ManyToManyField(blank=True, related_name='probe_insertion', to='data.Dataset'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0009_auto_20210624_1253.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-24 12:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('experiments', '0008_probeinsertion_datasets'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='channel', 15 | name='json', 16 | field=models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='coordinatesystem', 20 | name='json', 21 | field=models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='probeinsertion', 25 | name='json', 26 | field=models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='probemodel', 30 | name='json', 31 | field=models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 32 | ), 33 | migrations.AlterField( 34 | model_name='trajectoryestimate', 35 | name='json', 36 | field=models.JSONField(blank=True, help_text='Structured data, formatted in a user-defined way', null=True), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0010_probeinsertion_chronic_recording.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-07-01 20:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0016_chronicrecording'), 11 | ('experiments', '0009_auto_20210624_1253'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='probeinsertion', 17 | name='chronic_recording', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='probe_insertion', to='actions.chronicrecording'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0011_chronic_insertion.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-12-07 10:02 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('actions', '0018_session_projects_alter_session_project'), 11 | ('experiments', '0010_probeinsertion_chronic_recording'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='probeinsertion', 17 | name='chronic_recording', 18 | ), 19 | migrations.CreateModel( 20 | name='ChronicInsertion', 21 | fields=[ 22 | ('chronicrecording_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='actions.chronicrecording')), 23 | ('serial', models.CharField(blank=True, help_text='Probe serial number', max_length=255)), 24 | ('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chronic_insertion', to='experiments.probemodel')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | bases=('actions.chronicrecording',), 30 | ), 31 | migrations.AddField( 32 | model_name='probeinsertion', 33 | name='chronic_insertion', 34 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='probe_insertion', to='experiments.chronicinsertion'), 35 | ), 36 | migrations.AddField( 37 | model_name='trajectoryestimate', 38 | name='chronic_insertion', 39 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='trajectory_estimate', to='experiments.chronicinsertion'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0013_remove_trajectoryestimate_unique_trajectory_per_provenance_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-03-22 12:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('experiments', '0012_fov_imagingstack_imagingtype_alter_channel_lateral_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveConstraint( 14 | model_name='trajectoryestimate', 15 | name='unique_trajectory_per_provenance', 16 | ), 17 | migrations.AddConstraint( 18 | model_name='trajectoryestimate', 19 | constraint=models.UniqueConstraint(condition=models.Q(('probe_insertion__isnull', True)), fields=('provenance', 'chronic_insertion'), name='unique_trajectory_per_chronic_provenance'), 20 | ), 21 | migrations.AddConstraint( 22 | model_name='trajectoryestimate', 23 | constraint=models.UniqueConstraint(condition=models.Q(('probe_insertion__isnull', False)), fields=('provenance', 'probe_insertion'), name='unique_trajectory_per_provenance'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/0014_alter_probeinsertion_chronic_insertion.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2024-11-14 11:01 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('experiments', '0013_remove_trajectoryestimate_unique_trajectory_per_provenance_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='probeinsertion', 16 | name='chronic_insertion', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='probe_insertion', to='experiments.chronicinsertion'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/experiments/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/experiments/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/experiments/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from experiments import views as ev 3 | 4 | urlpatterns = [ 5 | path('insertions', ev.ProbeInsertionList.as_view(), name="probeinsertion-list"), 6 | path('insertions/', ev.ProbeInsertionDetail.as_view(), name="probeinsertion-detail"), 7 | path('trajectories', ev.TrajectoryEstimateList.as_view(), name="trajectoryestimate-list"), 8 | path('trajectories/', ev.TrajectoryEstimateDetail.as_view(), 9 | name="trajectoryestimate-detail"), 10 | path('channels', ev.ChannelList.as_view(), name="channel-list"), 11 | path('channels/', ev.ChannelDetail.as_view(), name="channel-detail"), 12 | path('brain-regions', ev.BrainRegionList.as_view(), name="brainregion-list"), 13 | path('brain-regions/', ev.BrainRegionDetail.as_view(), name="brainregion-detail"), 14 | path('chronic-insertions', ev.ChronicInsertionList.as_view(), name="chronicinsertion-list"), 15 | path('chronic-insertions/', ev.ChronicInsertionDetail.as_view(), 16 | name="chronicinsertion-detail"), 17 | path('fields-of-view', ev.FOVList.as_view(), name="fieldsofview-list"), 18 | path('fields-of-view/', ev.FOVDetail.as_view(), name="fieldsofview-detail"), 19 | path('fov-location', ev.FOVLocationList.as_view(), name="fovlocation-list"), 20 | path('fov-location/', ev.FOVLocationDetail.as_view(), name="fovlocation-detail"), 21 | path('imaging-stack', ev.ImagingStackList.as_view(), name="imagingstack-list"), 22 | path('imaging-stack/', ev.ImagingStackDetail.as_view(), name="imagingstack-detail")] 23 | -------------------------------------------------------------------------------- /alyx/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/jobs/__init__.py -------------------------------------------------------------------------------- /alyx/jobs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JobsConfig(AppConfig): 5 | name = 'jobs' 6 | -------------------------------------------------------------------------------- /alyx/jobs/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/jobs/management/__init__.py -------------------------------------------------------------------------------- /alyx/jobs/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/jobs/management/commands/__init__.py -------------------------------------------------------------------------------- /alyx/jobs/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-06-23 17:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('actions', '0011_auto_20200317_1055'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Task', 19 | fields=[ 20 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 21 | ('name', models.CharField(blank=True, max_length=64, null=True)), 22 | ('priority', models.SmallIntegerField(blank=True, null=True)), 23 | ('io_charge', models.SmallIntegerField(blank=True, null=True)), 24 | ('level', models.SmallIntegerField(blank=True, null=True)), 25 | ('gpu', models.SmallIntegerField(blank=True, null=True)), 26 | ('cpu', models.SmallIntegerField(blank=True, null=True)), 27 | ('ram', models.SmallIntegerField(blank=True, null=True)), 28 | ('time_out_secs', models.SmallIntegerField(blank=True, null=True)), 29 | ('time_elapsed_secs', models.FloatField(blank=True, null=True)), 30 | ('executable', models.CharField(blank=True, help_text='Usually the Python class name on the workers', max_length=128, null=True)), 31 | ('graph', models.CharField(blank=True, help_text='The name of the graph containing a set of related and possibly dependent tasks', max_length=64, null=True)), 32 | ('status', models.IntegerField(choices=[(20, 'Waiting'), (30, 'Started'), (40, 'Errored'), (50, 'Empty'), (60, 'Complete')], default=10)), 33 | ('log', models.TextField(blank=True, null=True)), 34 | ('version', models.CharField(blank=True, help_text='version of the algorithm generating the file', max_length=64, null=True)), 35 | ('datetime', models.DateTimeField(auto_now=True)), 36 | ('parents', models.ManyToManyField(blank=True, related_name='children', to='jobs.Task')), 37 | ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='actions.Session')), 38 | ], 39 | ), 40 | migrations.AddConstraint( 41 | model_name='task', 42 | constraint=models.UniqueConstraint(fields=('name', 'session'), name='unique_task_name_per_session'), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0002_auto_20200629_0935.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-06-29 09:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('jobs', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='task', 16 | name='session', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='actions.Session'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0003_auto_20200705_2046.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-07-05 20:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobs', '0002_auto_20200629_0935'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='task', 15 | name='status', 16 | field=models.IntegerField(choices=[(20, 'Waiting'), (25, 'Held'), (30, 'Started'), (40, 'Errored'), (50, 'Empty'), (60, 'Complete')], default=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0004_auto_20200731_0936.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-07-31 09:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobs', '0003_auto_20200705_2046'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='task', 15 | name='status', 16 | field=models.IntegerField(choices=[(20, 'Waiting'), (25, 'Held'), (30, 'Started'), (40, 'Errored'), (45, 'Abandoned'), (50, 'Empty'), (60, 'Complete')], default=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0005_auto_20201119_0953.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-19 09:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobs', '0004_auto_20200731_0936'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='task', 15 | name='status', 16 | field=models.IntegerField(choices=[(20, 'Waiting'), (25, 'Held'), (30, 'Started'), (40, 'Errored'), (45, 'Abandoned'), (50, 'Empty'), (60, 'Complete')], default=10, help_text='20: Waiting / 25: Held / 30: Started / 40: Errored / 45: Abandoned / 50: Empty / 60: Complete'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0006_alter_task_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-07-01 11:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobs', '0005_auto_20201119_0953'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='task', 15 | name='status', 16 | field=models.IntegerField(choices=[(20, 'Waiting'), (25, 'Held'), (30, 'Started'), (40, 'Errored'), (45, 'Abandoned'), (50, 'Empty'), (55, 'Incomplete'), (60, 'Complete')], default=10, help_text='20: Waiting / 25: Held / 30: Started / 40: Errored / 45: Abandoned / 50: Empty / 55: Incomplete / 60: Complete'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0007_remove_task_unique_task_name_per_session_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-06-21 19:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobs', '0006_alter_task_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveConstraint( 14 | model_name='task', 15 | name='unique_task_name_per_session', 16 | ), 17 | migrations.AddField( 18 | model_name='task', 19 | name='arguments', 20 | field=models.JSONField(blank=True, help_text='dictionary of input arguments', null=True), 21 | ), 22 | migrations.AddConstraint( 23 | model_name='task', 24 | constraint=models.UniqueConstraint(fields=('name', 'session', 'arguments'), name='unique_name_arguments_per_session'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/0008_task_data_repository.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.6 on 2022-09-13 16:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('data', '0012_alter_datasettype_filename_pattern_and_more'), 11 | ('jobs', '0007_remove_task_unique_task_name_per_session_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='task', 17 | name='data_repository', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='data.datarepository'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /alyx/jobs/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/jobs/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/jobs/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from actions.models import Session 3 | from jobs.models import Task 4 | from data.models import DataRepository 5 | from alyx.base import BaseSerializerEnumField 6 | 7 | 8 | class TaskSerializer(serializers.ModelSerializer): 9 | parents = serializers.SlugRelatedField( 10 | read_only=False, required=False, slug_field='id', many=True, 11 | queryset=Task.objects.all(), 12 | ) 13 | session = serializers.SlugRelatedField( 14 | read_only=False, required=False, slug_field='id', many=False, 15 | queryset=Session.objects.all(), allow_null=True 16 | ) 17 | data_repository = serializers.SlugRelatedField( 18 | read_only=False, required=False, slug_field='name', many=False, 19 | queryset=DataRepository.objects.all() 20 | ) 21 | status = BaseSerializerEnumField(required=False) 22 | 23 | class Meta: 24 | model = Task 25 | fields = '__all__' 26 | -------------------------------------------------------------------------------- /alyx/jobs/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/jobs/templatetags/__init__.py -------------------------------------------------------------------------------- /alyx/jobs/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from jobs import views as jv 3 | 4 | urlpatterns = [ 5 | path('tasks', jv.TaskList.as_view(), name="tasks-list"), 6 | path('tasks/', jv.TaskDetail.as_view(), name="tasks-detail"), 7 | 8 | 9 | path('admin-tasks/status', jv.TasksStatusView.as_view(), name='tasks_status',), 10 | path('admin-tasks/status/', jv.TasksStatusView.as_view(), 11 | name='tasks_status_graph', ), 12 | path('admin-tasks/status//', jv.TasksStatusView.as_view(), 13 | name='tasks_status_graph_lab', ), 14 | ] 15 | -------------------------------------------------------------------------------- /alyx/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alyx.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /alyx/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/misc/__init__.py -------------------------------------------------------------------------------- /alyx/misc/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'misc' 6 | -------------------------------------------------------------------------------- /alyx/misc/fixtures/misc.cagetype.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "misc.cagetype", 4 | "pk": "0de47422-624b-4d29-9051-8a8a1e5aaceb", 5 | "fields": { 6 | "name": "GM500", 7 | "json": null, 8 | "description": "" 9 | } 10 | }, 11 | { 12 | "model": "misc.cagetype", 13 | "pk": "16261ee8-a2df-4c35-9c0d-dd4cf66e3be5", 14 | "fields": { 15 | "name": "Micro-Isolator", 16 | "json": null, 17 | "description": "" 18 | } 19 | }, 20 | { 21 | "model": "misc.cagetype", 22 | "pk": "630f19e7-d87e-45ad-8954-b2663d3839fe", 23 | "fields": { 24 | "name": "IVC", 25 | "json": null, 26 | "description": "" 27 | } 28 | }, 29 | { 30 | "model": "misc.cagetype", 31 | "pk": "cc4ad0ee-2a66-4612-a277-3f4659c74128", 32 | "fields": { 33 | "name": "Allentown", 34 | "json": null, 35 | "description": "" 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /alyx/misc/fixtures/misc.enrichment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "misc.enrichment", 4 | "pk": "0dc9ef0b-2f4b-40a6-a2b0-59f1990fa8fc", 5 | "fields": { 6 | "json": null, 7 | "name": "House", 8 | "description": "" 9 | } 10 | }, 11 | { 12 | "model": "misc.enrichment", 13 | "pk": "3cc8b8c8-634b-4811-984f-65989846e910", 14 | "fields": { 15 | "json": null, 16 | "name": "Nesting", 17 | "description": "" 18 | } 19 | }, 20 | { 21 | "model": "misc.enrichment", 22 | "pk": "51fd5a81-d08c-4dae-9075-1b66d9996725", 23 | "fields": { 24 | "json": null, 25 | "name": "Geometric object <5cm", 26 | "description": "" 27 | } 28 | }, 29 | { 30 | "model": "misc.enrichment", 31 | "pk": "535c8de9-feb0-44b8-871e-2a4e0866d3c5", 32 | "fields": { 33 | "json": null, 34 | "name": "Wheel", 35 | "description": "" 36 | } 37 | }, 38 | { 39 | "model": "misc.enrichment", 40 | "pk": "d06c1f89-46b4-4dd9-8c0e-8ca1782095a5", 41 | "fields": { 42 | "json": null, 43 | "name": "Cardboard tunnel", 44 | "description": "" 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /alyx/misc/fixtures/misc.food.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "misc.food", 4 | "pk": "6c509b8c-2ccf-4d3d-9d8d-0d2011599d69", 5 | "fields": { 6 | "json": null, 7 | "name": "Envigo 2919", 8 | "description": "" 9 | } 10 | }, 11 | { 12 | "model": "misc.food", 13 | "pk": "775b40e5-83a9-49d6-86ca-d4f7d25032af", 14 | "fields": { 15 | "json": null, 16 | "name": "Envigo 2914", 17 | "description": "" 18 | } 19 | }, 20 | { 21 | "model": "misc.food", 22 | "pk": "8b4cfefa-36dc-4d2d-9994-08d5c2a75008", 23 | "fields": { 24 | "json": null, 25 | "name": "Envigo 2018", 26 | "description": "" 27 | } 28 | }, 29 | { 30 | "model": "misc.food", 31 | "pk": "a3c381f7-9145-4ff8-b4dc-ccc02d7b752a", 32 | "fields": { 33 | "json": null, 34 | "name": "Picolab Rodent Diet 20 5053", 35 | "description": "" 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /alyx/misc/fixtures/misc.lab.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "misc.lab", 4 | "pk": "4027da48-7be3-43ec-a222-f75dffe36872", 5 | "fields": { 6 | "json": null, 7 | "name": "cortexlab", 8 | "institution": "University College London", 9 | "address": "Cruciform Building, Gower Street, London, WC1E 6BT, United Kingdom", 10 | "timezone": "Europe/London", 11 | "reference_weight_pct": 0.0, 12 | "zscore_weight_pct": 0.8, 13 | "cage_type": null, 14 | "enrichment": null, 15 | "food": null, 16 | "cage_cleaning_frequency_days": null, 17 | "light_cycle": null 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /alyx/misc/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/misc/management/__init__.py -------------------------------------------------------------------------------- /alyx/misc/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/misc/management/commands/__init__.py -------------------------------------------------------------------------------- /alyx/misc/management/commands/migrate_ucl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from django.core.management.base import BaseCommand 5 | from django.db.models import Count 6 | from data.models import FileRecord 7 | 8 | logger = logging.getLogger(__name__) 9 | logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)-15s %(message)s') 10 | 11 | 12 | def duplicates(): 13 | frs = FileRecord.objects.values('relative_path', 'data_repository') \ 14 | .order_by('relative_path').annotate(dcount=Count('data_repository')) \ 15 | .filter(dcount__gt=1) 16 | return frs 17 | 18 | 19 | class Command(BaseCommand): 20 | help = "One-off migration script for UCL" 21 | 22 | def handle(self, *args, **options): 23 | for fr in duplicates(): 24 | f = FileRecord.objects.filter( 25 | relative_path=fr['relative_path'], 26 | data_repository=fr['data_repository']) 27 | for _ in f[1:]: 28 | _.delete() 29 | print(duplicates().count()) 30 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/breeding_pairs.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_breedingpair.id, 2 | subjects_breedingpair.name, 3 | subjects_breedingpair.description, 4 | subjects_breedingpair.start_date, 5 | subjects_breedingpair.end_date, 6 | f.nickname AS father_name, 7 | m1.nickname AS mother1_name, 8 | m2.nickname AS mother2_name, 9 | subjects_line.name AS line, 10 | subjects_breedingpair.json 11 | FROM subjects_breedingpair 12 | LEFT JOIN subjects_line on subjects_breedingpair.line_id=subjects_line.id 13 | LEFT JOIN subjects_subject f on subjects_breedingpair.father_id=f.id 14 | LEFT JOIN subjects_subject m1 on subjects_breedingpair.mother1_id=m1.id 15 | LEFT JOIN subjects_subject m2 on subjects_breedingpair.mother2_id=m2.id 16 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/genotype_tests.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_subject.nickname AS subject_name, 2 | subjects_sequence.name AS sequence_name, 3 | subjects_genotypetest.test_result, 4 | subjects_genotypetest.json, 5 | subjects_genotypetest.id 6 | FROM subjects_genotypetest 7 | LEFT JOIN subjects_subject on subjects_subject.id=subjects_genotypetest.subject_id 8 | LEFT JOIN subjects_sequence on subjects_sequence.id=subjects_genotypetest.sequence_id 9 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/lines.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_line.id, 2 | subjects_line.nickname, 3 | subjects_line.name, 4 | subjects_line.description, 5 | subjects_line.target_phenotype, 6 | subjects_line.subject_autoname_index, 7 | subjects_line.breeding_pair_autoname_index, 8 | subjects_line.litter_autoname_index, 9 | subjects_species.nickname AS species, 10 | subjects_strain.name AS strain, 11 | subjects_line.json 12 | FROM subjects_line 13 | LEFT JOIN subjects_species on subjects_line.species_id=subjects_species.id 14 | LEFT JOIN subjects_strain on subjects_line.strain_id=subjects_strain.id 15 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/litters.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_litter.id, 2 | subjects_litter.name, 3 | subjects_litter.description, 4 | subjects_litter.birth_date, 5 | subjects_breedingpair.name AS breeding_pair, 6 | subjects_line.name AS line, 7 | subjects_litter.json 8 | FROM subjects_litter 9 | LEFT JOIN subjects_breedingpair on subjects_litter.breeding_pair_id=subjects_breedingpair.id 10 | LEFT JOIN subjects_line on subjects_litter.line_id=subjects_line.id 11 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/subject.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_subject.id, 2 | subjects_subject.nickname, 3 | subjects_subject.sex, 4 | subjects_subject.birth_date, 5 | subjects_subject.death_date, 6 | subjects_subject.implant_weight, 7 | subjects_subject.description, 8 | subjects_subject.ear_mark, 9 | subjects_breedingpair.name AS breeding_pair, 10 | subjects_line.name AS line, 11 | subjects_litter.name AS litter, 12 | misc_labmember.username AS responsible_user, 13 | subjects_source.name AS source, 14 | subjects_species.nickname AS species, 15 | subjects_strain.name AS strain, 16 | subjects_subject.wean_date, 17 | subjects_subject.actual_severity, 18 | subjects_subject.adverse_effects, 19 | subjects_subject.cull_method, 20 | subjects_subject.json, 21 | subjects_subject.request_id 22 | FROM subjects_subject 23 | LEFT JOIN subjects_line on subjects_subject.line_id=subjects_line.id 24 | LEFT JOIN misc_labmember on subjects_subject.responsible_user_id=misc_labmember.id 25 | LEFT JOIN subjects_litter on subjects_subject.litter_id=subjects_litter.id 26 | LEFT JOIN subjects_breedingpair on subjects_litter.breeding_pair_id=subjects_breedingpair.id 27 | LEFT JOIN subjects_source on subjects_subject.source_id=subjects_source.id 28 | LEFT JOIN subjects_species on subjects_subject.species_id=subjects_species.id 29 | LEFT JOIN subjects_strain on subjects_subject.strain_id=subjects_strain.id 30 | ORDER BY line, subjects_subject.birth_date DESC, subjects_subject.nickname DESC 31 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/queries/subjectrequest.sql: -------------------------------------------------------------------------------- 1 | SELECT subjects_subjectrequest.id, 2 | subjects_subjectrequest.json, 3 | subjects_subjectrequest.count, 4 | subjects_subjectrequest.date_time, 5 | subjects_subjectrequest.due_date, 6 | subjects_subjectrequest.description, 7 | subjects_line.name AS line_name, 8 | misc_labmember.username AS username 9 | FROM subjects_subjectrequest 10 | LEFT JOIN subjects_line on subjects_subjectrequest.line_id=subjects_line.id 11 | LEFT JOIN misc_labmember on subjects_subjectrequest.user_id=misc_labmember.id 12 | -------------------------------------------------------------------------------- /alyx/misc/management/commands/validate_subjects.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | from subjects.models import Subject 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Command(BaseCommand): 11 | help = ("Check and enforce the consistency of subject check boxes (reduced, etc.)") 12 | 13 | def add_arguments(self, parser): 14 | super(Command, self).add_arguments(parser) 15 | 16 | def handle(self, *args, **options): 17 | subjects = Subject.objects.filter( 18 | reduced_date__isnull=False, reduced=False).order_by('nickname') 19 | n = len(subjects) 20 | print("Check 'reduced' of %s subjects." % n) 21 | for s in subjects: 22 | print(" ", s) 23 | subjects.update(reduced=True) 24 | print() 25 | 26 | subjects = Subject.objects.filter( 27 | cull__isnull=False, to_be_culled=True).order_by('nickname') 28 | n = len(subjects) 29 | print("Check 'to_be_culled' of %s subjects." % n) 30 | for s in subjects: 31 | print(" ", s) 32 | subjects.update(to_be_culled=False) 33 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0002_auto_20181119_0930.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-11-19 09:30 2 | 3 | from django.db import migrations, models 4 | from django.utils import timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('misc', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='lab', 16 | name='reference_weight_pct', 17 | field=models.FloatField(default=0.0, help_text='The minimum mouse weight is a linear combination of the reference weight and the zscore weight.'), 18 | ), 19 | migrations.AddField( 20 | model_name='lab', 21 | name='zscore_weight_pct', 22 | field=models.FloatField(default=0.0, help_text='The minimum mouse weight is a linear combination of the reference weight and the zscore weight.'), 23 | ), 24 | migrations.AlterField( 25 | model_name='labmembership', 26 | name='start_date', 27 | field=models.DateField(blank=True, null=True, default=timezone.now), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0003_auto_20190124_1025.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-01-24 10:25 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import misc.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('misc', '0002_auto_20181119_0930'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='lablocation', 17 | name='lab', 18 | field=models.ForeignKey(default=misc.models.default_lab, on_delete=django.db.models.deletion.CASCADE, to='misc.Lab'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0005_lab_repositories.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2019-04-09 11:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('data', '0003_auto_20190315_1716'), 10 | ('misc', '0004_auto_20190309_1750'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='lab', 16 | name='repositories', 17 | field=models.ManyToManyField(blank=True, help_text='Related DataRepository instances. Any file which is registered to Alyx is automatically copied to all repositories assigned to its project.', to='data.DataRepository'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0006_labmember_allowed_users.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2019-12-24 10:35 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('misc', '0005_lab_repositories'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='labmember', 16 | name='allowed_users', 17 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0007_auto_20200120_1223.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2020-01-20 12:23 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('misc', '0006_labmember_allowed_users'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='labmember', 16 | name='allowed_users', 17 | field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0009_auto_20211122_1535.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-11-22 15:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('misc', '0008_auto_20210624_1253'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='note', 15 | name='object_id', 16 | field=models.UUIDField(help_text='UUID, an object of content_type with this ID must already exist to attach a note.'), 17 | ), 18 | migrations.AlterField( 19 | model_name='note', 20 | name='text', 21 | field=models.TextField(blank=True, help_text='String, content of the note or description of the image.'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0010_alter_lab_timezone.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 15:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("misc", "0009_auto_20211122_1535"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="lab", 15 | name="timezone", 16 | field=models.CharField( 17 | blank=True, 18 | default="Europe/London", 19 | help_text="Timezone of the server (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)", 20 | max_length=64, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /alyx/misc/migrations/0011_alter_lab_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2024-11-14 11:01 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('misc', '0010_alter_lab_timezone'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='lab', 16 | name='name', 17 | field=models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^\\w+$', 'Lab name must only contain letters, numbers, and underscores.')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/misc/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/misc/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/misc/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | from django.views.generic.base import RedirectView 3 | from misc import views as mv 4 | from django.conf.urls import include 5 | 6 | 7 | urlpatterns = [ 8 | path('', RedirectView.as_view(url='/admin')), # redirect the page to admin interface 9 | path('labs', mv.LabList.as_view(), name="lab-list"), 10 | path('labs/', mv.LabDetail.as_view(), name="lab-detail"), 11 | path('notes', mv.NoteList.as_view(), name="note-list"), 12 | path('notes/', mv.NoteDetail.as_view(), name="note-detail"), 13 | path('users', mv.UserList.as_view(), name="user-list"), 14 | path('users/', mv.UserDetail.as_view(), name="user-detail"), 15 | re_path('^uploaded/(?P.*)', mv.UploadedView.as_view(), name='uploaded'), 16 | path('cache.zip', mv.CacheDownloadView.as_view(), name='cache-download'), 17 | re_path(r'^cache/info(?:/(?P\w+))?/$', mv.CacheVersionView.as_view(), name='cache-info'), 18 | ] 19 | 20 | try: 21 | # If ibl-reports redirect home to reports page 22 | urlpatterns += [path('ibl_reports/', include('ibl_reports.urls')), ] 23 | # urlpatterns += [path('', RedirectView.as_view(url='/ibl_reports/overview')), ] 24 | except ModuleNotFoundError: 25 | pass 26 | # redirect the page to admin interface 27 | # urlpatterns += [path('', RedirectView.as_view(url='/admin')), ] 28 | -------------------------------------------------------------------------------- /alyx/rm_all_db_migrations.sh: -------------------------------------------------------------------------------- 1 | rm -r ./*/migrations/0* 2 | -------------------------------------------------------------------------------- /alyx/static/ref_weighings_female.csv: -------------------------------------------------------------------------------- 1 | 4,14.10,1.80 5,16.90,1.20 6,17.50,1.00 7,18.20,1.10 8,18.70,1.20 9,19.30,1.20 10,19.80,1.30 11,20.20,1.40 12,20.70,1.40 13,21.70,1.50 14,22.00,1.60 15,22.30,1.70 16,22.60,1.80 17,22.60,2.00 18,23.00,2.10 19,23.50,2.30 20,23.60,2.30 21,23.60,2.30 22,23.60,2.30 23,23.60,2.30 24,23.60,2.30 25,23.60,2.30 26,23.60,2.30 27,23.60,2.30 28,23.60,2.30 29,23.60,2.30 30,23.60,2.30 31,23.60,2.30 32,23.60,2.30 33,23.60,2.30 34,23.60,2.30 35,23.60,2.30 36,23.60,2.30 37,23.60,2.30 38,23.60,2.30 39,23.60,2.30 40,23.60,2.30 41,23.60,2.30 42,23.60,2.30 43,23.60,2.30 44,23.60,2.30 45,23.60,2.30 46,23.60,2.30 47,23.60,2.30 48,23.60,2.30 49,23.60,2.30 50,23.60,2.30 51,23.60,2.30 52,23.60,2.30 53,23.60,2.30 54,23.60,2.30 55,23.60,2.30 56,23.60,2.30 57,23.60,2.30 58,23.60,2.30 59,23.60,2.30 60,23.60,2.30 61,23.60,2.30 62,23.60,2.30 63,23.60,2.30 64,23.60,2.30 65,23.60,2.30 66,23.60,2.30 67,23.60,2.30 68,23.60,2.30 69,23.60,2.30 70,23.60,2.30 71,23.60,2.30 72,23.60,2.30 73,23.60,2.30 74,23.60,2.30 75,23.60,2.30 76,23.60,2.30 77,23.60,2.30 78,23.60,2.30 79,23.60,2.30 80,23.60,2.30 81,23.60,2.30 82,23.60,2.30 83,23.60,2.30 84,23.60,2.30 85,23.60,2.30 86,23.60,2.30 87,23.60,2.30 88,23.60,2.30 89,23.60,2.30 90,23.60,2.30 91,23.60,2.30 92,23.60,2.30 93,23.60,2.30 94,23.60,2.30 95,23.60,2.30 96,23.60,2.30 -------------------------------------------------------------------------------- /alyx/static/ref_weighings_male.csv: -------------------------------------------------------------------------------- 1 | 4,15.70,2.20 5,19.40,1.80 6,21.10,1.50 7,22.90,1.50 8,24.00,1.50 9,25.00,1.60 10,25.60,1.70 11,26.70,1.70 12,27.70,1.70 13,28.40,1.90 14,29.10,1.90 15,29.70,2.20 16,30.10,2.10 17,30.70,2.20 18,31.10,2.30 19,31.40,2.40 20,31.80,2.50 21,31.80,2.50 22,31.80,2.50 23,31.80,2.50 24,31.80,2.50 25,31.80,2.50 26,31.80,2.50 27,31.80,2.50 28,31.80,2.50 29,31.80,2.50 30,31.80,2.50 31,31.80,2.50 32,31.80,2.50 33,31.80,2.50 34,31.80,2.50 35,31.80,2.50 36,31.80,2.50 37,31.80,2.50 38,31.80,2.50 39,31.80,2.50 40,31.80,2.50 41,31.80,2.50 42,31.80,2.50 43,31.80,2.50 44,31.80,2.50 45,31.80,2.50 46,31.80,2.50 47,31.80,2.50 48,31.80,2.50 49,31.80,2.50 50,31.80,2.50 51,31.80,2.50 52,31.80,2.50 53,31.80,2.50 54,31.80,2.50 55,31.80,2.50 56,31.80,2.50 57,31.80,2.50 58,31.80,2.50 59,31.80,2.50 60,31.80,2.50 61,31.80,2.50 62,31.80,2.50 63,31.80,2.50 64,31.80,2.50 65,31.80,2.50 66,31.80,2.50 67,31.80,2.50 68,31.80,2.50 69,31.80,2.50 70,31.80,2.50 71,31.80,2.50 72,31.80,2.50 73,31.80,2.50 74,31.80,2.50 75,31.80,2.50 76,31.80,2.50 77,31.80,2.50 78,31.80,2.50 79,31.80,2.50 80,31.80,2.50 81,31.80,2.50 82,31.80,2.50 83,31.80,2.50 84,31.80,2.50 85,31.80,2.50 86,31.80,2.50 87,31.80,2.50 88,31.80,2.50 89,31.80,2.50 90,31.80,2.50 91,31.80,2.50 92,31.80,2.50 93,31.80,2.50 94,31.80,2.50 95,31.80,2.50 96,31.80,2.50 97,31.80,2.50 98,31.80,2.50 99,31.80,2.50 100,31.80,2.50 101,31.80,2.50 102,31.80,2.50 103,31.80,2.50 104,31.80,2.50 105,31.80,2.50 106,31.80,2.50 107,31.80,2.50 108,31.80,2.50 109,31.80,2.50 110,31.80,2.50 111,31.80,2.50 112,31.80,2.50 113,31.80,2.50 114,31.80,2.50 115,31.80,2.50 116,31.80,2.50 117,31.80,2.50 118,31.80,2.50 119,31.80,2.50 120,31.80,2.50 121,31.80,2.50 122,31.80,2.50 123,31.80,2.50 124,31.80,2.50 125,31.80,2.50 126,31.80,2.50 127,31.80,2.50 128,31.80,2.50 129,31.80,2.50 130,31.80,2.50 131,31.80,2.50 132,31.80,2.50 133,31.80,2.50 134,31.80,2.50 135,31.80,2.50 -------------------------------------------------------------------------------- /alyx/subjects/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'subjects.apps.SubjectsConfig' 2 | -------------------------------------------------------------------------------- /alyx/subjects/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SubjectsConfig(AppConfig): 5 | name = 'subjects' 6 | verbose_name = 'Subject admin' 7 | -------------------------------------------------------------------------------- /alyx/subjects/fixtures/subjects.source.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "subjects.source", 4 | "pk": "1bad1f22-6700-4f80-9bcc-64e7d8ebc497", 5 | "fields": { 6 | "name": "Allen Institute", 7 | "json": null, 8 | "description": "" 9 | } 10 | }, 11 | { 12 | "model": "subjects.source", 13 | "pk": "66ac5ce0-c988-429d-885c-8f63ce4329de", 14 | "fields": { 15 | "name": "Cruciform BSU", 16 | "json": null, 17 | "description": "Subjects born in-house." 18 | } 19 | }, 20 | { 21 | "model": "subjects.source", 22 | "pk": "a203214f-a34a-4b65-ba19-5ce85e07a005", 23 | "fields": { 24 | "name": "Charles River", 25 | "json": null, 26 | "description": "" 27 | } 28 | }, 29 | { 30 | "model": "subjects.source", 31 | "pk": "c2b2242e-fa25-432d-96ec-122208e8b66c", 32 | "fields": { 33 | "name": "Jax", 34 | "json": null, 35 | "description": "Jackson labs" 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /alyx/subjects/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/management/__init__.py -------------------------------------------------------------------------------- /alyx/subjects/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/management/commands/__init__.py -------------------------------------------------------------------------------- /alyx/subjects/migrations/0002_auto_20181015_1026.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-15 10:26 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='zygosityrule', 15 | unique_together={('line', 'allele', 'sequence0', 'sequence0_result', 'sequence1', 'sequence1_result')}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0003_auto_20190124_1025.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-01-24 10:25 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import misc.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('misc', '0003_auto_20190124_1025'), 12 | ('subjects', '0002_auto_20181015_1026'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='line', 18 | name='lab', 19 | field=models.ForeignKey(default=misc.models.default_lab, on_delete=django.db.models.deletion.CASCADE, to='misc.Lab'), 20 | ), 21 | migrations.AlterField( 22 | model_name='subject', 23 | name='lab', 24 | field=models.ForeignKey(default=misc.models.default_lab, on_delete=django.db.models.deletion.CASCADE, to='misc.Lab'), 25 | ), 26 | migrations.AlterUniqueTogether( 27 | name='line', 28 | unique_together={('nickname', 'lab')}, 29 | ), 30 | migrations.AlterUniqueTogether( 31 | name='litter', 32 | unique_together={('name',)}, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0004_remove_project_repositories.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2019-04-09 11:02 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0003_auto_20190124_1025'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='project', 15 | name='repositories', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0005_auto_20191224_1035.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2019-12-24 10:35 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0004_remove_project_repositories'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='Adverse effect', 15 | ), 16 | migrations.DeleteModel( 17 | name='Cull subject', 18 | ), 19 | migrations.CreateModel( 20 | name='Adverse_effect', 21 | fields=[ 22 | ], 23 | options={ 24 | 'proxy': True, 25 | 'indexes': [], 26 | 'constraints': [], 27 | }, 28 | bases=('subjects.subject',), 29 | ), 30 | migrations.CreateModel( 31 | name='Cull_subject', 32 | fields=[ 33 | ], 34 | options={ 35 | 'proxy': True, 36 | 'indexes': [], 37 | 'constraints': [], 38 | }, 39 | bases=('subjects.subject',), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0006_auto_20200317_1055.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2020-03-17 10:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0005_auto_20191224_1035'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='subject', 15 | name='projects', 16 | field=models.ManyToManyField(blank=True, help_text='Project associated to this session', to='subjects.Project', verbose_name='Subject Projects'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0007_auto_20200921_1346.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2020-09-21 13:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0006_auto_20200317_1055'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='subject', 15 | name='implant_weight', 16 | field=models.FloatField(help_text='Implant weight in grams', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0008_auto_20201015_1320.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2020-10-15 13:20 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('subjects', '0007_auto_20200921_1346'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='subject', 16 | name='nickname', 17 | field=models.CharField(default='-', help_text="Easy-to-remember name (e.g. 'Hercules').", max_length=64, validators=[django.core.validators.RegexValidator('^[-._~\\+\\*\\w]+$', 'Nicknames must only contain letters, numbers, or any of -._~.')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0009_auto_20201103_1434.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-11-03 14:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0008_auto_20201015_1320'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='subject', 15 | name='implant_weight', 16 | field=models.FloatField(blank=True, help_text='Implant weight in grams', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0011_alter_subject_nickname.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-05-11 16:24 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('subjects', '0010_auto_20210624_1253'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='subject', 16 | name='nickname', 17 | field=models.CharField(default='-', help_text="Easy-to-remember name (e.g. 'Hercules').", max_length=64, validators=[django.core.validators.RegexValidator('^[\\w-]+$', 'Nicknames must only contain letters, numbers, hyphens and underscores.')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0012_alter_subject_nickname.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-06-16 11:38 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('subjects', '0011_alter_subject_nickname'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='subject', 16 | name='nickname', 17 | field=models.CharField(default='-', help_text="Easy-to-remember name (e.g. 'Hercules').", max_length=64, validators=[django.core.validators.RegexValidator('^[\\w.-]+$', 'Nicknames must only contain letters, numbers, hyphens and underscores. Dots are reserved for breeding subjects.')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0013_remove_subject_implant_weight.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-15 13:58 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0012_alter_subject_nickname'), 10 | ('actions', '0024_surgery_implant_weight'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='subject', 16 | name='implant_weight', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/0014_delete_adverse_effect_delete_cull_subject_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2025-02-27 16:52 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('subjects', '0013_remove_subject_implant_weight'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='Adverse_effect', 15 | ), 16 | migrations.DeleteModel( 17 | name='Cull_subject', 18 | ), 19 | migrations.CreateModel( 20 | name='AdverseEffect', 21 | fields=[ 22 | ], 23 | options={ 24 | 'proxy': True, 25 | 'indexes': [], 26 | 'constraints': [], 27 | }, 28 | bases=('subjects.subject',), 29 | ), 30 | migrations.CreateModel( 31 | name='CullSubject', 32 | fields=[ 33 | ], 34 | options={ 35 | 'proxy': True, 36 | 'indexes': [], 37 | 'constraints': [], 38 | }, 39 | bases=('subjects.subject',), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /alyx/subjects/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/migrations/__init__.py -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/css/application.css: -------------------------------------------------------------------------------- 1 | /* global body padding */ 2 | body { 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | } 6 | 7 | @media (min-width: 768px) { 8 | body { 9 | padding-top: 50px; 10 | padding-bottom: 50px; 11 | } 12 | } 13 | 14 | 15 | /* global spacing overrides */ 16 | h1, h2, h3, h4, h5, h6, 17 | .h1, .h2, .h3, .h4, .h5, .h6 { 18 | margin-top: 0; 19 | } 20 | hr { 21 | margin-top: 30px; 22 | margin-bottom: 30px; 23 | } 24 | 25 | .navbar-fixed-top, 26 | .navbar-static-top { 27 | border-bottom: 0; 28 | } 29 | -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.eot -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.ttf -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.woff -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/static/subjects/assets/fonts/toolkit-entypo.woff2 -------------------------------------------------------------------------------- /alyx/subjects/static/subjects/assets/img/avatar-mdo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/alyx/subjects/static/subjects/assets/img/avatar-mdo.png -------------------------------------------------------------------------------- /alyx/subjects/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | import subjects.views as sv 3 | 4 | urlpatterns = [ 5 | path('projects', sv.ProjectList.as_view(), 6 | name="project-list"), 7 | 8 | path('projects/', sv.ProjectDetail.as_view(), 9 | name="project-detail"), 10 | 11 | path('water-restricted-subjects', sv.WaterRestrictedSubjectList.as_view(), 12 | name="water-restricted-subject-list"), 13 | 14 | path('subjects', sv.SubjectList.as_view(), 15 | name="subject-list"), 16 | 17 | path('subjects/', sv.SubjectDetail.as_view(), 18 | name="subject-detail"), 19 | ] 20 | -------------------------------------------------------------------------------- /alyx/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | {% block userlinks %} 5 | go to 6 | subjects / 7 | actions / 8 | data / 9 | misc / 10 | experiments / 11 | jobs / 12 | tasks / 13 | rest documentation / 14 | 15 | {{ block.super }} / 16 | {% trans 'Report a problem' %} 17 | {% endblock %} 18 | 19 | {% block extrastyle %} 20 | 21 | {% if "localhost" in request.META.HTTP_HOST %} 22 | 30 | {% endif %} 31 | 32 | {% if "dev" in request.META.HTTP_HOST %} 33 | 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block extrahead %} 45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /alyx/templates/admin/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | 5 | {% block extrastyle %} 6 | {{ block.super }} 7 | 18 | {% endblock %} 19 | 20 | 21 | {% block footer %} 22 | {{ block.super }} 23 | 35 | {% endblock %} 36 | 37 | 38 | {% block breadcrumbs %} 39 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /alyx/templates/admin/search_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls static admin_list %} 2 | {% if cl.search_fields %} 3 |
16 | {% endif %} 17 | -------------------------------------------------------------------------------- /alyx/templates/error_500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 500 Internal Server Error 7 | 62 | 63 | 64 |
65 |

500 Internal Server Error

66 |

Oupsy, something went wrong on our end. !

67 |

Feel free to reach out with the information below, we'll do our best to resolve the issue.

68 |
69 | {{ request|safe }} 70 |
71 |
72 | {{ traceback|safe }} 73 |
74 |
75 | 76 | -------------------------------------------------------------------------------- /alyx/templates/includes/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /alyx/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html"%} 2 | {% block main_container %} 3 | 4 | {% if object_list %} 5 |
6 |
7 |
Alyx
8 |

{% block title %}Home{% endblock %}

9 |
10 |
11 |

Soon this will show a dashboard of all your stuff. But it's not yet implemented...

12 | {% if is_paginated %} 13 | 26 | {% endif %} 27 | {% else %} 28 |
29 |
30 |

Whoops!

31 |

No subjects yet!

32 |
33 | {% endif %} 34 | {% endblock %} -------------------------------------------------------------------------------- /alyx/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | 3 | {% block branding %} 4 | 5 | Alyx REST API Browser 6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /alyx/templates/subject_history.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for obj in object_list %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 |
datenametype
{{ obj.date_time }}{{ obj.name }}{{ obj.type }}{{ obj.arg0 }}{{ obj.arg1 }}{{ obj.arg2 }}{{ obj.arg3 }}
33 | 34 | {% endblock %} 35 | 36 | {% block title %} 37 | {{ title|striptags }} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /alyx/templates/subjects_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block main_container %} 3 | {% if object_list %} 4 | 5 |
6 |
7 |
Alyx
8 |

{% block title %}Subjects{% endblock %}

9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for object in object_list %} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 |
NicknameResponsible UserStrainGenotypeAge
{{ object.nickname }}{{ object.responsible_user }}{{ object.strain }}{{ object.genotype }}{{ object.age_days }} days
45 |
46 |
47 | {% if is_paginated %} 48 | 61 | {% endif %} 62 | {% else %} 63 |
64 |
65 |

Whoops!

66 |

No subjects yet!

67 |
68 | {% endif %} 69 | {% endblock %} -------------------------------------------------------------------------------- /alyx/templates/training.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | 5 | {% block content %} 6 | 7 | 12 | 13 |
14 | < Previous week 15 | Today 16 | Next week > 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for wd in wds %} 27 | 28 | {% endfor %} 29 | 30 | 31 | 32 | {% for obj in object_list %} 33 | 34 | 35 | 36 | 37 | {% for trained in obj.training_days %} 38 | 39 | {% endfor %} 40 | 41 | {% endfor %} 42 | 43 |
subjectusertraining days{{ wd | date:"D d M, Y" }}
{{ obj.nickname }}{{ obj.username }}{{ obj.n_training_days }}
44 | 45 | {% endblock %} 46 | 47 | {% block title %} 48 | {{ title|striptags }} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /alyx/templates/water_history.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | 5 | {% block content %} 6 | 7 |
8 | 9 |
10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% for obj in object_list %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {% endfor %} 55 | 56 |
dateweightweight refweight expweight pctweight minwater rewardwater suppl.water totwater expwater excessimplant weight
{{ obj.date | date:"D d M, Y" }}{{ obj.weighing_at | floatformat:1 }} {{ obj.weighing_at | yesno:'g,,' }}{{ obj.reference_weight | floatformat:1 }} g{{ obj.expected_weight | floatformat:1 }} g{{ obj.percentage_weight | floatformat:1 }}%{{ obj.min_weight | floatformat:1 }} g{{ obj.given_water_reward | floatformat:2 }} mL{{ obj.given_water_supplement | floatformat:2 }} mL{{ obj.given_water_total | floatformat:2 }} mL{{ obj.expected_water | floatformat:2 }} mL{{ obj.excess_water | floatformat:2 }} mL{% if obj.implant_weight %} {{ obj.implant_weight | floatformat:1 }} {% else %} 0.0 {% endif %} g
57 | 58 | {% endblock %} 59 | 60 | {% block title %} 61 | {{ title|striptags }} 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | This directory should contain: 2 | 3 | * `dumped_static.json` obtained with: 4 | 5 | ``` 6 | python3 /data/www/alyx/alyx/manage.py dumpdata auth.user auth.group subjects.species subjects.strain subjects.allele subjects.line subjects.linegenotypetest subjects.source subjects.sequence equipment.lablocation actions.proceduretype --indent 2 --natural-foreign -e sessions -e admin > dumped_static.json 7 | ``` 8 | 9 | Note: at the moment, you have to manually delete the permissions of Experiment or Charu in the json before importing, otherwise you'll enter into some bug. These users are set as superusers manually. 10 | 11 | * `gdrive.json` obtained as [explained here](http://gspread.readthedocs.io/en/latest/oauth2.html). 12 | 13 | Then, to import the data: 14 | 15 | * `make reset_all`: this will **completely erase the alyx database** and recreate it from the dumped data in the json file, and from the google sheets. 16 | -------------------------------------------------------------------------------- /data/all_dumped_anon.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/data/all_dumped_anon.json.gz -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation contribution guide 2 | 3 | # Dependencies 4 | ``` 5 | pip install myst-parser 6 | pip install sphinx_rtd_theme 7 | ``` 8 | 9 | # Build documentation locally 10 | From the root of the repository. 11 | `sphinx-build -b html ./docs ./docs/_build/` 12 | -------------------------------------------------------------------------------- /docs/_static/001-alyx.conf: -------------------------------------------------------------------------------- 1 | 2 | # The ServerName directive sets the request scheme, hostname and port that 3 | # the server uses to identify itself. This is used when creating 4 | # redirection URLs. In the context of virtual hosts, the ServerName 5 | # specifies what hostname must appear in the request's Host: header to 6 | # match this virtual host. For the default virtual host (this file) this 7 | # value is not decisive as it is used as a last resort host regardless. 8 | # However, you must set it for any further virtual host explicitly. 9 | 10 | LogLevel info 11 | 12 | ServerName alyx.internationalbrainlab.org 13 | ServerAdmin webmaster@internationalbrainlab.org 14 | 15 | 16 | 17 | Require all granted 18 | 19 | 20 | 21 | 22 | Alias /static/ /var/www/alyx/alyx/static/ 23 | Alias /media/ /var/www/alyx/alyx/media/ 24 | 25 | 26 | Require all granted 27 | 28 | 29 | 30 | Require all granted 31 | 32 | 33 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, 34 | # error, crit, alert, emerg. 35 | # It is also possible to configure the loglevel for particular 36 | # modules, e.g. 37 | #LogLevel info ssl:warn 38 | 39 | ErrorLog ${APACHE_LOG_DIR}/error_alyx.log 40 | CustomLog ${APACHE_LOG_DIR}/access_alyx.log combined 41 | 42 | # Memory limit set to ~3GiB. For more information on WSGI flags, see 43 | # https://modwsgi.readthedocs.io/en/master/configuration-directives/WSGIDaemonProcess.html 44 | 45 | WSGIDaemonProcess alyx python-path=/var/www/alyx/alyx python-home=/var/www/alyx/alyxvenv listen-backlog=1000 cpu-time-limit=60 memory-limit=3221225000 46 | WSGIProcessGroup alyx 47 | WSGIScriptAlias / /var/www/alyx/alyx/alyx/wsgi.py 48 | WSGIPassAuthorization On 49 | 50 | # For most configuration files from conf-available/, which are 51 | # enabled or disabled at a global level, it is possible to 52 | # include a line for only one particular virtual host. For example the 53 | # following line enables the CGI configuration for this host only 54 | # after it has been globally disabled with "a2disconf". 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/_static/Full_Alyx_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/docs/_static/Full_Alyx_ERD.png -------------------------------------------------------------------------------- /docs/_static/actions_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/docs/_static/actions_ERD.png -------------------------------------------------------------------------------- /docs/_static/data_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/docs/_static/data_ERD.png -------------------------------------------------------------------------------- /docs/_static/misc_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/docs/_static/misc_ERD.png -------------------------------------------------------------------------------- /docs/_static/subjects_ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cortex-lab/alyx/8bd3e2bb05bacaa76b1cde376a9c1bc68a722fc8/docs/_static/subjects_ERD.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | REST API 2 | ======================== 3 | 4 | Overview 5 | ------------------------ 6 | 7 | The **base URL** is the URL of your Alyx installation, for example at IBL: https://openalyx.internationalbrainlab.org 8 | 9 | With REST_, you make an HTTP request to a particular URL, named an **endpoint**, with possibly some parameters, and you obtain a response in JSON_. There are several types of HTTP requests used in Alyx: 10 | 11 | GET 12 | obtain read-only data about an object, or perform a query 13 | 14 | POST 15 | create a new object 16 | 17 | PATCH 18 | update some fields of an object 19 | 20 | PUT 21 | update all fields of an object 22 | 23 | Some GET endpoints return a list of objects satisfying to your query, while other endpoints return the detail of a single object. 24 | 25 | The `/` endpoint (base URL) returns the list of all available endpoints. 26 | 27 | Every object is identified with a unique 128-bit UUID_, representing by a string of 32 hexadecimal digits, like e.g. `6915b95b-c6d4-45a6-80c3-324675723d3e`. 28 | 29 | When we say to do a GET request to the `/blah/` endpoint, we mean to perform an HTTP GET request to the url `https://yourbaseurl/blah/`. 30 | 31 | With POST, PATCH, and PUT requests, data is passed as key-value pairs in JSON_. For example, doing a POST request to the `/blah/` endpoint with `key1=val1` and `key2=val2` means performing an HTTP POST request to the url `https://yourbaseurl/blah/` with data a string with the `application/json` mime type, and the contents `{"key1": "val1", "key2": "val2"}`. 32 | 33 | .. _REST: https://en.wikipedia.org/wiki/Representational_state_transfer 34 | .. _JSON: https://en.wikipedia.org/wiki/JSON 35 | .. _UUID: https://en.wikipedia.org/wiki/Universally_unique_identifier 36 | 37 | In practice, a library in your language should provide you with primitives such as `get(url)`, `post(url, key1=val1, ...)`, etc. so you don't need to understand all of the underlying details: just the HTTP request type, the endpoint URL, the fields you need to pass, and the fields that are returned by the endpoint. 38 | 39 | Going further 40 | ------------------------ 41 | The list of endpoints, fields and methods is self-documented for each database at the `/docs` URL. 42 | For example, a public Alyx instance is available here for reference: https://openalyx.internationalbrainlab.org/docs/ 43 | 44 | REST endpoints are programmatically accessed client side. And example of a client side application is described in details here: [https://one.internationalbrainlab.org/](https://int-brain-lab.github.io/ONE/). 45 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Alyx 2 | ================================= 3 | 4 | Alyx is a database designed for storage and retrieval of all data in an experimental neuroscience laboratory - from subject management through data acquisition, raw data file tracking and storage of metadata resulting from manual analysis. 5 | 6 | Alyx is currently used in production at the `Cortexlab at UCL `_ and at the `International Brain Lab `_. 7 | 8 | Alyx is built using industry-standard tools (PostgreSQL_ and Django_) and designed to be interacted with using online webforms to enter and retrieve data manually, or with a documented REST API programmatically (using built-in functions in MATLAB_, Python_, and most other modern programming languages, or command-line tools such as curl_). 9 | 10 | It requires minimal setup and can be hosted on your own internal server or in the cloud, for example with Amazon EC2. 11 | 12 | .. _PostgreSQL: https://www.postgresql.org/ 13 | .. _Django: https://www.djangoproject.com/ 14 | .. _Python: https://www.python.org/ 15 | .. _MATLAB: http://uk.mathworks.com/products/matlab/ 16 | .. _curl: https://curl.haxx.se/ 17 | 18 | Table of contents: 19 | ================== 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | gettingstarted.md 25 | webserving.md 26 | motivation.rst 27 | considerations.rst 28 | api.rst 29 | models.rst 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | List of models 2 | ================== 3 | These models generally correspond one-to-one with tables in the database. 4 | 5 | Subjects 6 | ------------------- 7 | .. image:: _static/subjects_ERD.png 8 | .. automodule:: subjects.models 9 | :member-order: bysource 10 | :show-inheritance: 11 | :members: 12 | 13 | Actions 14 | ------------------- 15 | .. image:: _static/actions_ERD.png 16 | .. automodule:: actions.models 17 | :member-order: bysource 18 | :show-inheritance: 19 | :members: 20 | 21 | Data 22 | ------------------- 23 | .. image:: _static/data_ERD.png 24 | .. automodule:: data.models 25 | :member-order: bysource 26 | :show-inheritance: 27 | :members: 28 | 29 | Misc 30 | --------------------- 31 | .. image:: _static/misc_ERD.png 32 | .. automodule:: misc.models 33 | :member-order: bysource 34 | :show-inheritance: 35 | :members: 36 | -------------------------------------------------------------------------------- /docs/motivation.rst: -------------------------------------------------------------------------------- 1 | Motivation 2 | ===================================== 3 | 4 | Previous standardisation efforts, as well as current data organisation methods used in laboratories, have several drawbacks. Most recently, the `Neurodata Without Borders`_ project has worked to standardise a format for all experimental neurophysiology data. Alyx builds on this work and adds a number of key advantages: 5 | 6 | .. _Neurodata Without Borders: https://neurodatawithoutborders.github.io/ 7 | 8 | - **Searchability**: To use data, one has to first find the data. Alyx allows a user to quickly and simply search a database of neurophysiology experiments to find that needed for their scientific question. The search could run over all data collected in the user’s own lab, or all the shared data in the world. 9 | 10 | - **Lightweight organization**: A barrier to use of the current NWB format is its monolithic nature: in order use a dataset, a user must download the entire file, even if they only need a small part of it. The large size of these files presents a serious barrier to many users. 11 | 12 | - **Ease of use**: The HDF5 format at the base of the current NWB format is an obstacle to its adoption by the neurophysiology community; it will be replaced by simple binary files. 13 | 14 | - **Cloud-ready**: As more scientists move to cloud-based computing platforms, it is essential that large data files by quickly readable on these systems. HDF5 presents problems in this regard, that are solved by simple binary files. 15 | 16 | - **Encouraging uptake**: Working scientists will only switch from their current file formats if there is a strong incentive to do so. The proposed format comes with two “killer apps” that will encourage widespread adoption: a REST API to facilitate those building scientific tools in integrating support for Alyx into their applications, and set of online webforms to ensure all manually-entered metadata is provided by experimenters. 17 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | myst-parser 2 | sphinx_rtd_theme -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | colorlog 3 | coreapi 4 | django>=4.0.6 5 | django-admin-list-filter-dropdown 6 | django-admin-rangefilter 7 | django-autocomplete-light 8 | django-cleanup 9 | django-filter>=23.5 10 | django-mptt 11 | django-polymorphic>=3.0.0 12 | django-reversion>=3.0.8 13 | django-storages 14 | django-structlog 15 | django-test-without-migrations 16 | djangorestframework 17 | docutils 18 | drfdocs 19 | flake8 20 | globus-cli 21 | globus-sdk 22 | markdown 23 | matplotlib 24 | pillow 25 | psycopg2-binary 26 | python-dateutil 27 | python-magic 28 | pytz 29 | setuptools 30 | structlog>=21.5.0 31 | webdavclient3 32 | ONE-api>=3.0 33 | -------------------------------------------------------------------------------- /requirements_frozen.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | boto3==1.38.24 3 | botocore==1.38.24 4 | certifi==2025.4.26 5 | cffi==1.17.1 6 | charset-normalizer==3.4.2 7 | click==8.2.1 8 | colorlog==6.9.0 9 | contourpy==1.3.2 10 | coreapi==2.3.3 11 | coreschema==0.0.4 12 | coverage==7.8.2 13 | coveralls==4.0.1 14 | cryptography==45.0.3 15 | cycler==0.12.1 16 | Django==5.2.1 17 | django-admin-list-filter-dropdown==1.0.3 18 | django-admin-rangefilter==0.13.2 19 | django-autocomplete-light==3.12.1 20 | django-cleanup==9.0.0 21 | django-filter==25.1 22 | django-ipware==7.0.1 23 | django-js-asset==3.1.2 24 | django-mptt==0.17.0 25 | django-polymorphic==4.1.0 26 | django-reversion==5.1.0 27 | django-storages==1.14.6 28 | django-structlog==9.1.1 29 | django-test-without-migrations==0.6 30 | djangorestframework==3.16.0 31 | docopt==0.6.2 32 | docutils==0.21.2 33 | drfdocs==0.0.11 34 | flake8==7.2.0 35 | fonttools==4.58.0 36 | globus-cli==3.35.0 37 | globus-sdk==3.56.0 38 | iblutil==1.17.0 39 | idna==3.10 40 | itypes==1.2.0 41 | Jinja2==3.1.6 42 | jmespath==1.0.1 43 | kiwisolver==1.4.8 44 | llvmlite==0.44.0 45 | lxml==5.4.0 46 | Markdown==3.8 47 | MarkupSafe==3.0.2 48 | matplotlib==3.10.3 49 | mccabe==0.7.0 50 | numba==0.61.2 51 | numpy==2.2.6 52 | ONE-api==3.1.1 53 | packaging==25.0 54 | pandas==2.2.3 55 | pillow==11.2.1 56 | psycopg2-binary==2.9.10 57 | pyarrow==20.0.0 58 | pycodestyle==2.13.0 59 | pycparser==2.22 60 | pyflakes==3.3.2 61 | PyJWT==2.10.1 62 | pyparsing==3.2.3 63 | python-dateutil==2.9.0.post0 64 | python-ipware==3.0.0 65 | python-magic==0.4.27 66 | pytz==2025.2 67 | PyYAML==6.0.2 68 | requests==2.32.3 69 | ruff==0.11.11 70 | s3transfer==0.13.0 71 | setuptools==80.9.0 72 | six==1.17.0 73 | sqlparse==0.5.3 74 | structlog==25.3.0 75 | tqdm==4.67.1 76 | tzdata==2025.2 77 | uritemplate==4.1.1 78 | urllib3==2.4.0 79 | webdavclient3==3.14.6 80 | -------------------------------------------------------------------------------- /scripts/auto-update.sh: -------------------------------------------------------------------------------- 1 | # 1/ activate environment 2 | source /var/www/alyx-main/venv/bin/activate; 3 | cd /var/www/alyx-main/alyx 4 | # 2/ pull the changes from github (on your favourite branch) 5 | git stash 6 | git pull 7 | git stash pop 8 | # 3/ install any new requirements 9 | pip install -r requirements.txt 10 | # 4/ update database if scheme changes 11 | ./manage.py makemigrations 12 | ./manage.py migrate 13 | # 5/ If new fixtures load them in the database 14 | ../scripts/load-init-fixtures.sh 15 | # 6/ if new tables change the postgres permissions 16 | ./manage.py set_db_permissions 17 | ./manage.py set_user_permissions 18 | # 7/ if there were updates to the Django version collect the static files 19 | ./manage.py collectstatic --no-input 20 | # 8/ restart the apache server 21 | sudo service apache2 reload 22 | -------------------------------------------------------------------------------- /scripts/check-backup.sh: -------------------------------------------------------------------------------- 1 | DATE=$(date +%Y-%m-%d) 2 | URL=https://ibl.flatironinstitute.org/json/${DATE}_alyxfull.sql.gz 3 | LOGIN=$(cat ~/.one_params | jq -r '.HTTP_DATA_SERVER_LOGIN') 4 | PWD=$(cat ~/.one_params | jq -r '.HTTP_DATA_SERVER_PWD') 5 | FILESIZE=$(curl -u $LOGIN:$PWD -sI $URL | grep -i Content-Length | awk '{print $2 + 0}') 6 | 7 | # # macOS: 8 | # #alias NOTIF='/Users/username/Library/Python/3.8/bin/ntfy -t "Alyx backup" send ' 9 | 10 | # Ubuntu: 11 | function NOTIF() { 12 | notify-send "$1" "$2" 13 | } 14 | 15 | if [[ $FILESIZE -lt 600000000 ]] 16 | then 17 | NOTIF "ALYX BACKUP ALERT" "Backup failed on $DATE, file was $FILESIZE bytes" 18 | else 19 | NOTIF "Alyx backup" "Alyx backup for $DATE was $FILESIZE bytes" 20 | fi 21 | -------------------------------------------------------------------------------- /scripts/delete_migrations.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -r ./*/migrations/0* 3 | -------------------------------------------------------------------------------- /scripts/deployment_examples/01_backup_ibl.sh: -------------------------------------------------------------------------------- 1 | backup_dir="/backups/alyx-backups/$(date +%Y-%m-%d)" 2 | mkdir -p "$backup_dir" 3 | 4 | # Full SQL dump. 5 | /usr/bin/pg_dump -cOx -U ibl_dev -h localhost ibl -f "$backup_dir/alyx_full.sql" 6 | gzip -f "$backup_dir/alyx_full.sql" 7 | #scp -P 61022 "$backup_dir/alyx_full.sql.gz" alyx@ibl.flatironinstitute.org:/mnt/ibl/json/$(date +%Y-%m-%d)_alyxfull.sql.gz 8 | rsync -av --progress -e "ssh -i /home/ubuntu/.ssh/sdsc_alyx.pem -p 62022" "$backup_dir/alyx_full.sql.gz" alyx@ibl.flatironinstitute.org:/mnt/ibl/json/$(date +%Y-%m-%d)_alyxfull.sql.gz 9 | 10 | # Alyx cache tables 11 | source /var/www/alyx-main/venv/bin/activate 12 | python /var/www/alyx-main/alyx/manage.py one_cache --int-id 13 | 14 | source /var/www/alyx-dev/venv/bin/activate 15 | python /var/www/alyx-dev/alyx/manage.py one_cache --int-id 16 | 17 | # Full django JSON dump, used by datajoint 18 | python /var/www/alyx-main/alyx/manage.py dumpdata \ 19 | -e contenttypes -e auth.permission \ 20 | -e reversion.version -e reversion.revision -e admin.logentry \ 21 | -e actions.ephyssession \ 22 | -e actions.notification \ 23 | -e actions.notificationrule \ 24 | -e actions.virusinjection \ 25 | -e data.download \ 26 | -e experiments.brainregion \ 27 | -e jobs.task \ 28 | -e misc.note \ 29 | -e subjects.subjectrequest \ 30 | --indent 1 -o "alyx_full.json" 31 | gzip -f "alyx_full.json" 32 | #scp -i /home/ubuntu/.ssh/sdsc_alyx.pem -P 61022 "alyx_full.json.gz" alyx@ibl.flatironinstitute.org:/mnt/ibl/json/alyxfull.json.gz 33 | rsync -av --progress -e "ssh -i /home/ubuntu/.ssh/sdsc_alyx.pem -p 62022" "alyx_full.json.gz" alyx@ibl.flatironinstitute.org:/mnt/ibl/json/alyxfull.json.gz 34 | 35 | # clean up the backups on AWS instance 36 | python /var/www/alyx-main/scripts/deployment_examples/99_purge_duplicate_backups.py 37 | 38 | # statistics 39 | psql -U ibl_dev -h localhost -d ibl -f /home/ubuntu/table_sizes.sql > "${backup_dir}_pg_stats.txt" 40 | -------------------------------------------------------------------------------- /scripts/deployment_examples/02c_globus_sync_PT.sh: -------------------------------------------------------------------------------- 1 | cd /var/www/alyx-main/ 2 | source ./venv/bin/activate 3 | cd alyx 4 | 5 | ./manage.py files bulksync --lab=danlab 6 | ./manage.py files bulksync --lab=steinmetzlab 7 | ./manage.py files bulksync --lab=churchlandlab_ucla 8 | 9 | ./manage.py files bulktransfer --lab=danlab 10 | ./manage.py files bulktransfer --lab=steinmetzlab 11 | ./manage.py files bulktransfer --lab=churchlandlab_ucla 12 | 13 | sleep 900 14 | 15 | ./manage.py files bulksync --lab=danlab 16 | ./manage.py files bulksync --lab=steinmetzlab 17 | ./manage.py files bulksync --lab=churchlandlab_ucla 18 | -------------------------------------------------------------------------------- /scripts/deployment_examples/03_dev_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # First dumps database main 4 | /usr/bin/pg_dump -cOx -U ibl_dev -h localhost ibl -f alyx_full.sql 5 | # Delete the full dev database 6 | psql -q -U ibl_dev -h localhost -d ibl_dev -c "drop schema public cascade" 7 | psql -q -U ibl_dev -h localhost -d ibl_dev -c "create schema public" 8 | # Loads the main database into the dev one 9 | psql -h localhost -U ibl_dev -d ibl_dev -f alyx_full.sql 10 | rm alyx_full.sql 11 | # Mirror the migrations from alyx-main to alyx-dev: wipe them all out and then copy from alyx-main 12 | #rm -r /var/www/alyx-dev/alyx/*/migrations/0* 13 | #cp -r /var/www/alyx-main/alyx/misc/migrations/0* /var/www/alyx-dev/alyx/misc/migrations/ 14 | #cp -r /var/www/alyx-main/alyx/data/migrations/0* /var/www/alyx-dev/alyx/data/migrations/ 15 | #cp -r /var/www/alyx-main/alyx/actions/migrations/0* /var/www/alyx-dev/alyx/actions/migrations/ 16 | #cp -r /var/www/alyx-main/alyx/subjects/migrations/0* /var/www/alyx-dev/alyx/subjects/migrations/ 17 | # Apply migrations (if any) 18 | cd /var/www/alyx-dev/alyx 19 | source ../venv/bin/activate 20 | ./manage.py makemigrations 21 | ./manage.py migrate 22 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/000-default-conf-alyx-dev: -------------------------------------------------------------------------------- 1 | 2 | ServerName dev.alyx.internationalbrainlab.org 3 | Redirect permanent / https://dev.alyx.internationalbrainlab.org/ 4 | 5 | 6 | 7 | ServerName dev.alyx.internationalbrainlab.org 8 | ServerAdmin webmaster@internationalbrainlab.org 9 | DocumentRoot /var/www/alyx 10 | 11 | 12 | 13 | Require all granted 14 | 15 | Include /etc/apache2/sites-available/ip_whitelist.conf 16 | 17 | 18 | Alias /static/ /var/www/alyx/alyx/static/ 19 | Alias /media/ /var/www/alyx/alyx/media/ 20 | 21 | 22 | Require all granted 23 | 24 | 25 | 26 | Require all granted 27 | 28 | 29 | ErrorLog ${APACHE_LOG_DIR}/error_alyx.log 30 | CustomLog ${APACHE_LOG_DIR}/access_alyx.log combined 31 | 32 | WSGIApplicationGroup %{GLOBAL} 33 | WSGIDaemonProcess alyx python-path=/var/www/alyx/alyx python-home=/var/www/alyx/venv socket-user=#33 listen-backlog=50 34 | WSGIProcessGroup alyx 35 | WSGIScriptAlias / /var/www/alyx/alyx/alyx/wsgi.py 36 | WSGIPassAuthorization On 37 | 38 | SSLEngine on 39 | SSLCertificateFile /etc/apache2/ssl/fullchain.pem 40 | SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem 41 | SSLProtocol all -SSLv2 -SSLv3 42 | SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS 43 | SSLHonorCipherOrder on 44 | SSLCompression off 45 | SSLOptions +StrictRequire 46 | 47 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/000-default-conf-alyx-prod: -------------------------------------------------------------------------------- 1 | 2 | ServerName alyx.internationalbrainlab.org 3 | Redirect permanent / https://alyx.internationalbrainlab.org/ 4 | 5 | 6 | 7 | ServerName alyx.internationalbrainlab.org 8 | ServerAdmin webmaster@internationalbrainlab.org 9 | DocumentRoot /var/www/alyx 10 | 11 | 12 | 13 | Require all granted 14 | 15 | Include /etc/apache2/sites-available/ip_whitelist.conf 16 | 17 | 18 | Alias /static/ /var/www/alyx/alyx/static/ 19 | Alias /media/ /var/www/alyx/alyx/media/ 20 | 21 | 22 | Require all granted 23 | 24 | 25 | 26 | Require all granted 27 | 28 | 29 | ErrorLog ${APACHE_LOG_DIR}/error_alyx.log 30 | CustomLog ${APACHE_LOG_DIR}/access_alyx.log combined 31 | 32 | WSGIApplicationGroup %{GLOBAL} 33 | WSGIDaemonProcess alyx python-path=/var/www/alyx/alyx python-home=/var/www/alyx/venv socket-user=#33 listen-backlog=50 34 | WSGIProcessGroup alyx 35 | WSGIScriptAlias / /var/www/alyx/alyx/alyx/wsgi.py 36 | WSGIPassAuthorization On 37 | 38 | SSLEngine on 39 | SSLCertificateFile /etc/apache2/ssl/fullchain.pem 40 | SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem 41 | SSLProtocol all -SSLv2 -SSLv3 42 | SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS 43 | SSLHonorCipherOrder on 44 | SSLCompression off 45 | SSLOptions +StrictRequire 46 | 47 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/000-default-conf-openalyx: -------------------------------------------------------------------------------- 1 | 2 | ServerName openalyx.internationalbrainlab.org 3 | Redirect permanent / https://openalyx.internationalbrainlab.org/ 4 | 5 | 6 | 7 | ServerName openalyx.internationalbrainlab.org 8 | ServerAdmin webmaster@internationalbrainlab.org 9 | DocumentRoot /var/www/alyx 10 | 11 | 12 | 13 | Require all granted 14 | 15 | Include /etc/apache2/sites-available/ip_whitelist.conf 16 | 17 | 18 | Alias /static/ /var/www/alyx/alyx/static/ 19 | Alias /media/ /var/www/alyx/alyx/media/ 20 | 21 | 22 | Require all granted 23 | 24 | 25 | 26 | Require all granted 27 | 28 | 29 | ErrorLog ${APACHE_LOG_DIR}/error_alyx.log 30 | CustomLog ${APACHE_LOG_DIR}/access_alyx.log combined 31 | 32 | WSGIApplicationGroup %{GLOBAL} 33 | WSGIDaemonProcess alyx python-path=/var/www/alyx/alyx python-home=/var/www/alyx/venv socket-user=#33 listen-backlog=50 34 | WSGIProcessGroup alyx 35 | WSGIScriptAlias / /var/www/alyx/alyx/alyx/wsgi.py 36 | WSGIPassAuthorization On 37 | 38 | SSLEngine on 39 | SSLCertificateFile /etc/apache2/ssl/fullchain.pem 40 | SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem 41 | SSLProtocol all -SSLv2 -SSLv3 42 | SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS 43 | SSLHonorCipherOrder on 44 | SSLCompression off 45 | SSLOptions +StrictRequire 46 | 47 | # Configurations for easy maintenance mode 48 | RewriteEngine On 49 | RewriteCond %{ENV:REDIRECT_STATUS} !=503 50 | RewriteCond "/var/www/alyx/maintenance.trigger" -f 51 | RewriteRule ^(.*)$ /$1 [R=503,L] 52 | 53 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | # Time zone autoconfig 4 | ENV TZ=Europe/Lisbon 5 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 6 | 7 | # Install services, packages and perform cleanup 8 | RUN apt-get update && apt-get install -y \ 9 | apache2 \ 10 | apache2-utils \ 11 | git \ 12 | libapache2-mod-wsgi-py3 \ 13 | postgresql \ 14 | python3.8 \ 15 | python3-pip \ 16 | python3-venv \ 17 | virtualenv \ 18 | && apt-get autoremove \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Clone repo and configure virtual environment 22 | RUN git clone --branch master https://github.com/cortex-lab/alyx.git /var/www/alyx 23 | # Best practice for configuring python venv 24 | ENV VIRTUAL_ENV=/var/www/alyx/venv 25 | RUN virtualenv ${VIRTUAL_ENV} --python=python3 26 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 27 | WORKDIR /var/www/alyx 28 | RUN pip install -r requirements_frozen.txt 29 | WORKDIR /var/www/alyx/alyx 30 | RUN pip install ONE-api 31 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/Dockerfile.ibl: -------------------------------------------------------------------------------- 1 | FROM internationalbrainlab/alyx:base 2 | 3 | # Set build environment, passed from command line --build-arg (alyx-prod, alyx-dev, openalyx, etc) 4 | ARG BUILD_ENV 5 | 6 | # Apache ENVs 7 | ENV APACHE_RUN_USER www-data 8 | ENV APACHE_RUN_GROUP www-data 9 | ENV APACHE_LOCK_DIR /var/lock/apache2 10 | ENV APACHE_LOG_DIR /var/log/apache2 11 | ENV APACHE_PID_FILE /var/run/apache2/apache2.pid 12 | ENV APACHE_SERVER_NAME ${BUILD_ENV}.internationalbrainlab.org 13 | 14 | # Apache configs 15 | COPY apache-conf-${BUILD_ENV} /etc/apache2/apache2.conf 16 | COPY 000-default-conf-${BUILD_ENV} /etc/apache2/sites-available/000-default.conf 17 | COPY ip-whitelist-conf /etc/apache2/sites-available/ip_whitelist.conf 18 | COPY fullchain.pem-${BUILD_ENV} /etc/apache2/ssl/fullchain.pem 19 | COPY privkey.pem-${BUILD_ENV} /etc/apache2/ssl/privkey.pem 20 | 21 | # IBL Alyx configs 22 | RUN git clone --branch main https://github.com/int-brain-lab/iblalyx.git /home/ubuntu/iblalyx 23 | COPY settings.py-${BUILD_ENV} /var/www/alyx/alyx/alyx/settings.py 24 | COPY settings_secret.py-${BUILD_ENV} /var/www/alyx/alyx/alyx/settings_secret.py 25 | COPY settings_lab.py-${BUILD_ENV} /var/www/alyx/alyx/alyx/settings_lab.py 26 | RUN mkdir -p /backups/tables/ 27 | RUN mkdir -p /backups/uploaded/ \ 28 | && chown www-data:www-data /backups/uploaded/ \ 29 | && chmod 777 /backups/uploaded/ 30 | RUN touch /var/log/alyx.log \ 31 | && chown www-data:www-data /var/log/alyx.log \ 32 | && chmod 644 /var/log/alyx.log 33 | RUN ln -s /home/ubuntu/iblalyx/management/ibl_reports /var/www/alyx/alyx/ibl_reports 34 | RUN ln -s /home/ubuntu/iblalyx/management/ibl_reports/templates /var/www/alyx/alyx/templates/ibl_reports 35 | 36 | # Expose ports, enable apache modules, and launch apache 37 | EXPOSE 80 443 5432 38 | RUN a2enmod rewrite 39 | RUN a2enmod ssl 40 | RUN a2enmod wsgi 41 | CMD ["/usr/sbin/apache2ctl", "-DFOREGROUND"] 42 | 43 | # Certbot renewal configuration requirements 44 | RUN apt-get update && apt-get install -y \ 45 | awscli \ 46 | certbot \ 47 | python3-certbot-apache \ 48 | wget \ 49 | && apt-get autoremove \ 50 | && rm -rf /var/lib/apt/lists/* -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/apache-conf-alyx-dev: -------------------------------------------------------------------------------- 1 | ServerName dev.alyx.internationalbrainlab.org 2 | Mutex file:${APACHE_LOCK_DIR} default 3 | PidFile ${APACHE_PID_FILE} 4 | Timeout 300 5 | KeepAlive On 6 | MaxKeepAliveRequests 100 7 | KeepAliveTimeout 5 8 | User ${APACHE_RUN_USER} 9 | Group ${APACHE_RUN_GROUP} 10 | HostnameLookups Off 11 | ErrorLog ${APACHE_LOG_DIR}/error.log 12 | LogLevel warn 13 | IncludeOptional mods-enabled/*.load 14 | IncludeOptional mods-enabled/*.conf 15 | Include ports.conf 16 | 17 | 18 | Options FollowSymLinks 19 | AllowOverride None 20 | Require all denied 21 | 22 | 23 | AllowOverride None 24 | Require all granted 25 | 26 | 27 | Options Indexes FollowSymLinks 28 | AllowOverride All 29 | Require all granted 30 | 31 | 32 | AccessFileName .htaccess 33 | 34 | 35 | Require all denied 36 | 37 | 38 | LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined 39 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined 40 | LogFormat "%h %l %u %t \"%r\" %>s %O" common 41 | LogFormat "%{Referer}i -> %U" referer 42 | LogFormat "%{User-agent}i" agent 43 | 44 | IncludeOptional conf-enabled/*.conf 45 | IncludeOptional sites-enabled/* 46 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/apache-conf-alyx-prod: -------------------------------------------------------------------------------- 1 | ServerName alyx.internationalbrainlab.org 2 | Mutex file:${APACHE_LOCK_DIR} default 3 | PidFile ${APACHE_PID_FILE} 4 | Timeout 300 5 | KeepAlive On 6 | MaxKeepAliveRequests 100 7 | KeepAliveTimeout 5 8 | User ${APACHE_RUN_USER} 9 | Group ${APACHE_RUN_GROUP} 10 | HostnameLookups Off 11 | ErrorLog ${APACHE_LOG_DIR}/error.log 12 | LogLevel warn 13 | IncludeOptional mods-enabled/*.load 14 | IncludeOptional mods-enabled/*.conf 15 | Include ports.conf 16 | 17 | 18 | Options FollowSymLinks 19 | AllowOverride None 20 | Require all denied 21 | 22 | 23 | AllowOverride None 24 | Require all granted 25 | 26 | 27 | Options Indexes FollowSymLinks 28 | AllowOverride All 29 | Require all granted 30 | 31 | 32 | AccessFileName .htaccess 33 | 34 | 35 | Require all denied 36 | 37 | 38 | LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined 39 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined 40 | LogFormat "%h %l %u %t \"%r\" %>s %O" common 41 | LogFormat "%{Referer}i -> %U" referer 42 | LogFormat "%{User-agent}i" agent 43 | 44 | IncludeOptional conf-enabled/*.conf 45 | IncludeOptional sites-enabled/* 46 | -------------------------------------------------------------------------------- /scripts/deployment_examples/alyx-docker/apache-conf-openalyx: -------------------------------------------------------------------------------- 1 | ServerName openalyx.internationalbrainlab.org 2 | Mutex file:${APACHE_LOCK_DIR} default 3 | PidFile ${APACHE_PID_FILE} 4 | Timeout 300 5 | KeepAlive On 6 | MaxKeepAliveRequests 100 7 | KeepAliveTimeout 5 8 | User ${APACHE_RUN_USER} 9 | Group ${APACHE_RUN_GROUP} 10 | HostnameLookups Off 11 | ErrorLog ${APACHE_LOG_DIR}/error.log 12 | LogLevel warn 13 | IncludeOptional mods-enabled/*.load 14 | IncludeOptional mods-enabled/*.conf 15 | Include ports.conf 16 | 17 | 18 | Options FollowSymLinks 19 | AllowOverride None 20 | Require all denied 21 | 22 | 23 | AllowOverride None 24 | Require all granted 25 | 26 | 27 | Options Indexes FollowSymLinks 28 | AllowOverride All 29 | Require all granted 30 | 31 | 32 | AccessFileName .htaccess 33 | 34 | 35 | Require all denied 36 | 37 | 38 | LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined 39 | LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined 40 | LogFormat "%h %l %u %t \"%r\" %>s %O" common 41 | LogFormat "%{Referer}i -> %U" referer 42 | LogFormat "%{User-agent}i" agent 43 | 44 | IncludeOptional conf-enabled/*.conf 45 | IncludeOptional sites-enabled/* 46 | -------------------------------------------------------------------------------- /scripts/deployment_examples/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM postgres:13.1 3 | 4 | # note - pg_ctl & datadir as follows: 5 | # /usr/lib/postgresql/9.6/bin/pg_ctl -D /var/lib/postgresql/data 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y git python3-pip nvi \ 9 | && pip3 install --upgrade pip 10 | 11 | RUN cd / \ 12 | && git clone https://github.com/cortex-lab/alyx.git \ 13 | && cd alyx && pip3 install -r requirements.txt 14 | 15 | EXPOSE 8000/tcp 16 | COPY alyx-entrypoint.sh / 17 | ENTRYPOINT ["/alyx-entrypoint.sh" ] 18 | -------------------------------------------------------------------------------- /scripts/deployment_examples/docker/README: -------------------------------------------------------------------------------- 1 | 2 | alyx docker container 3 | ===================== 4 | 5 | for use in testing - 6 | 7 | requirements: 8 | 9 | - docker 10 | - docker-compose 11 | - an existing database dump in dump.sql.gz 12 | 13 | usage: 14 | 15 | $ cp dot-env.example .env 16 | $ $EDITOR .env 17 | $ # ... fetch a full alyx dump into dump.sql.gz 18 | $ docker-compose up -d 19 | 20 | then login to browser at http://127.0.0.1:8000/ 21 | using 'admin' and the ALYX_DB_PASSWORD used in .env 22 | 23 | the container entrypoint will attempt to reload the database and reconfigure 24 | the admin user on each invocation based on the presence of certain files: 25 | 26 | /alyx/alyx/db_loaded 27 | /alyx/alyx/superuser_created 28 | 29 | (paths are container-relative; see also alyx-entrypoint.sh) 30 | 31 | to reuse environment (e.g. after 'docker-compose stop alyx'), either 32 | change entrypoint in compose file, or map an alyx git checkout into '/alyx' 33 | as a volume to allow the db_loaded and superuser_created files to persist 34 | across runs (commented out example in 'docker-compose.yml') 35 | 36 | -------------------------------------------------------------------------------- /scripts/deployment_examples/docker/alyx-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # alyx entrypoint script 4 | # ====================== 5 | 6 | # globals 7 | # ------- 8 | 9 | dbdump="/dump.sql.gz" 10 | dbloaded="/alyx/alyx/db_loaded" 11 | sucreated="/alyx/alyx/superuser_created" 12 | 13 | err_exit() { echo "error: $*"; exit 1; } 14 | 15 | # checks 16 | # ------ 17 | 18 | [ -z "$PGUSER" ] && err_exit "PGUSER unset"; 19 | [ -z "$PGHOST" ] && err_exit "PGPASSWORD unset"; 20 | [ -z "$PGPASSWORD" ] && err_exit "PGPASSWORD unset"; 21 | [ ! -f "${dbdump}" ] && err_exit "no database dump in $dbdump"; 22 | 23 | # create/load database 24 | # -------------------- 25 | 26 | echo '# => database create/load' 27 | 28 | if [ ! -f "${dbloaded}" ]; then 29 | 30 | echo '# ==> database create' 31 | createdb alyx 32 | 33 | echo '# ==> database load' 34 | gzip -dc ${dbdump} |psql -d alyx 35 | 36 | touch ${dbloaded} 37 | fi 38 | 39 | # configure alyx/django 40 | # --------------------- 41 | 42 | echo '# => configuring alyx' 43 | 44 | if [ ! -f "$sucreated" ]; then 45 | 46 | echo '# ==> configuring settings_secret.py' 47 | 48 | sed \ 49 | -e "s/%SECRET_KEY%/0xdeadbeef/" \ 50 | -e "s/%DBNAME%/alyx/" \ 51 | -e "s/%DBUSER%/$PGUSER/" \ 52 | -e "s/%DBPASSWORD%/$PGPASSWORD/" \ 53 | -e "s/127.0.0.1/$PGHOST/" \ 54 | < /alyx/alyx/alyx/settings_secret_template.py \ 55 | > /alyx/alyx/alyx/settings_secret.py 56 | 57 | echo '# ==> creating alyx superuser' 58 | 59 | /alyx/alyx/manage.py createsuperuser \ 60 | --no-input \ 61 | --username admin \ 62 | --email admin@localhost 63 | 64 | echo '# ==> setting alyx superuser password' 65 | 66 | # note on superuser create: 67 | # 68 | # - no-input 'createsuperuser' creates without password 69 | # - cant set password from cli here or in setpassword command 70 | # - so script reset via manage.py shell 71 | # - see also: 72 | # https://stackoverflow.com/questions/6358030/\ 73 | # how-to-reset-django-admin-password 74 | 75 | /alyx/alyx/manage.py shell <<-EOF 76 | 77 | from django.contrib.auth import get_user_model 78 | User = get_user_model() 79 | admin = User.objects.get(username='admin') 80 | admin.set_password('$PGPASSWORD') 81 | admin.save() 82 | exit() 83 | 84 | EOF 85 | 86 | touch ${sucreated} 87 | fi 88 | 89 | # start alyx 90 | # ---------- 91 | 92 | echo '# => starting alyx' 93 | 94 | /alyx/alyx/manage.py makemigrations 95 | /alyx/alyx/manage.py migrate 96 | 97 | /alyx/alyx/manage.py runserver --insecure 0.0.0.0:8000 98 | 99 | -------------------------------------------------------------------------------- /scripts/deployment_examples/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: '2.4' 3 | 4 | services: 5 | 6 | # debug env: 7 | # tty: 8 | # image: postgres:13.1 9 | # init: true 10 | # command: tail -f /dev/null 11 | 12 | db: 13 | image: postgres:13.1 14 | environment: 15 | - POSTGRES_PASSWORD=${ALYX_DB_PASSWORD?set ALYX_DB_PASSWORD in .env} 16 | healthcheck: 17 | test: [ "CMD", "/usr/bin/psql", "-U", "postgres", "-c", "\\l" ] 18 | timeout: 5s 19 | retries: 60 20 | interval: 1s 21 | 22 | alyx: 23 | image: alyx-docker 24 | build: 25 | context: . 26 | init: true 27 | depends_on: 28 | db: 29 | condition: service_healthy 30 | environment: 31 | - PGUSER=${ALYX_DB_USER?set ALYX_DB_USER in .env} 32 | - PGHOST=${ALYX_DB_HOST?set ALYX_DB_HOST in .env} 33 | - PGPASSWORD=${ALYX_DB_PASSWORD?set ALYX_DB_PASSWORD in .env} 34 | ports: 35 | - '8000:8000' 36 | volumes: 37 | - ./dump.sql.gz:/dump.sql.gz 38 | # - ./alyx:/alyx 39 | 40 | -------------------------------------------------------------------------------- /scripts/deployment_examples/docker/dot-env.example: -------------------------------------------------------------------------------- 1 | 2 | # alyx-docker .env 3 | # ================ 4 | # 5 | # required: 6 | # 7 | # ALYX_DB_USER: database user for alyx 8 | # ALYX_DB_HOST: database host for alyx (from docker-compose 'db' service name) 9 | # ALYX_DB_PASSWORD: alyx 'admin'/db 'postgres' user password 10 | 11 | ALYX_DB_USER=postgres 12 | ALYX_DB_HOST=db 13 | ALYX_DB_PASSWORD=postgres 14 | 15 | -------------------------------------------------------------------------------- /scripts/deployment_examples/ibl_patcher_sync.py: -------------------------------------------------------------------------------- 1 | # do the synchronisation of IBL patcher files DMZ 2 | import logging 3 | from data.models import FileRecord, Dataset 4 | import data.transfers as transfers 5 | 6 | logger = logging.getLogger('data.transfers') 7 | logger.setLevel(20) 8 | 9 | # get the datasets that have one file record on the DMZ 10 | dsets = FileRecord.objects.filter(data_repository__name='ibl_patcher' 11 | ).values_list('dataset', flat=True).distinct() 12 | dsets = Dataset.objects.filter(pk__in=dsets) 13 | 14 | # delete the filerecords that are on the server already 15 | transfers.globus_delete_local_datasets(dsets, dry=False) 16 | 17 | 18 | # redo the query 19 | dsets = FileRecord.objects.filter(data_repository__name='ibl_patcher' 20 | ).values_list('dataset', flat=True).distinct() 21 | dsets = Dataset.objects.filter(pk__in=dsets) 22 | 23 | gc, tm = transfers.globus_transfer_datasets(dsets, dry=False) 24 | # todo waitfor transfer to finish 25 | 26 | # set the exist flag to true 27 | frs = FileRecord.objects.filter(data_repository__globus_is_personal=False, dataset__in=dsets) 28 | frs.update(exists=True) 29 | 30 | # remove the data from the FTP DMZ using globus (this also removes file records from dB) 31 | transfers.globus_delete_local_datasets(dsets, dry=False) 32 | -------------------------------------------------------------------------------- /scripts/deployment_examples/public_data_release.py: -------------------------------------------------------------------------------- 1 | """ 2 | DATA RELEASE PROTOTYPE 3 | From a full database, prune only the sessions needed for a public data release 4 | At the end, generates commands to be run on the flatiron server 5 | """ 6 | from pathlib import Path 7 | 8 | from django.conf import settings 9 | 10 | from actions.models import Session 11 | from subjects.models import Subject 12 | from misc.models import LabMember 13 | from data.models import DataRepository 14 | 15 | eids = ['89f0d6ff-69f4-45bc-b89e-72868abb042a', 'd33baf74-263c-4b37-a0d0-b79dcb80a764'] 16 | 17 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 18 | # Makes sure this is not run on a production database 19 | # THE CODE BELOW WILL PERMANENTLY DELETE ALL SESSIONS NOT IN THE EIDS LIST ABOVE 20 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 21 | assert 'public.alyx.internationalbrainlab.org' in settings.ALLOWED_HOSTS 22 | 23 | # first prune the database 24 | # have to delete sessions batch by batch to avoid out of memory issues 25 | N = 500 26 | while True: 27 | other_sessions = Session.objects.exclude(pk__in=eids) 28 | print(other_sessions.count()) 29 | if other_sessions.count() > N: 30 | to_del = Session.objects.filter(pk__in=other_sessions[:N].values_list('pk')) 31 | to_del.delete() 32 | else: 33 | other_sessions.delete() 34 | break 35 | 36 | ses = Session.objects.all() 37 | 38 | Subject.objects.exclude(pk__in=ses.values_list('subject')).delete() 39 | LabMember.objects.exclude(pk__in=ses.values_list('users').distinct()).delete() 40 | DataRepository.objects.filter(globus_is_personal=True).delete() 41 | 42 | repos = DataRepository.objects.all() 43 | for repo in repos: 44 | repo.data_url = repo.data_url.replace( 45 | "http://ibl.flatironinstitute.org/", "http://ibl.flatironinstitute.org/public/") 46 | repo.save() 47 | 48 | LabMember.objects.create_user('iblpublic', password="NeuroPhysTest") 49 | 50 | # Then create the commands to be run on the flatiron server 51 | # (TODO run commands from here but needs SSH keypairs) 52 | 53 | ROOT_PATH = Path('/mnt/ibl') 54 | PUBLIC_PATH = Path('/mnt/ibl/public') 55 | for s in ses: 56 | rel_session_path = Path(s.lab.name, 'Subjects', s.subject.nickname, 57 | s.start_time.strftime("%Y-%m-%d"), str(s.number).zfill(3)) 58 | cmd0 = f"mkdir -p {PUBLIC_PATH.joinpath(rel_session_path).parent}" 59 | cmd1 = f"ln -s {ROOT_PATH.joinpath(rel_session_path)} {PUBLIC_PATH.joinpath(rel_session_path)}" 60 | print(cmd0) 61 | print(cmd1) 62 | -------------------------------------------------------------------------------- /scripts/dump-init-fixtures.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ../alyx 3 | source ../alyxvenv/bin/activate 4 | 5 | ./manage.py dumpdata actions.proceduretype --indent 1 -o ./actions/fixtures/actions.proceduretype.json 6 | ./manage.py dumpdata actions.watertype --indent 1 -o ./actions/fixtures/actions.watertype.json 7 | ./manage.py dumpdata actions.cullreason --indent 1 -o ./actions/fixtures/actions.cullreason.json 8 | ./manage.py dumpdata actions.cullmethod --indent 1 -o ./actions/fixtures/actions.cullmethod.json 9 | 10 | ./manage.py dumpdata data.datarepositorytype --indent 1 -o ./data/fixtures/data.datarepositorytype.json 11 | ./manage.py dumpdata data.dataformat --indent 1 -o ./data/fixtures/data.dataformat.json 12 | # NB: need to null created_by field 13 | ./manage.py dumpdata data.datasettype --indent 1 -o ./data/fixtures/data.datasettype.json 14 | 15 | ./manage.py dumpdata misc.cagetype --indent 1 -o ./misc/fixtures/misc.cagetype.json 16 | ./manage.py dumpdata misc.enrichment --indent 1 -o ./misc/fixtures/misc.enrichment.json 17 | ./manage.py dumpdata misc.food --indent 1 -o ./misc/fixtures/misc.food.json 18 | 19 | ./manage.py dumpdata subjects.source --indent 1 -o ./subjects/fixtures/subjects.source.json 20 | 21 | ./manage.py dumpdata experiments.coordinatesystem --indent 1 -o ./experiments/fixtures/experiments.coordinatesystem.json 22 | ./manage.py dumpdata experiments.probemodel --indent 1 -o ./experiments/fixtures/experiments.probemodel.json 23 | ./manage.py dumpdata experiments.brainregions --indent 1 -o ./experiments/fixtures/experiments.brainregions.json 24 | -------------------------------------------------------------------------------- /scripts/load-init-fixtures.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ../alyx 3 | #source ../alyxvenv/bin/activate 4 | 5 | ./manage.py loaddata ./actions/fixtures/actions.proceduretype.json 6 | ./manage.py loaddata ./actions/fixtures/actions.watertype.json 7 | ./manage.py loaddata ./actions/fixtures/actions.cullreason.json 8 | ./manage.py loaddata ./actions/fixtures/actions.cullmethod.json 9 | 10 | ./manage.py loaddata ./data/fixtures/data.datarepositorytype.json 11 | ./manage.py loaddata ./data/fixtures/data.dataformat.json 12 | ./manage.py loaddata ./data/fixtures/data.datasettype.json 13 | 14 | ./manage.py loaddata ./misc/fixtures/misc.cagetype.json 15 | ./manage.py loaddata ./misc/fixtures/misc.enrichment.json 16 | ./manage.py loaddata ./misc/fixtures/misc.food.json 17 | #./manage.py loaddata ./misc/fixtures/misc.lab.json 18 | 19 | ./manage.py loaddata ./subjects/fixtures/subjects.source.json 20 | 21 | ./manage.py loaddata ./experiments/fixtures/experiments.coordinatesystem.json 22 | ./manage.py loaddata ./experiments/fixtures/experiments.probemodel.json 23 | ./manage.py loaddata ./experiments/fixtures/experiments.brainregion.json 24 | ./manage.py loaddata ./experiments/fixtures/experiments.imagingtype.json 25 | -------------------------------------------------------------------------------- /scripts/oneoff/2019-07-30-datasettypes.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | from data.models import DatasetType, Dataset 3 | 4 | # All existing dataset types. 5 | dataset_types = set(map(itemgetter(0), DatasetType.objects.values_list('name'))) 6 | 7 | # Dataset types that exist with AND without _ibl_ prefix need to be merged. 8 | duplicates = set('_ibl_' + name for name in dataset_types).intersection(dataset_types) 9 | 10 | # Reassign the object.attribute datasets to _ibl_object.attribute 11 | for name in duplicates: 12 | name_noibl = name[5:] 13 | assert name_noibl in dataset_types 14 | datasets = Dataset.objects.filter(dataset_type__name=name_noibl) 15 | assert name.startswith('_ibl_') 16 | print("Updating %d datasets with dataset type %s." % (len(datasets), name)) 17 | datasets.update(dataset_type=DatasetType.objects.get(name=name)) 18 | 19 | # There should no longer be any dataset with the reassigned dataset types. 20 | assert len(Dataset.objects.filter(dataset_type__name=name_noibl)) == 0 21 | 22 | # So we remove it. 23 | DatasetType.objects.get(name=name_noibl).delete() 24 | print("Dataset type %s removed." % name_noibl) 25 | -------------------------------------------------------------------------------- /scripts/oneoff/2019-08-30-patch_register.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from actions.models import Session 4 | from data.models import FileRecord, DataRepository 5 | import data.transfers 6 | 7 | PATCHING_GLOBUS_ID = "4ef795c6-05e0-11e9-9f96-0a06afd4a22e" 8 | 9 | sessions = Session.objects.filter(id="fb053e3d-4ca5-447f-a2ca-fedb93138864") 10 | 11 | fr_local = FileRecord.objects.filter(dataset__session__in=sessions, 12 | data_repository__globus_is_personal=True) 13 | fr_server = FileRecord.objects.filter(dataset__session__in=sessions, 14 | data_repository__globus_is_personal=False) 15 | 16 | assert fr_local.count() == fr_server.count() 17 | 18 | fr_server.update(exists=False) 19 | files_repos_save = fr_local.values_list('id', 'data_repository') 20 | fr_local.update() 21 | 22 | repo = fr_local.values_list('data_repository', flat=True).distinct() 23 | assert repo.count() == 1 24 | repo = DataRepository.objects.get(id=repo[0]) 25 | 26 | globus_id_save = repo.globus_endpoint_id 27 | repo.globus_endpoint_id = PATCHING_GLOBUS_ID 28 | repo.save() 29 | 30 | print('Transfering files over') 31 | data.transfers.bulk_transfer(lab=repo.lab_set.get().name) 32 | repo.globus_endpoint_id = globus_id_save 33 | repo.save() 34 | 35 | print('Waiting 10 mins to perform a read after write') 36 | time.sleep(600) 37 | data.transfers.bulk_sync(lab=repo.lab_set.get().name) 38 | -------------------------------------------------------------------------------- /scripts/oneoff/2019-09-30-UCL_IBL_compare.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import model_to_dict 2 | 3 | from subjects.models import Subject, Project 4 | from actions.models import Cull 5 | 6 | 7 | def compare_querysets(qs0, qs1, exclude_fields=None, include_fields=None, verbose=True): 8 | pk0 = list(qs0.values_list('pk', flat=True)) 9 | pk1 = list(qs1.values_list('pk', flat=True)) 10 | pks = list(set(pk0 + pk1)) 11 | for pk in pks: 12 | if pk not in pk1: 13 | if verbose: 14 | print(f'{pk} does not exist on right') 15 | continue 16 | if pk not in pk0: 17 | if verbose: 18 | print(f'{pk} does not exist on left') 19 | continue 20 | o1 = qs1.get(pk=pk) 21 | o0 = qs0.get(pk=pk) 22 | d0 = model_to_dict(o0) 23 | d1 = model_to_dict(o1) 24 | for ff in d0: 25 | if exclude_fields and ff in exclude_fields: 26 | continue 27 | if include_fields and ff not in include_fields: 28 | continue 29 | if not d0[ff] == d1[ff]: 30 | print(o0, d0[ff], d1[ff]) 31 | 32 | 33 | def compare_model(model, db0='cortexlab', db1='default', typ='intersect', **kwargs): 34 | # type intersect, left or right 35 | pk0 = model.objects.using(db0).values_list('id', flat=True) 36 | pk1 = model.objects.using(db1).values_list('id', flat=True) 37 | if typ == 'left': 38 | pk_ref = list(pk0) 39 | elif typ == 'right': 40 | pk_ref = list(pk1) 41 | elif typ == 'intersect': 42 | pk_ref = list(set(pk0).intersection(set(pk1))) 43 | qs0 = model.objects.using(db0).filter(pk__in=pk_ref) 44 | qs1 = model.objects.using(db1).filter(pk__in=pk_ref) 45 | compare_querysets(qs0, qs1, **kwargs) 46 | 47 | 48 | # compare Projects and Subjects 49 | compare_model(Project, typ='intersect') 50 | compare_model(Subject, db0='cortexlab', db1='default', include_fields=['projects'], typ='left', 51 | verbose=False) 52 | 53 | # compare ull objects Subjects 54 | subs = Subject.objects.values_list('pk', flat=True) 55 | qs0 = Cull.objects.using('cortexlab').filter(subject__pk__in=list(subs)) 56 | subs = Subject.objects.using('cortexlab').values_list('pk', flat=True) 57 | qs1 = Cull.objects.filter(subject__pk__in=list(subs)) 58 | compare_querysets(qs0, qs1) 59 | -------------------------------------------------------------------------------- /scripts/oneoff/2019-10-04-DatasetTypes_DataMigration.py: -------------------------------------------------------------------------------- 1 | """ `./manage.py shell < ../scripts/oneoff/2019-10-04-DatasetTypes_DataMigration.py` """ 2 | 3 | from django.core.management import call_command 4 | from data.models import DatasetType, Dataset 5 | 6 | DRY = False 7 | 8 | dtypes_old2delte_reassign = [ 9 | # _iblrig_Camera.timestamps 10 | ('93b79e71-c8f6-4d81-b298-2c8f6aefe192', 'b5ec79de-9c9e-4009-8892-10aa2ddb9638'), 11 | # __iblrig_Camera.timestamps 12 | ('169323bd-7f91-4b75-a3e5-69530db33d21', 'b5ec79de-9c9e-4009-8892-10aa2ddb9638'), 13 | # _iblrig_Camera.raw 14 | ('b3e9dded-027e-4cf0-8392-2baeb3bfcabd', 'e40899d0-a883-40ac-8214-344bcf249d09'), 15 | # _iblrig_Camera.raw 16 | ('df68dbb7-8d0e-4a5b-b829-e2a832a89b62', 'e40899d0-a883-40ac-8214-344bcf249d09'), 17 | # wheel.times 18 | ('b396747f-0c67-4de4-9610-c7e210a5b86a', '74c0120c-7515-478f-9725-53d587d86c49'), 19 | ] 20 | 21 | # load init fixtures 22 | call_command('loaddata', './data/fixtures/data.dataformat.json') 23 | call_command('loaddata', './data/fixtures/data.datasettype.json') 24 | 25 | for pks in dtypes_old2delte_reassign: 26 | dt2del = DatasetType.objects.filter(pk=pks[0]) 27 | if not dt2del: 28 | continue 29 | # if we find any dataset reassign them to the proper one 30 | datasets = Dataset.objects.filter(dataset_type=dt2del[0]) 31 | if len(datasets): 32 | d2new = DatasetType.objects.get(pk=pks[1]) 33 | print('reassign ' + dt2del[0].name + ' to ' + d2new.name) 34 | if not DRY: 35 | datasets.update(dataset_type=d2new) 36 | print('delete ' + dt2del[0].name) 37 | if not DRY: 38 | dt2del.delete() 39 | -------------------------------------------------------------------------------- /scripts/oneoff/2024-03-16-update_test_fixture.py: -------------------------------------------------------------------------------- 1 | """Move implant weight in test fixtures.""" 2 | import gzip 3 | import json 4 | import random 5 | from pathlib import Path 6 | 7 | # Fixture file 8 | path = Path(__file__).parents[2].joinpath('data', 'all_dumped_anon.json.gz') 9 | if not path.exists(): 10 | raise FileNotFoundError 11 | 12 | # Load and parse fixture 13 | with gzip.open(path, 'rb') as fp: 14 | data = json.load(fp) 15 | 16 | # Get implant weight map 17 | pk2iw = {r['pk']: r['fields']['implant_weight'] 18 | for r in filter(lambda r: r['model'] == 'subjects.subject', data)} 19 | 20 | # Add implant weights to surgeries 21 | for record in filter(lambda r: r['model'] == 'actions.surgery', data): 22 | # Check if implant surgery 23 | implant = (any('implant' in p for p in record['fields'].get('procedures', [])) or 24 | 'headplate' in record['fields']['narrative']) 25 | # Implant weight should be subject's implant weight 26 | iw = pk2iw[record['fields']['subject']] 27 | if iw is None: # ... or a random float rounded to 2 decimal places 28 | iw = float(f'{random.randint(15, 20) + random.random():.2f}') 29 | # If not implant surgery, set to 0, otherwise use above weight 30 | record['fields'].update(implant_weight=iw if implant else 0.) 31 | 32 | # Remove implant weights from subjects 33 | for record in filter(lambda r: r['model'] == 'subjects.subject', data): 34 | record['fields'].pop('implant_weight') 35 | 36 | # find any with multiple surgeries 37 | # from collections import Counter 38 | # surgeries = filter(lambda r: r['model'] == 'actions.surgery', data) 39 | # counter = Counter(map(lambda r: r['fields']['subject'], surgeries)) 40 | # pk, total = counter.most_common()[3] 41 | # assert total > 1 42 | # recs = filter(lambda r: r['model'] == 'actions.surgery' and r['fields']['subject'] == pk, data) 43 | 44 | # Write to file 45 | with gzip.open(path, 'wt', encoding='UTF-8') as fp: 46 | json.dump(data, fp, indent=2) 47 | -------------------------------------------------------------------------------- /scripts/oneoff/2025-01-01-water_fixture_rename.py: -------------------------------------------------------------------------------- 1 | """There are some duplicate water type fixtures and inconsistent naming. 2 | 3 | To address this the convention will now be principal substance first folled by a descending list 4 | of concentation and name of each addative. For example, 'Water 2% Citric Acid', 'Hydrogel 1% NaCl'. 5 | 6 | This script will migrate the water type 'Citric Acid Water 2%' to 'Water 2% Citric Acid'. 7 | NB: This should be run after applying latest fixtures! 8 | 9 | https://github.com/cortex-lab/alyx/issues/886 10 | """ 11 | from actions.models import WaterType, WaterAdministration, WaterRestriction 12 | 13 | to_change = [ 14 | ('Citric Acid 2.5%', 'Water 2.5% Citric Acid'), 15 | ('Citric Acid Water 3%', 'Water 3% Citric Acid'), 16 | ('Citric Acid Water 2%', 'Water 2% Citric Acid') 17 | ] 18 | for old_name, new_name in to_change: 19 | try: 20 | old_water_type = WaterType.objects.get(name=old_name) 21 | except WaterType.DoesNotExist: 22 | continue 23 | try: 24 | # Test whether the new water type already exists 25 | correct_water_type = WaterType.objects.get(name=new_name) 26 | # Find all water administrations and restrictions with old water type 27 | water_administrations = WaterAdministration.objects.filter(water_type=old_water_type) 28 | water_restrictions = WaterRestriction.objects.filter(water_type=old_water_type) 29 | # Update the water type to new name 30 | n_updated = water_administrations.update(water_type=correct_water_type) 31 | n_updated += water_restrictions.update(water_type=correct_water_type) 32 | # Remove old water type 33 | affected, affected_models = old_water_type.delete() 34 | assert affected == 1, 'only one water type should have been deleted' 35 | print(f'New water type "{new_name}" added to {n_updated} records; "{old_name}" deleted') 36 | except WaterType.DoesNotExist: 37 | # If the new water type does not exist, simply rename the old one 38 | old_water_type.name = new_name 39 | old_water_type.save() 40 | print(f'Water type "{old_name}" has been renamed to "{new_name}"') -------------------------------------------------------------------------------- /scripts/sanitize-init-fixtures.sh.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # for the data.datasettype fixture, remove unknown and null the created-by field 4 | file_fixtures = './data/fixtures/data.datasettype.json' 5 | with open(file_fixtures) as ff: 6 | fix = json.load(ff) 7 | 8 | for i, f in enumerate(fix): 9 | f['fields']['created_by'] = None 10 | if f['fields']['name'] == 'unknown': 11 | fix.remove(f) 12 | 13 | with open(file_fixtures, 'w') as outfile: 14 | json.dump(fix, outfile, indent=1) 15 | 16 | # for the data.dataformat fixture, remove unknown 17 | file_fixtures = './data/fixtures/data.dataformat.json' 18 | with open(file_fixtures) as ff: 19 | fix = json.load(ff) 20 | 21 | for i, f in enumerate(fix): 22 | if f['fields']['name'] == 'unknown': 23 | fix.remove(f) 24 | 25 | with open(file_fixtures, 'w') as outfile: 26 | json.dump(fix, outfile, indent=1) 27 | -------------------------------------------------------------------------------- /scripts/sync_ucl/pk_sync/Alyx_update_PK_from_UCL.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | IBL_DATABASE="ibl" 4 | ALYX_PATH="/var/www/alyx-main/" 5 | 6 | set -e 7 | echo "Downloading the cortexlab database backup" 8 | cd $ALYX_PATH 9 | 10 | scp ubuntu@alyx.cortexlab.net:/var/www/alyx/alyx-backups/$(date +%Y-%m-%d)/alyx_full.sql.gz ./scripts/sync_ucl/cortexlab.sql.gz 11 | 12 | echo "Reinitialize the cortexlab database" 13 | psql -q -U ibl_dev -h localhost -d cortexlab -c "drop schema public cascade" 14 | psql -q -U ibl_dev -h localhost -d cortexlab -c "create schema public" 15 | gunzip ./scripts/sync_ucl/cortexlab.sql.gz 16 | psql -h localhost -U ibl_dev -d cortexlab -f ./scripts/sync_ucl/cortexlab.sql 17 | rm ./scripts/sync_ucl/cortexlab.sql 18 | 19 | cd alyx 20 | source ../venv/bin/activate 21 | echo "DEV - REINIT PKS: full database initialisation" 22 | # update primary keys in the current database 23 | ./manage.py shell < ../scripts/sync_ucl/pk_sync/Alyx_update_PK_from_UCL.py 24 | # Deletes the full dev database 25 | psql -q -U ibl_dev -h localhost -d $IBL_DATABASE -c "drop schema public cascade" 26 | psql -q -U ibl_dev -h localhost -d $IBL_DATABASE -c "create schema public" 27 | # Apply all the migrations from scratch on the empty database 28 | ./manage.py makemigrations # should show no changes detected 29 | ./manage.py migrate 30 | ./manage.py shell -c "from data.models import DataFormat; DataFormat.objects.get(name='unknown').delete()" 31 | ./manage.py shell -c "from data.models import DatasetType; DatasetType.objects.get(name='unknown').delete()" 32 | # Load everything in the database 33 | psql -q -U ibl_dev -h localhost -d $IBL_DATABASE -c "delete from auth_group_permissions; delete from auth_permission; delete from django_admin_log; delete from django_content_type;" 34 | echo "DEV - LOAD DB w/ new PKS" 35 | ./manage.py loaddata ../scripts/sync_ucl/ibl-alyx-pkupdate-after.json 36 | -------------------------------------------------------------------------------- /scripts/sync_ucl/ucl_post_json_import.py: -------------------------------------------------------------------------------- 1 | from misc.models import Lab 2 | from experiments.models import ProbeInsertion 3 | (Lab.objects 4 | .filter(name='cortexlab') 5 | .update(json=Lab.objects.using('cortexlab').get(name='cortexlab').json) 6 | ) 7 | 8 | for pi in ProbeInsertion.objects.filter(session__lab__name='cortexlab'): 9 | pi.save() 10 | -------------------------------------------------------------------------------- /scripts/templates/.pgpass_template: -------------------------------------------------------------------------------- 1 | localhost:5432:%DBNAME%:%DBUSER%:%DBPASSWORD% 2 | -------------------------------------------------------------------------------- /scripts/templates/dump_db.sh: -------------------------------------------------------------------------------- 1 | DBNAME='%DBNAME%' 2 | DBUSER='%DBUSER%' 3 | FILENAME='alyx.sql' 4 | 5 | pg_dump -cOx -U $DBUSER -h localhost $DBNAME -f $FILENAME 6 | gzip $FILENAME 7 | -------------------------------------------------------------------------------- /scripts/templates/load_db.sh: -------------------------------------------------------------------------------- 1 | DBNAME='%DBNAME%' 2 | DBUSER='%DBUSER%' 3 | FILENAME='alyx.sql.gz' 4 | 5 | gunzip $FILENAME 6 | alyxvenv/bin/python alyx/manage.py reset_db 7 | psql -h localhost -U $DBUSER -d $DBNAME -f ${FILENAME%.*} 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [tool:pytest] 5 | 6 | [flake8] 7 | ignore = E117,E265,E731,F403,E741,E722,W504,D 8 | max-line-length = 99 9 | exclude = migrations 10 | 11 | [coverage:run] 12 | branch = False 13 | source = alyx 14 | omit = 15 | 16 | [coverage:report] 17 | exclude_lines = 18 | pragma: no cover 19 | raise AssertionError 20 | raise NotImplementedError 21 | pass 22 | return$ 23 | show_missing = True 24 | -------------------------------------------------------------------------------- /utils/user-uuid/dump.sh: -------------------------------------------------------------------------------- 1 | ../../alyx/manage.py dumpdata -e contenttypes -e auth.permission -e reversion.version -e reversion.revision -e admin.logentry -e authtoken.token -e auth.group --indent 1 > dump.json 2 | -------------------------------------------------------------------------------- /utils/user-uuid/load.sh: -------------------------------------------------------------------------------- 1 | ../../alyx/manage.py loaddata dump.uuid.json 2 | --------------------------------------------------------------------------------