├── .env
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── gh-pages.yml
│ ├── side-effects.yml
│ └── unit-test.yml
├── .gitignore
├── .worker.env
├── LICENCE
├── README.md
├── backend
├── Dockerfile
├── Dockerfile-test
├── backend_django
│ ├── __init__.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── settings_dev.py
│ │ ├── settings_prod.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── galv
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── fixtures
│ │ │ ├── DataColumnType.json
│ │ │ └── DataUnit.json
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ └── commands
│ │ │ │ ├── __init__.py
│ │ │ │ ├── create_superuser.py
│ │ │ │ └── init_db.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── pagination.py
│ │ ├── permissions.py
│ │ ├── schema.py
│ │ ├── serializers.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── factories.py
│ │ │ ├── test_api_tokens.py
│ │ │ ├── test_view_cells.py
│ │ │ ├── test_view_datasets.py
│ │ │ ├── test_view_equipment.py
│ │ │ ├── test_view_files.py
│ │ │ ├── test_view_harvester.py
│ │ │ ├── test_view_path.py
│ │ │ ├── test_view_user.py
│ │ │ └── utils.py
│ │ ├── utils.py
│ │ └── views.py
│ └── manage.py
├── requirements-test.txt
├── requirements.txt
└── server.sh
├── docker-compose.dev.yml
├── docker-compose.docs.yml
├── docker-compose.harvester.yml
├── docker-compose.test.yml
├── docker-compose.yml
├── docs
├── Makefile
├── README.txt
├── make.bat
└── source
│ ├── DevelopmentGuide.rst
│ ├── FirstTimeQuickSetup.rst
│ ├── UserGuide.rst
│ ├── conf.py
│ ├── img
│ ├── Galv-logo-lg.png
│ ├── Galv-logo-shortened.png
│ ├── Galv-logo-sm.png
│ ├── GalvStructure.PNG
│ ├── Galv_DB_ERD.png
│ ├── galv_frontend.jpg
│ └── galv_frontend_v1.png
│ └── index.rst
├── frontend
├── .dockerignore
├── .env
├── .gitignore
├── Dockerfile
├── Dockerfile_dev
├── README.md
├── index.html
├── inject_envvars.sh
├── nginx.conf.template
├── package.json
├── public
│ ├── Galvanalyser-icon.svg
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── APIConnection.ts
│ ├── ActionButtons.tsx
│ ├── Api.js
│ ├── App.css
│ ├── App.tsx
│ ├── ApproveUsers.tsx
│ ├── AsyncTable.tsx
│ ├── CellList.tsx
│ ├── Cells.tsx
│ ├── DatasetChart.tsx
│ ├── DatasetDetail.js
│ ├── Datasets.tsx
│ ├── Equipment.tsx
│ ├── Files.tsx
│ ├── FormComponents.js
│ ├── Galv-icon.svg
│ ├── Galv-logo.svg
│ ├── GetDatasetJulia.js
│ ├── GetDatasetMatlab.js
│ ├── GetDatasetPython.js
│ ├── HarvesterEnv.tsx
│ ├── Harvesters.tsx
│ ├── Login.tsx
│ ├── MonitoredPaths.tsx
│ ├── PaginatedTable.tsx
│ ├── Tokens.tsx
│ ├── UseStyles.ts
│ ├── UserProfile.tsx
│ ├── UserRoleSet.tsx
│ ├── __mocks__
│ │ ├── CellList.tsx
│ │ ├── DatasetChart.tsx
│ │ ├── Files.tsx
│ │ ├── GetDatasetJulia.js
│ │ ├── GetDatasetMatlab.js
│ │ ├── GetDatasetPython.js
│ │ ├── HarvesterEnv.tsx
│ │ ├── MonitoredPaths.tsx
│ │ └── UserRoleSet.tsx
│ ├── auth
│ │ └── index.js
│ ├── conf.json
│ ├── index.css
│ ├── index.d.ts
│ ├── index.jsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.js
│ ├── setupTests.js
│ └── test
│ │ ├── ActionButtons.test.js
│ │ ├── ActivateUsers.test.js
│ │ ├── CellList.test.js
│ │ ├── Cells.test.js
│ │ ├── DatasetChart.test.js
│ │ ├── Datasets.test.js
│ │ ├── Equipment.test.js
│ │ ├── Files.test.js
│ │ ├── Harvester.test.js
│ │ ├── HarvesterEnv.test.js
│ │ ├── MonitoredPaths.test.js
│ │ ├── README.md
│ │ ├── Tokens.test.js
│ │ ├── UserProfile.test.js
│ │ ├── UserRoleSet.test.js
│ │ ├── fixtures
│ │ ├── cell_families.json
│ │ ├── cells.json
│ │ ├── columns.json
│ │ ├── datasets.json
│ │ ├── equipment.json
│ │ ├── files.json
│ │ ├── harvesters.json
│ │ ├── inactive_users.json
│ │ ├── monitored_paths.json
│ │ ├── tokens.json
│ │ └── users.json
│ │ └── run_tests.sh
├── tsconfig.json
└── yarn.lock
├── galv.service
├── harvester
├── Dockerfile
├── harvester
│ ├── __init__.py
│ ├── api.py
│ ├── harvest.py
│ ├── parse
│ │ ├── __init__.py
│ │ ├── biologic_input_file.py
│ │ ├── exceptions.py
│ │ ├── input_file.py
│ │ ├── ivium_input_file.py
│ │ └── maccor_input_file.py
│ ├── run.py
│ ├── settings.py
│ └── utils.py
├── requirements.txt
├── start.py
└── test
│ ├── __init__.py
│ └── test_harvester.py
├── nginx-proxy
├── Dockerfile
└── default
├── restructure.png
└── setup.py
/.env:
--------------------------------------------------------------------------------
1 | # this is the directory that the galv postgres database will be located
2 | GALV_DATA_PATH=./.run/data/galv
3 |
4 | # this is the directory that will be scanned for battery tester output files when
5 | # running the harvester test suite
6 | GALV_HARVESTER_TEST_PATH=./.run/test_datafiles
7 |
8 | # Required to get react running:
9 | NODE_OPTIONS=--openssl-legacy-provider
10 |
11 | # database access; other credentials in .env.secret
12 | POSTGRES_USER=postgres
13 | # (container name)
14 | POSTGRES_HOST=postgres
15 | POSTGRES_PORT=5432
16 |
17 | # CORS configuration for backend
18 | VIRTUAL_HOST_ROOT=localhost
19 |
20 | # NGINX configuration
21 | # LETSENCRYPT_HOST=localhost
22 | LETSENCRYPT_EMAIL=oxfordrse@gmail.com
23 | LETSENCRYPT_TEST=true
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve Galv
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Additional context**
24 | Add any other context about the problem here.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Run when Docs workflow completes
6 | workflow_run:
7 | branches:
8 | - main
9 | workflows: [Docs]
10 | types:
11 | - completed
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
17 | permissions:
18 | contents: read
19 | pages: write
20 | id-token: write
21 |
22 | # Allow one concurrent deployment
23 | concurrency:
24 | group: "pages"
25 | cancel-in-progress: true
26 |
27 | jobs:
28 | # Single deploy job since we're just deploying
29 | deploy:
30 | environment:
31 | name: github-pages
32 | url: ${{ steps.deployment.outputs.page_url }}
33 | runs-on: ubuntu-latest
34 | steps:
35 | - name: Checkout
36 | uses: actions/checkout@v3
37 | with:
38 | ref: 'gh-pages'
39 | - name: Setup Pages
40 | uses: actions/configure-pages@v3
41 | - name: Upload artifact
42 | uses: actions/upload-pages-artifact@v1
43 | with:
44 | # Upload entire repository
45 | path: '.'
46 | - name: Deploy to GitHub Pages
47 | id: deployment
48 | uses: actions/deploy-pages@v1
49 |
--------------------------------------------------------------------------------
/.github/workflows/side-effects.yml:
--------------------------------------------------------------------------------
1 | # Generate documentation and associated files
2 | # If file gets unweildy with just one job, could refactor to use outputs:
3 | # https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs
4 |
5 | # This name is referenced by gh-pages.yml workflow. Update there if this changes.
6 | name: Docs
7 | on:
8 | push:
9 | branches:
10 | - main
11 | pull_request:
12 | branches:
13 | - '**'
14 | workflow_dispatch:
15 | inputs:
16 | deploy_docs:
17 | type: boolean
18 | description: 'Run the deploy-to-gh-pages step'
19 | required: false
20 | default: false
21 | debug_enabled:
22 | type: boolean
23 | description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
24 | required: false
25 | default: false
26 |
27 | jobs:
28 | build-erd:
29 | runs-on: ubuntu-latest
30 | env:
31 | POSTGRES_PASSWORD: "galv"
32 | DJANGO_SECRET_KEY: "long-and-insecure-key-12345"
33 | steps:
34 | - uses: actions/checkout@v3
35 | - name: Install additional requirements
36 | run: |
37 | sudo apt-get install -y graphviz
38 | sudo pip install sphinx
39 | mkdir docs/source/resources
40 | echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" > .env.secret
41 | echo "DJANGO_SECRET_KEY=$DJANGO_SECRET_KEY" >> .env.secret
42 | echo "django-extensions==3.2.1" >> backend/requirements.txt
43 | sed -i "s+'rest_framework',+'rest_framework', 'django_extensions',+g" backend/backend_django/config/settings_prod.py
44 |
45 | - name: Create Entity Relationship Diagram
46 | run: |
47 | docker-compose -f docker-compose.docs.yml run app python manage.py graph_models --dot --output output.dot galv
48 | dot -Tpng backend/backend_django/output.dot -o docs/source/resources/ERD.png
49 |
50 | - name: Create API spec
51 | run: |
52 | docker-compose -f docker-compose.docs.yml run app python manage.py spectacular --file schema.yml
53 | docker-compose -f docker-compose.docs.yml run app python manage.py spectacular --format openapi-json --file schema.json
54 | mv backend/backend_django/schema.* docs/source/resources/
55 |
56 | # - name: Create API client
57 | # run: |
58 | # echo "{\"lang\": \"python\", \"type\": \"CLIENT\", \"codegenVersion\": \"V3\", \"spec\": $(cat docs/schema.json)}" > payload.json
59 | # curl -d @payload.json --output docs/source/resources/galv-client-python.zip -H "Content-Type: application/json" https://generator3.swagger.io/api/generate
60 | # # Check size
61 | # if [ ! -s docs/source/resources/galv-client-python.zip ]; then
62 | # echo "Downloaded python client zip file is zero bytes"
63 | # exit 1
64 | # fi
65 | # # Check we can unzip
66 | # unzip -t docs/source/resources/galv-client-python.zip
67 |
68 | # Enable tmate debugging of manually-triggered workflows if the input option was provided
69 | - name: Setup tmate session
70 | uses: mxschmitt/action-tmate@v3
71 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
72 |
73 | - name: Sphinx build
74 | run: |
75 | cd docs
76 | make html
77 |
78 | - name: Push to gh-pages branch
79 | if: (github.ref_name == 'main' && github.event_name == 'push') || inputs.deploy_docs
80 | run: |
81 | git worktree add gh-pages
82 | git config user.name "Deploy from CI"
83 | git config user.email ""
84 | cd gh-pages
85 | # Delete the ref to avoid keeping history.
86 | git update-ref -d refs/heads/gh-pages
87 | rm -rf *
88 | mv ../docs/build/html/* .
89 | git add .
90 | git commit -m "Deploy $GITHUB_SHA to gh-pages"
91 | git push --set-upstream origin gh-pages --force
92 |
--------------------------------------------------------------------------------
/.github/workflows/unit-test.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests (Docker)
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - '**'
9 | workflow_dispatch:
10 | inputs:
11 | debug_enabled:
12 | type: boolean
13 | description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
14 | required: false
15 | default: false
16 |
17 | jobs:
18 | test-harvester:
19 | runs-on: ubuntu-latest
20 | env:
21 | GALV_HARVESTER_TEST_PATH: .test_datafiles
22 | steps:
23 | - uses: actions/checkout@v3
24 |
25 | - name: Install smbclient
26 | run: |
27 | sudo apt-get update
28 | sudo apt-get install -y smbclient
29 |
30 | - name: Restore cached test suite
31 | id: cache-restore
32 | uses: actions/cache/restore@v3
33 | with:
34 | path: ${{ env.GALV_HARVESTER_TEST_PATH }}
35 | key: liionsden-test-suite
36 |
37 | - name: Download test suite
38 | if: steps.cache-restore.outputs.cache-hit != 'true'
39 | env:
40 | LIIONSDEN_SMB_PATH: ${{ secrets.LIIONSDEN_SMB_PATH }}
41 | LIIONSDEN_SMB_USERNAME: ${{ secrets.LIIONSDEN_SMB_USERNAME}}
42 | LIIONSDEN_SMB_PASSWORD: ${{ secrets.LIIONSDEN_SMB_PASSWORD}}
43 | run: |
44 | sudo mkdir -p $GALV_HARVESTER_TEST_PATH
45 | cd $GALV_HARVESTER_TEST_PATH
46 | sudo smbget -R $LIIONSDEN_SMB_PATH/test-suite-small -U "$LIIONSDEN_SMB_USERNAME%$LIIONSDEN_SMB_PASSWORD"
47 |
48 | - name: Cache test suite
49 | id: cache-save
50 | if: steps.cache-restore.outputs.cache-hit != 'true'
51 | uses: actions/cache/save@v3
52 | with:
53 | path: ${{ env.GALV_HARVESTER_TEST_PATH }}
54 | key: ${{ steps.cache-restore.outputs.cache-primary-key }}
55 |
56 | - name: Build the stack
57 | run: touch .env.secret && docker-compose -f docker-compose.test.yml build harvester_test
58 |
59 | # Enable tmate debugging of manually-triggered workflows if the input option was provided
60 | - name: Setup tmate session
61 | uses: mxschmitt/action-tmate@v3
62 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
63 |
64 | - name: Run tests
65 | run: docker-compose -f docker-compose.test.yml up harvester_test
66 |
67 | test-backend:
68 | runs-on: ubuntu-latest
69 | env:
70 | POSTGRES_PASSWORD: "galv"
71 | DJANGO_SECRET_KEY: "long-and-insecure-key-12345"
72 | FRONTEND_VIRTUAL_HOST: "http://localhost"
73 | VIRTUAL_HOST: "localhost"
74 | steps:
75 | - uses: actions/checkout@v3
76 |
77 | - name: Set up secrets
78 | run: |
79 | touch .env.secret
80 | echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" > .env.secret
81 | echo "DJANGO_SECRET_KEY=$DJANGO_SECRET_KEY" >> .env.secret
82 | echo "FRONTEND_VIRTUAL_HOST=$FRONTEND_VIRTUAL_HOST" >> .env.secret
83 | echo "VIRTUAL_HOST=$VIRTUAL_HOST" >> .env.secret
84 | echo "POSTGRES_HOST=postgres_test" >> .env.secret
85 |
86 | # Enable tmate debugging of manually-triggered workflows if the input option was provided
87 | - name: Setup tmate session
88 | uses: mxschmitt/action-tmate@v3
89 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
90 |
91 | - name: Build the stack
92 | run: docker-compose -f docker-compose.test.yml up -d --build app_test
93 |
94 | - name: Run tests
95 | run: docker-compose -f docker-compose.test.yml run --rm app_test bash -c "cd .. && ./server.sh"
96 |
97 | test-frontend:
98 | runs-on: ubuntu-latest
99 | steps:
100 | - uses: actions/checkout@v3
101 |
102 | - name: Set up secrets
103 | run: touch .env.secret
104 |
105 | - name: Build the stack
106 | run: docker-compose -f docker-compose.test.yml build frontend_test
107 |
108 | # Enable tmate debugging of manually-triggered workflows if the input option was provided
109 | - name: Setup tmate session
110 | uses: mxschmitt/action-tmate@v3
111 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
112 |
113 | - name: Run tests
114 | run: docker-compose -f docker-compose.test.yml run --rm frontend_test bash -c "./src/test/run_tests.sh"
115 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .vscode
3 | .venv
4 | .removed
5 | galv/protobuf
6 | galv/webapp/assets/protobuf
7 | libs/galv-js-protobufs
8 | galv-protobuf.js
9 | webstack/.env
10 | .pnpm-store
11 | *.egg-info
12 | env
13 |
14 | .idea/
15 |
16 | .run/
17 |
18 | .test-data/
19 |
20 | frontend/src/demo_matlab_code.m
21 |
22 | **/.env.*
23 | dev.sh
24 | backend/backend_django/django_celery_beat.schedulersDatabaseScheduler
25 |
26 | .harvester/
27 |
28 | *.pptx
29 |
30 | docker-compose.override.yml
31 | backend/backend_django/galv/migrations/*.py
32 | !backend/backend_django/galv/migrations/__init__.py
33 |
34 | docs/build/
35 |
36 | .certs/
37 |
38 | .vhost/
39 |
40 | .static_files/
41 |
42 | node_modules/
43 |
44 | .DS_Store
45 |
--------------------------------------------------------------------------------
/.worker.env:
--------------------------------------------------------------------------------
1 | # this is the base directory for the harvesters run by the server (note harvesters can
2 | # also be setup independently from the server if required, see documentation for
3 | # details). New directories added for scanning will be relative to this base directory
4 | GALV_HARVESTER_BASE_PATH=/home/mrobins/git/tmp/datafiles
5 |
6 | RABBITMQ_URL=pyamqp://test-user:test-user@rabbitmq:5672
7 | REDIS_URL=redis://:redis@redis:6379
8 | CELERY_WORKER_LOG_DIR=/home/mrobins/git/tmp/data/celery
9 |
10 | HARVESTER_USERNAME=harv
11 | HARVESTER_PASSWORD=harv
12 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
2 | of Oxford, and the 'Galv' Developers. All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 🚨Galv has moved🚨
4 |
5 | Galv has moved to separate repositories for the [backend](https://github.com/Battery-Intelligence-Lab/galv-backend), [frontend](https://github.com/Battery-Intelligence-Lab/galv-frontend), and [harvester](https://github.com/Battery-Intelligence-Lab/galv-harvester).
6 | Future changes will be made to those repositories rather than this one.
7 | Please target issues at the relevant one of those repositories.
8 |
9 | Galv v2 has a rewritten structure providing more flexibility, better interlinking of resources, and a more streamlined codebase.
10 |
11 | # Readme
12 |
13 | [](https://github.com/Battery-Intelligence-Lab/galv/actions/workflows/unit-test.yml)
14 | [](https://battery-intelligence-lab.github.io/galv/index.html)
15 |
16 | [](https://github.com/Battery-Intelligence-Lab/galv/actions/workflows/pages/pages-build-deployment)
17 |
18 |
19 | Galv is an open-source platform for automated storage of battery data with advanced metadata support for battery scientists. Galv is deployed with [Docker](https://docs.docker.com/) to support robust local and cloud instances. An example frontend view is displayed below.
20 |
21 |
22 |
23 |
24 |
25 | ## Features:
26 | - REST API for easy data storage and retrieval
27 | - A Python, Julia, and MATLAB client for the REST API
28 | - Metadata support using ontology definitions from BattINFO/EMMO
29 | - A distributed platform with local data harvesters
30 | - Docker based deployment
31 |
32 |
33 | ## Getting Started
34 | Deploying a [Galv](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#galv-server) instance in a battery lab can make it easy to access, analyse, and share experimental data. The steps to achieve this are:
35 | 1. Set the cycler's data export/save location to a single directory.
36 |
37 | 2. Set up a [harvester](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#harvesters) on a computer with access to the directory (can be local or via a shared directory/drive).
38 |
39 | 3. Deploy Galv onto a local machine, or onto a cloud instance. Selection between these depend on security and access requirements. Note, network connection between the harvester and the Galv instance is necessary.
40 |
41 | 4. Log into the lab [web frontend](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#web-frontend) and configure the [harvester](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#harvesters) to crawl the appropriate directories.
42 |
43 | 5. Use the [web frontend](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#web-frontend) to add metadata and view data, or use the [Python](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#python-client) client to ingest the data for analysis.
44 |
45 | The harvesters are able to parse the following file types:
46 |
47 | - MACCOR files in ``.txt``, ``.xsl``/``.xslx``, or ``raw`` format
48 | - Ivium files in ``.idf`` format
49 | - Biologic files in ``.mpr`` format (EC-Lab firmware < 11.50)
50 |
51 | Galv uses a relational database that stores each dataset along with information about column types, units, and other relevant metadata (e.g. cell information, owner, purpose of the experiment). The [REST API](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#rest-api) provides its own definition via a downloadable OpenAPI schema file (`schema/`), and provides interactive documentation via SwaggerUI (`schema/swagger-ui/`) and Redoc (`schema/redoc/`).
52 |
53 | The schema can be downloaded from the [documentation page](https://Battery-Intelligence-Lab.github.io/galv/UserGuide.html#api-spec). The below diagram presents an overview of Galv's architecture. The arrows indicate the direction of data flow.
54 |
55 |
56 |
57 |
58 |
59 |
60 | ## Project documentation
61 |
62 | Full documentation is available [here](https://Battery-Intelligence-Lab.github.io/galv/), build by Sphinx from `./docs/source/*.rst`.
63 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | FROM python:3.10.4-slim@sha256:a2e8240faa44748fe18c5b37f83e14101a38dd3f4a1425d18e9e0e913f89b562
6 |
7 | ENV PYTHONDONTWRITEBYTECODE=1
8 | ENV PYTHONUNBUFFERED=1
9 |
10 | # Install postgresql-client for healthchecking
11 | RUN apt-get update && \
12 | apt-get install -y \
13 | postgresql-client \
14 | build-essential libssl-dev libffi-dev python3-dev python-dev && \
15 | apt-get autoremove && \
16 | apt-get autoclean
17 |
18 | RUN mkdir -p /usr/app
19 | WORKDIR /usr/app
20 | COPY requirements.txt /requirements.txt
21 | RUN pip install -r /requirements.txt
22 | COPY . /usr/app
23 | RUN cp -rf /usr/local/lib/python3.10/site-packages/rest_framework/static/* /static
24 |
--------------------------------------------------------------------------------
/backend/Dockerfile-test:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | FROM python:3.10.4-slim@sha256:a2e8240faa44748fe18c5b37f83e14101a38dd3f4a1425d18e9e0e913f89b562
6 |
7 | ENV PYTHONDONTWRITEBYTECODE=1
8 | ENV PYTHONUNBUFFERED=1
9 | ENV DJANGO_TEST=TRUE
10 |
11 | # Install postgresql-client for healthchecking
12 | RUN apt-get update && \
13 | apt-get install -y \
14 | postgresql-client \
15 | build-essential libssl-dev libffi-dev python3-dev python-dev && \
16 | apt-get autoremove && \
17 | apt-get autoclean
18 |
19 | RUN mkdir -p /usr/app
20 | WORKDIR /usr/app
21 | COPY requirements.txt /requirements.txt
22 | RUN pip install -r /requirements.txt
23 | COPY requirements-test.txt /requirements-test.txt
24 | RUN pip install -r /requirements-test.txt
25 | COPY . /usr/app
26 |
--------------------------------------------------------------------------------
/backend/backend_django/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/config/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import galv.schema
6 |
--------------------------------------------------------------------------------
/backend/backend_django/config/asgi.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | """
6 | ASGI config for backend_django project.
7 |
8 | It exposes the ASGI callable as a module-level variable named ``application``.
9 |
10 | For more information on this path, see
11 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
12 | """
13 |
14 | import os
15 |
16 | from django.core.asgi import get_asgi_application
17 |
18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
19 |
20 | application = get_asgi_application()
21 |
--------------------------------------------------------------------------------
/backend/backend_django/config/settings.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import os
6 |
7 | if 'DJANGO_SETTINGS' in os.environ and os.environ['DJANGO_SETTINGS'] == "dev":
8 | from .settings_dev import *
9 | else:
10 | from .settings_prod import *
11 |
--------------------------------------------------------------------------------
/backend/backend_django/config/urls.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | """backend_django URL Configuration
6 |
7 | The `urlpatterns` list routes URLs to views. For more information please see:
8 | https://docs.djangoproject.com/en/4.1/topics/http/urls/
9 | Examples:
10 | Function views
11 | 1. Add an import: from my_app import views
12 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
13 | Class-based views
14 | 1. Add an import: from other_app.views import Home
15 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
16 | Including another URLconf
17 | 1. Import the include() function: from django.urls import include, path
18 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
19 | """
20 | from django.contrib import admin
21 | from django.urls import include, path
22 | from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
23 | from rest_framework import routers
24 | from galv import views
25 |
26 | router = routers.DefaultRouter()
27 |
28 | router.register(r'harvesters', views.HarvesterViewSet)
29 | router.register(r'harvest_errors', views.HarvestErrorViewSet)
30 | router.register(r'monitored_paths', views.MonitoredPathViewSet)
31 | router.register(r'files', views.ObservedFileViewSet)
32 | router.register(r'datasets', views.DatasetViewSet)
33 | router.register(r'columns', views.DataColumnViewSet)
34 | router.register(r'column_types', views.DataColumnTypeViewSet)
35 | router.register(r'units', views.DataUnitViewSet)
36 | router.register(r'equipment', views.EquipmentViewSet)
37 | router.register(r'cell_families', views.CellFamilyViewSet)
38 | router.register(r'cells', views.CellViewSet)
39 | router.register(r'inactive_users', views.InactiveViewSet, basename='inactive_user')
40 | router.register(r'users', views.UserViewSet, basename='user')
41 | router.register(r'groups', views.GroupViewSet)
42 | router.register(r'tokens', views.TokenViewSet, basename='tokens')
43 |
44 | # Wire up our API using automatic URL routing.
45 | # Additionally, we include login URLs for the browsable API.
46 | urlpatterns = [
47 | path('', include(router.urls)),
48 | # path('data/{pk}/', views.TimeseriesDataViewSet.as_view({'get': 'detail'}), name='timeseriesdata-detail'),
49 | path('admin/', admin.site.urls),
50 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
51 | path(r'login/', views.LoginView.as_view(), name='knox_login'),
52 | path(r'logout/', views.LogoutView.as_view(), name='knox_logout'),
53 | path(r'logoutall/', views.LogoutAllView.as_view(), name='knox_logoutall'),
54 | path(r'create_token/', views.CreateTokenView.as_view(), name='knox_create_token'),
55 | path('schema/', SpectacularAPIView.as_view(), name='schema'),
56 | path('schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
57 | path('schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
58 | ]
59 | pass
60 |
--------------------------------------------------------------------------------
/backend/backend_django/config/wsgi.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | """
6 | WSGI config for backend_django project.
7 |
8 | It exposes the WSGI callable as a module-level variable named ``application``.
9 |
10 | For more information on this path, see
11 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
12 | """
13 |
14 | import os
15 |
16 | from django.core.wsgi import get_wsgi_application
17 |
18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
19 |
20 | application = get_wsgi_application()
21 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/galv/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/galv/admin.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from django.contrib import admin
6 |
7 | # Register your models here.
8 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/apps.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from django.apps import AppConfig
6 |
7 |
8 | class GalvConfig(AppConfig):
9 | default_auto_field = 'django.db.models.BigAutoField'
10 | name = 'galv'
11 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/fixtures/DataColumnType.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "pk": 1,
4 | "model": "galv.DataColumnType",
5 | "fields": {
6 | "name": "Unknown",
7 | "description": "unknown column type",
8 | "is_default": true
9 | }
10 | },
11 | {
12 | "pk": 2,
13 | "model": "galv.DataColumnType",
14 | "fields": {
15 | "name": "Sample Number",
16 | "description": "The sample or record number. Is increased by one each time a test machine records a reading. Usually counts from 1 at the start of a test",
17 | "unit": 1,
18 | "is_default": true
19 | }
20 | },
21 | {
22 | "pk": 3,
23 | "model": "galv.DataColumnType",
24 | "fields": {
25 | "name": "Time",
26 | "description": "The time in seconds since the test run began.",
27 | "unit": 2,
28 | "is_default": true
29 | }
30 | },
31 | {
32 | "pk": 4,
33 | "model": "galv.DataColumnType",
34 | "fields": {
35 | "name": "Volts",
36 | "description": "The voltage of the cell.",
37 | "unit": 3,
38 | "is_default": true
39 | }
40 | },
41 | {
42 | "pk": 5,
43 | "model": "galv.DataColumnType",
44 | "fields": {
45 | "name": "Amps",
46 | "description": "The current current.",
47 | "unit": 4,
48 | "is_default": true
49 | }
50 | },
51 | {
52 | "pk": 6,
53 | "model": "galv.DataColumnType",
54 | "fields": {
55 | "name": "Energy Capacity",
56 | "description": "The Energy Capacity.",
57 | "unit": 5,
58 | "is_default": true
59 | }
60 | },
61 | {
62 | "pk": 7,
63 | "model": "galv.DataColumnType",
64 | "fields": {
65 | "name": "Charge Capacity",
66 | "description": "The Charge Capacity.",
67 | "unit": 6,
68 | "is_default": true
69 | }
70 | },
71 | {
72 | "pk": 8,
73 | "model": "galv.DataColumnType",
74 | "fields": {
75 | "name": "Temperature",
76 | "description": "The temperature.",
77 | "unit": 7,
78 | "is_default": true
79 | }
80 | },
81 | {
82 | "pk": 9,
83 | "model": "galv.DataColumnType",
84 | "fields": {
85 | "name": "Step Time",
86 | "description": "The time in seconds since the current step began.",
87 | "unit": 8,
88 | "is_default": true
89 | }
90 | },
91 | {
92 | "pk": 10,
93 | "model": "galv.DataColumnType",
94 | "fields": {
95 | "name": "Impedence Magnitude",
96 | "description": "The magnitude of the impedence (EIS).",
97 | "unit": 9,
98 | "is_default": true
99 | }
100 | },
101 | {
102 | "pk": 11,
103 | "model": "galv.DataColumnType",
104 | "fields": {
105 | "name": "Impedence Phase",
106 | "description": "The phase of the impedence (EIS).",
107 | "unit": 10,
108 | "is_default": true
109 | }
110 | },
111 | {
112 | "pk": 12,
113 | "model": "galv.DataColumnType",
114 | "fields": {
115 | "name": "Frequency",
116 | "description": "The frequency of the input EIS voltage signal.",
117 | "unit": 11,
118 | "is_default": true
119 | }
120 | }
121 | ]
--------------------------------------------------------------------------------
/backend/backend_django/galv/fixtures/DataUnit.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "pk": 1,
4 | "model": "galv.DataUnit",
5 | "fields": {
6 | "name": "Unitless",
7 | "symbol": "",
8 | "description": "A value with no units",
9 | "is_default": true
10 | }
11 | },
12 | {
13 | "pk": 2,
14 | "model": "galv.DataUnit",
15 | "fields": {
16 | "name": "Time",
17 | "symbol": "s",
18 | "description": "Time in seconds",
19 | "is_default": true
20 | }
21 | },
22 | {
23 | "pk": 3,
24 | "model": "galv.DataUnit",
25 | "fields": {
26 | "name": "Volts",
27 | "symbol": "V",
28 | "description": "Voltage",
29 | "is_default": true
30 | }
31 | },
32 | {
33 | "pk": 4,
34 | "model": "galv.DataUnit",
35 | "fields": {
36 | "name": "Amps",
37 | "symbol": "A",
38 | "description": "Current",
39 | "is_default": true
40 | }
41 | },
42 | {
43 | "pk": 5,
44 | "model": "galv.DataUnit",
45 | "fields": {
46 | "name": "Energy",
47 | "symbol": "Wh",
48 | "description": "Energy in Watt-Hours",
49 | "is_default": true
50 | }
51 | },
52 | {
53 | "pk": 6,
54 | "model": "galv.DataUnit",
55 | "fields": {
56 | "name": "Charge",
57 | "symbol": "Ah",
58 | "description": "Charge in Amp-Hours",
59 | "is_default": true
60 | }
61 | },
62 | {
63 | "pk": 7,
64 | "model": "galv.DataUnit",
65 | "fields": {
66 | "name": "Temperature",
67 | "symbol": "°c",
68 | "description": "Temperature in Centigrade",
69 | "is_default": true
70 | }
71 | },
72 | {
73 | "pk": 8,
74 | "model": "galv.DataUnit",
75 | "fields": {
76 | "name": "Power",
77 | "symbol": "W",
78 | "description": "Power in Watts",
79 | "is_default": true
80 | }
81 | },
82 | {
83 | "pk": 9,
84 | "model": "galv.DataUnit",
85 | "fields": {
86 | "name": "Ohm",
87 | "symbol": "Ω",
88 | "description": "Resistance or impediance in Ohms",
89 | "is_default": true
90 | }
91 | },
92 | {
93 | "pk": 10,
94 | "model": "galv.DataUnit",
95 | "fields": {
96 | "name": "Degrees",
97 | "symbol": "°",
98 | "description": "Angle in degrees",
99 | "is_default": true
100 | }
101 | },
102 | {
103 | "pk": 11,
104 | "model": "galv.DataUnit",
105 | "fields": {
106 | "name": "Frequency",
107 | "symbol": "Hz",
108 | "description": "Frequency in Hz",
109 | "is_default": true
110 | }
111 | }
112 | ]
--------------------------------------------------------------------------------
/backend/backend_django/galv/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/galv/management/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/galv/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/galv/management/commands/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/galv/management/commands/create_superuser.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from django.core.management.base import BaseCommand
6 | from django.contrib.auth.models import User
7 | import os
8 |
9 |
10 | class Command(BaseCommand):
11 | help = """
12 | Create superuser with login details from envvars
13 | DJANGO_SUPERUSER_USERNAME (default=admin),
14 | DJANGO_SUPERUSER_PASSWORD (required)
15 | """
16 |
17 | def handle(self, *args, **options):
18 | password = os.getenv('DJANGO_SUPERUSER_PASSWORD', "")
19 | if not len(password):
20 | self.stdout.write(self.style.WARNING(
21 | 'No DJANGO_SUPERUSER_PASSWORD specified, skipping superuser creation.'
22 | ))
23 | return
24 | username = os.getenv('DJANGO_SUPERUSER_USERNAME', 'admin')
25 | if User.objects.filter(username=username).exists():
26 | self.stdout.write(self.style.WARNING(
27 | f'User {username} already exists: skipping user creation.'
28 | ))
29 | return
30 | User.objects.create_user(
31 | username=username,
32 | password=password,
33 | is_superuser=True,
34 | is_staff=True,
35 | is_active=True
36 | )
37 | self.stdout.write(self.style.SUCCESS(f'Created superuser {username}.'))
38 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/management/commands/init_db.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from django.core.management.base import BaseCommand
6 | from django.db import connection
7 |
8 |
9 | class Command(BaseCommand):
10 | help = "Create timeseries_data table in database."
11 |
12 | def handle(self, *args, **options):
13 | self.stdout.write("Creating timeseries_data table... ")
14 | with connection.cursor() as curs:
15 | curs.execute("""
16 | CREATE TABLE IF NOT EXISTS timeseries_data (
17 | sample bigint NOT NULL,
18 | column_id bigint NOT NULL,
19 | value double precision NOT NULL,
20 | PRIMARY KEY (sample, column_id),
21 | FOREIGN KEY (column_id)
22 | REFERENCES "galv_datacolumn" (id) MATCH SIMPLE
23 | ON UPDATE CASCADE
24 | ON DELETE RESTRICT
25 | ) WITH (OIDS = FALSE)
26 | """)
27 | self.stdout.write(self.style.SUCCESS('Complete.'))
28 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/galv/migrations/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/galv/pagination.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from rest_framework.pagination import PageNumberPagination, BasePagination
6 |
7 |
8 | class Unpaginatable(BasePagination):
9 | def paginate_queryset(self, queryset, request, view=None):
10 | # s: str = request.query_params.get('all', "")
11 | # falsy = ['false', 'f', '0', 'no', 'n']
12 | # if s.lower() in falsy or request.query_params.get('page', None):
13 | # page_num = PageNumberPagination
14 | # return page_num.paginate_queryset(
15 | # PageNumberPagination(),
16 | # queryset=queryset,
17 | # request=request,
18 | # view=view
19 | # )
20 |
21 | return None
22 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/permissions.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 | from django.http import Http404
5 | from django.urls import resolve, Resolver404
6 | from urllib.parse import urlparse
7 | from django.db.models import Q
8 | from rest_framework import permissions
9 | from .models import Harvester, MonitoredPath
10 |
11 |
12 | class HarvesterAccess(permissions.BasePermission):
13 | """
14 | Object-level permission to only allow Harvester to edit its own attributes.
15 | """
16 | message = "Invalid AUTHORIZATION header. Required 'Harvester [api_key]' or 'Bearer [api_token]'."
17 |
18 | endpoints = ['config', 'report']
19 |
20 | def has_object_permission(self, request, view, obj):
21 | if view.action in self.endpoints:
22 | return obj.id == int(view.kwargs['pk']) and \
23 | request.META.get('HTTP_AUTHORIZATION', '') == f"Harvester {obj.api_key}"
24 | # /harvesters/ returns all harvesters because their envvars are redacted
25 | if view.action == 'list' and request.method == 'GET':
26 | return True
27 | # Read/write detail test
28 | user_groups = request.user.groups.all()
29 | # Allow access to Harvesters where we have a Path
30 | path_harvesters = [p.harvester.id for p in MonitoredPath.objects.filter(
31 | Q(user_group__in=user_groups) | Q(admin_group__in=user_groups)
32 | )]
33 | user_harvesters = Harvester.objects.filter(
34 | Q(user_group__in=user_groups) |
35 | Q(id__in=path_harvesters)
36 | )
37 | admin_harvesters = Harvester.objects.filter(admin_group__in=user_groups)
38 | if request.method in permissions.SAFE_METHODS:
39 | return obj in user_harvesters or obj in admin_harvesters
40 | return obj in admin_harvesters
41 |
42 |
43 | class MonitoredPathAccess(permissions.BasePermission):
44 | """
45 | MonitoredPaths can be read by users in the user_group or admin_group.
46 | MonitoredPaths can be edited by users in the admin_group.
47 |
48 | MonitoredPaths can be created by users in the harvester's user_group and admin_group.
49 | """
50 | def has_object_permission(self, request, view, obj):
51 | user_groups = request.user.groups.all()
52 | if request.method in permissions.SAFE_METHODS:
53 | return obj.user_group in user_groups or obj.admin_group in user_groups
54 | return obj.admin_group in user_groups
55 |
56 | def has_permission(self, request, view):
57 | if view.action == 'create':
58 | user_groups = request.user.groups.all()
59 | try:
60 | harvester_id = resolve(urlparse(request.data.get('harvester')).path).kwargs.get('pk')
61 | except Resolver404:
62 | raise Http404(f"Invalid harvester URL '{request.data.get('harvester')}'")
63 | harvester = Harvester.objects.get(id=harvester_id)
64 | return harvester.user_group in user_groups or harvester.admin_group in user_groups
65 | return True
66 |
67 |
68 | class ReadOnlyIfInUse(permissions.BasePermission):
69 | def has_object_permission(self, request, view, obj):
70 | if request.method in permissions.SAFE_METHODS:
71 | return True
72 | return not obj.in_use()
73 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/schema.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | from drf_spectacular.extensions import OpenApiAuthenticationExtension
6 | from knox.settings import knox_settings
7 |
8 | class KnoxTokenScheme(OpenApiAuthenticationExtension):
9 | target_class = 'knox.auth.TokenAuthentication'
10 | name = 'knoxTokenAuth'
11 | match_subclasses = True
12 | priority = 1
13 |
14 | def get_security_definition(self, auto_schema):
15 | if knox_settings.AUTH_HEADER_PREFIX == 'Bearer':
16 | return {
17 | 'type': 'http',
18 | 'scheme': 'bearer',
19 | }
20 | else:
21 | return {
22 | 'type': 'apiKey',
23 | 'in': 'header',
24 | 'name': 'Authorization',
25 | 'description': 'Token-based authentication with required prefix "%s"' % knox_settings.AUTH_HEADER_PREFIX
26 | }
27 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/backend/backend_django/galv/tests/__init__.py
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/factories.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import os
6 |
7 | import factory
8 | import faker
9 | import django.conf.global_settings
10 | from django.utils import timezone
11 | from galv.models import Harvester, \
12 | HarvestError, \
13 | MonitoredPath, \
14 | ObservedFile, \
15 | Cell, \
16 | CellFamily, \
17 | Dataset, \
18 | Equipment, \
19 | DataUnit, \
20 | DataColumnType, \
21 | DataColumn, \
22 | TimeseriesRangeLabel, \
23 | FileState
24 | from django.contrib.auth.models import User, Group
25 |
26 | fake = faker.Faker(django.conf.global_settings.LANGUAGE_CODE)
27 |
28 |
29 | class UserFactory(factory.django.DjangoModelFactory):
30 | class Meta:
31 | model = User
32 | django_get_or_create = ('username',)
33 |
34 | username = factory.Faker('user_name')
35 |
36 |
37 | class GroupFactory(factory.django.DjangoModelFactory):
38 | class Meta:
39 | model = Group
40 | django_get_or_create = ('name',)
41 | exclude = ('n',)
42 |
43 | n = factory.Faker('random_int', min=1, max=100000)
44 | name = factory.LazyAttribute(lambda x: f"group_{x.n}")
45 |
46 |
47 | class HarvesterFactory(factory.django.DjangoModelFactory):
48 | class Meta:
49 | model = Harvester
50 | django_get_or_create = ('name',)
51 | exclude = ('first_name',)
52 |
53 | first_name = fake.unique.first_name()
54 | name = factory.LazyAttribute(lambda x: f"Harvester {x.first_name}")
55 |
56 | @factory.post_generation
57 | def groups(obj, *args, **kwargs):
58 | user_group = GroupFactory.create(name=f"harvester_{obj.id}_user_group")
59 | admin_group = GroupFactory.create(name=f"harvester_{obj.id}_admin_group")
60 | obj.user_group = user_group
61 | obj.admin_group = admin_group
62 | obj.save()
63 |
64 |
65 | class MonitoredPathFactory(factory.django.DjangoModelFactory):
66 | class Meta:
67 | model = MonitoredPath
68 | django_get_or_create = ('path', 'harvester',)
69 | exclude = ('p',)
70 |
71 | p = factory.Faker(
72 | 'file_path',
73 | absolute=False,
74 | depth=factory.Faker('random_int', min=1, max=10)
75 | )
76 |
77 | path = factory.LazyAttribute(lambda x: os.path.dirname(x.p))
78 | regex = ".*"
79 | harvester = factory.SubFactory(HarvesterFactory)
80 |
81 | @factory.post_generation
82 | def groups(obj, *args, **kwargs):
83 | user_group = GroupFactory.create(name=f"path_{obj.id}_user_group")
84 | admin_group = GroupFactory.create(name=f"path_{obj.id}_admin_group")
85 | obj.user_group = user_group
86 | obj.admin_group = admin_group
87 | obj.save()
88 |
89 |
90 | class ObservedFileFactory(factory.django.DjangoModelFactory):
91 | class Meta:
92 | model = ObservedFile
93 | django_get_or_create = ('harvester', 'path')
94 |
95 | path = factory.Faker('file_path')
96 | harvester = factory.SubFactory(HarvesterFactory)
97 |
98 |
99 | class DatasetFactory(factory.django.DjangoModelFactory):
100 | class Meta:
101 | model = Dataset
102 | django_get_or_create = ('file', 'date',)
103 |
104 | file = factory.SubFactory(ObservedFileFactory)
105 | date = timezone.make_aware(timezone.datetime.now())
106 |
107 |
108 | class CellFamilyFactory(factory.django.DjangoModelFactory):
109 | class Meta:
110 | model = CellFamily
111 |
112 | name = factory.Faker('catch_phrase')
113 | form_factor = factory.Faker('bs')
114 | link_to_datasheet = factory.Faker('uri')
115 | anode_chemistry = factory.Faker('bs')
116 | cathode_chemistry = factory.Faker('bs')
117 | manufacturer = factory.Faker('company')
118 | nominal_capacity = factory.Faker('pyfloat', min_value=1.0, max_value=1000000.0)
119 | nominal_cell_weight = factory.Faker('pyfloat', min_value=1.0, max_value=1000000.0)
120 |
121 |
122 | class CellFactory(factory.django.DjangoModelFactory):
123 | class Meta:
124 | model = Cell
125 |
126 | uid = factory.Faker('bothify', text='?????-##??#-#?#??-?####-?#???')
127 | display_name = factory.Faker('catch_phrase')
128 | family = factory.SubFactory(CellFamilyFactory)
129 |
130 |
131 | class EquipmentFactory(factory.django.DjangoModelFactory):
132 | class Meta:
133 | model = Equipment
134 |
135 | name = factory.Faker('catch_phrase')
136 | type = factory.Faker('bs')
137 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_api_tokens.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from galv.models import KnoxAuthToken
12 |
13 | from .utils import GalvTestCase
14 | from .factories import UserFactory
15 |
16 | logger = logging.getLogger(__file__)
17 | logger.setLevel(logging.INFO)
18 |
19 |
20 | class TokenTests(GalvTestCase):
21 | def setUp(self):
22 | self.user = UserFactory.create(username='test_user')
23 | self.other_user = UserFactory.create(username='test_user_other')
24 |
25 | def test_crud(self):
26 | self.client.force_login(self.user)
27 | body = {'name': 'Test API token', 'ttl': 600}
28 | url = reverse('knox_create_token')
29 | print("Test create API token")
30 | response = self.client.post(url, body)
31 | self.assertEqual(response.status_code, status.HTTP_200_OK)
32 | self.assertEqual(response.json().get('name'), body['name'])
33 | self.assertIn('token', response.json())
34 | print("OK")
35 |
36 | print("Test list tokens")
37 | url = reverse('tokens-list')
38 | response = self.client.get(url)
39 | self.assertEqual(len(response.json()), 1)
40 | detail_url = response.json()[0]['url']
41 | self.client.force_login(self.other_user)
42 | self.assertEqual(self.client.get(detail_url).status_code, status.HTTP_404_NOT_FOUND)
43 | print("OK")
44 |
45 | print("Test token detail")
46 | self.client.force_login(self.user)
47 | response = self.client.get(detail_url)
48 | self.assertEqual(response.json()['name'], body['name'])
49 | self.assertNotIn('token', response.json())
50 | print("OK")
51 |
52 | print("Test update")
53 | new_name = "new token name"
54 | response = self.client.patch(detail_url, {"name": new_name})
55 | self.assertEqual(response.status_code, status.HTTP_200_OK)
56 | self.assertEqual(response.json()['name'], new_name)
57 | print("OK")
58 |
59 | print("Test token delete")
60 | response = self.client.delete(detail_url)
61 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
62 | self.assertEqual(KnoxAuthToken.objects.filter(knox_token_key__regex=f"_{self.user.id}$").exists(), False)
63 | print("OK")
64 |
65 |
66 | if __name__ == '__main__':
67 | unittest.main()
68 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_view_cells.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from .factories import CellFamilyFactory, CellFactory
12 | from galv.models import CellFamily, Cell
13 |
14 | logger = logging.getLogger(__file__)
15 | logger.setLevel(logging.INFO)
16 |
17 |
18 | class CellFamilyTests(APITestCase):
19 | def test_create(self):
20 | body = {
21 | 'name': 'Test CF', 'form_factor': 'test', 'link_to_datasheet': 'http',
22 | 'anode_chemistry': 'yes','cathode_chemistry': 'yes',
23 | 'nominal_capacity': 5.5, 'nominal_cell_weight': 1.2, 'manufacturer': 'na'
24 | }
25 | url = reverse('cellfamily-list')
26 | print("Test create Cell Family")
27 | response = self.client.post(url, body)
28 | self.assertEqual(response.status_code, status.HTTP_201_CREATED)
29 | family_url = response.json().get('url')
30 | self.assertIsInstance(family_url, str)
31 | print("OK")
32 | print("Test create Cell")
33 | response = self.client.post(
34 | reverse('cell-list'),
35 | {'uid': 'some-unique-id-1234', 'display_name': 'test cell', 'family': family_url}
36 | )
37 | self.assertEqual(response.status_code, status.HTTP_201_CREATED)
38 | print("OK")
39 |
40 | def test_update(self):
41 | cell_family = CellFamilyFactory.create(name='Test family')
42 | url = reverse('cellfamily-detail', args=(cell_family.id,))
43 | print("Test update Cell Family")
44 | response = self.client.patch(url, {'name': 'cell family'})
45 | self.assertEqual(response.status_code, status.HTTP_200_OK)
46 | self.assertEqual(CellFamily.objects.get(id=cell_family.id).name, 'cell family')
47 | print("OK")
48 | print("Test update Cell")
49 | cell = CellFactory.create(family=cell_family)
50 | url = reverse('cell-detail', args=(cell.id,))
51 | response = self.client.patch(url, {'display_name': 'c123'})
52 | self.assertEqual(response.status_code, status.HTTP_200_OK)
53 | self.assertEqual(Cell.objects.get(id=cell.id).display_name, 'c123')
54 | response = self.client.patch(url, {'uid': 'c123'})
55 | self.assertEqual(response.status_code, status.HTTP_200_OK)
56 | self.assertEqual(Cell.objects.get(id=cell.id).uid, 'c123')
57 | print("OK")
58 |
59 |
60 | if __name__ == '__main__':
61 | unittest.main()
62 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_view_datasets.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from .factories import UserFactory, \
12 | HarvesterFactory, \
13 | DatasetFactory, MonitoredPathFactory
14 |
15 | logger = logging.getLogger(__file__)
16 | logger.setLevel(logging.INFO)
17 |
18 |
19 | class DatasetTests(APITestCase):
20 | def setUp(self):
21 | self.harvester = HarvesterFactory.create(name='Test Dataset')
22 | self.dataset = DatasetFactory.create(file__harvester=self.harvester)
23 | self.monitored_path = MonitoredPathFactory.create(harvester=self.harvester, path="/")
24 | self.user = UserFactory.create(username='test_user')
25 | self.admin_user = UserFactory.create(username='test_user_admin')
26 | self.user.groups.add(self.harvester.user_group)
27 | self.admin_user.groups.add(self.monitored_path.admin_group)
28 | self.url = reverse('dataset-detail', args=(self.dataset.id,))
29 |
30 | def test_view(self):
31 | self.client.force_login(self.user)
32 | print("Test rejection of dataset view")
33 | self.assertNotEqual(self.client.get(self.url).status_code, status.HTTP_200_OK)
34 | print("OK")
35 | self.client.force_login(self.admin_user)
36 | print("Test dataset view")
37 | self.assertEqual(self.client.get(self.url).status_code, status.HTTP_200_OK)
38 | print("OK")
39 |
40 |
41 | if __name__ == '__main__':
42 | unittest.main()
43 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_view_equipment.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from .factories import EquipmentFactory
12 | from galv.models import Equipment
13 |
14 | logger = logging.getLogger(__file__)
15 | logger.setLevel(logging.INFO)
16 |
17 |
18 | class EquipmentTests(APITestCase):
19 | def test_create(self):
20 | body = {'name': 'Test EQ', 'type': 'test'}
21 | url = reverse('equipment-list')
22 | print("Test create Equipment")
23 | response = self.client.post(url, body)
24 | self.assertEqual(response.status_code, status.HTTP_201_CREATED)
25 | print("OK")
26 |
27 | def test_update(self):
28 | equipment = EquipmentFactory.create(name='Test kit')
29 | url = reverse('equipment-detail', args=(equipment.id,))
30 | print("Test update Equipment")
31 | response = self.client.patch(url, {'name': 'New kit'})
32 | self.assertEqual(response.status_code, status.HTTP_200_OK)
33 | self.assertEqual(Equipment.objects.get(id=equipment.id).name, 'New kit')
34 | print("OK")
35 |
36 |
37 | if __name__ == '__main__':
38 | unittest.main()
39 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_view_files.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from .factories import UserFactory, \
12 | HarvesterFactory, \
13 | MonitoredPathFactory, \
14 | ObservedFileFactory
15 | from galv.models import ObservedFile, \
16 | FileState
17 |
18 | logger = logging.getLogger(__file__)
19 | logger.setLevel(logging.INFO)
20 |
21 |
22 | class ObservedFileTests(APITestCase):
23 | def setUp(self):
24 | self.harvester = HarvesterFactory.create(name='Test Files')
25 | self.path = MonitoredPathFactory.create(harvester=self.harvester, path="/")
26 | self.files = ObservedFileFactory.create_batch(size=5, harvester=self.harvester)
27 | self.user = UserFactory.create(username='test_user')
28 | self.admin_user = UserFactory.create(username='test_user_admin')
29 | self.user.groups.add(self.harvester.user_group)
30 | self.admin_user.groups.add(self.path.admin_group)
31 | self.url = reverse('observedfile-detail', args=(self.files[0].id,))
32 |
33 | def test_view(self):
34 | self.client.force_login(self.user)
35 | print("Test rejection of view path")
36 | self.assertNotEqual(self.client.get(self.url).status_code, status.HTTP_200_OK)
37 | print("OK")
38 | self.client.force_login(self.admin_user)
39 | print("Test view path")
40 | self.assertEqual(self.client.get(self.url).status_code, status.HTTP_200_OK)
41 | print("OK")
42 |
43 | def test_reimport(self):
44 | self.client.force_login(self.admin_user)
45 | print("Test reimport")
46 | url = reverse('observedfile-reimport', args=(self.files[0].id,))
47 | self.assertEqual(self.client.get(url).status_code, status.HTTP_200_OK)
48 | self.assertEqual(ObservedFile.objects.get(id=self.files[0].id).state, FileState.RETRY_IMPORT)
49 | print("OK")
50 |
51 |
52 | if __name__ == '__main__':
53 | unittest.main()
54 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/test_view_path.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 | import json
5 | import unittest
6 | from django.urls import reverse
7 | from rest_framework import status
8 | from rest_framework.test import APITestCase
9 | import logging
10 |
11 | from .factories import UserFactory, \
12 | HarvesterFactory, \
13 | MonitoredPathFactory
14 | from galv.models import Harvester, \
15 | MonitoredPath
16 |
17 | logger = logging.getLogger(__file__)
18 | logger.setLevel(logging.INFO)
19 |
20 |
21 | class MonitoredPathTests(APITestCase):
22 | def setUp(self):
23 | self.path = '/path/to/data'
24 | self.harvester = HarvesterFactory.create(name='Test Paths')
25 | self.non_user = UserFactory.create(username='test_paths')
26 | self.user = UserFactory.create(username='test_paths_user')
27 | self.user.groups.add(self.harvester.user_group)
28 | self.admin_user = UserFactory.create(username='test_paths_admin')
29 | self.admin_user.groups.add(self.harvester.admin_group)
30 |
31 | def test_create(self):
32 | self.client.force_login(self.non_user)
33 | url = reverse('monitoredpath-list')
34 | print("Test rejection of Create Path - no authorisation")
35 | self.client.force_login(self.user)
36 | body = {
37 | 'path': self.path,
38 | 'regex': '.*',
39 | 'harvester': reverse('harvester-detail', args=(self.harvester.id,)),
40 | 'stable_time': 60
41 | }
42 | self.assertEqual(
43 | self.client.post(url, body).status_code,
44 | status.HTTP_400_BAD_REQUEST
45 | )
46 | print("OK")
47 | print("Test rejection of Create Path - no path")
48 | no_path = {**body}
49 | no_path.pop('path')
50 | self.assertEqual(
51 | self.client.post(url, no_path).status_code,
52 | status.HTTP_400_BAD_REQUEST
53 | )
54 | print("OK")
55 | print("Test rejection of Create Path - invalid harvester")
56 | i = 1 if self.harvester.id != 1 else 2
57 | self.assertEqual(
58 | self.client.post(url, {'harvester': i}).status_code,
59 | status.HTTP_404_NOT_FOUND
60 | )
61 | print("OK")
62 | print("Test successful Path creation")
63 | self.client.force_login(self.admin_user)
64 | self.assertEqual(
65 | self.client.post(url, body).status_code,
66 | status.HTTP_201_CREATED
67 | )
68 | print("OK")
69 | print("Test rejection of duplicate name")
70 | self.assertEqual(
71 | self.client.post(url, body).status_code,
72 | status.HTTP_400_BAD_REQUEST
73 | )
74 | print("OK")
75 |
76 | def test_update(self):
77 | path = MonitoredPathFactory.create(path=self.path, harvester=self.harvester)
78 | self.admin_user.groups.add(path.admin_group)
79 | url = reverse('monitoredpath-detail', args=(path.id,))
80 | print("Test update rejected - authorisation")
81 | self.client.force_login(self.user)
82 | body = {'path': path.path, 'regex': '^abc', 'stable_time': 100}
83 | self.assertEqual(
84 | self.client.patch(url, body).status_code,
85 | status.HTTP_404_NOT_FOUND
86 | )
87 | print("OK")
88 | print("Test update okay")
89 | self.client.force_login(self.admin_user)
90 | body = {'path': path.path, 'regex': '^abc', 'stable_time': 1}
91 | self.assertEqual(
92 | self.client.patch(url, body).status_code,
93 | status.HTTP_200_OK
94 | )
95 | self.assertEqual(
96 | MonitoredPath.objects.get(path=path.path, harvester=self.harvester).stable_time,
97 | body.get('stable_time')
98 | )
99 | self.assertEqual(
100 | MonitoredPath.objects.get(path=path.path, harvester=self.harvester).regex,
101 | body.get('regex')
102 | )
103 | print("OK")
104 |
105 |
106 | if __name__ == '__main__':
107 | unittest.main()
108 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/tests/utils.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import base64
6 |
7 | from django.urls import reverse
8 | from rest_framework.test import APITestCase
9 |
10 |
11 | class GalvTestCase(APITestCase):
12 |
13 | def get_token_header_for_user(self, user):
14 | self.client.force_login(user)
15 | user.set_password('foobar')
16 | user.save()
17 | auth_str = base64.b64encode(bytes(f"{user.username}:foobar", 'utf-8'))
18 | basic_auth = self.client.post(
19 | reverse('knox_login'),
20 | {},
21 | HTTP_AUTHORIZATION=f"Basic {auth_str.decode('utf-8')}"
22 | )
23 | token = basic_auth.json()['token']
24 | return {'HTTP_AUTHORIZATION': f"Bearer {token}"}
25 |
--------------------------------------------------------------------------------
/backend/backend_django/galv/utils.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import os
6 | import re
7 |
8 | from .models import MonitoredPath, Harvester, ObservedFile
9 |
10 |
11 | def get_monitored_paths(path: os.PathLike|str, harvester: Harvester) -> list[MonitoredPath]:
12 | """
13 | Return the MonitoredPaths on this Harvester that match the given path.
14 | MonitoredPaths are matched by path and regex.
15 | """
16 | monitored_paths = MonitoredPath.objects.filter(harvester=harvester)
17 | monitored_paths = [p for p in monitored_paths if os.path.abspath(path).startswith(os.path.abspath(p.path))]
18 | return [p for p in monitored_paths if re.search(p.regex, os.path.relpath(path, p.path))]
19 |
20 |
21 | # TODO: If these lookups are too slow, we could keep track of the monitored_path used
22 | # each time a file is reported on, and use that to lookup files by path directly.
23 | def get_files_from_path(path: MonitoredPath) -> list[ObservedFile]:
24 | """
25 | Return a list of files from the given path that match the MonitoredPath's regex.
26 | """
27 | files = ObservedFile.objects.filter(path__startswith=path.path, harvester=path.harvester)
28 | if not path.regex:
29 | out = files
30 | else:
31 | regex = re.compile(path.regex)
32 | out = [p for p in files if re.search(regex, os.path.relpath(p.path, path.path))]
33 | return out
34 |
--------------------------------------------------------------------------------
/backend/backend_django/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # SPDX-License-Identifier: BSD-2-Clause
4 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
5 | # of Oxford, and the 'Galv' Developers. All rights reserved.
6 |
7 | """Django's command-line utility for administrative tasks."""
8 | import os
9 | import sys
10 |
11 |
12 | def main():
13 | """Run administrative tasks."""
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
15 | try:
16 | from django.core.management import execute_from_command_line
17 | except ImportError as exc:
18 | raise ImportError(
19 | "Couldn't import Django. Are you sure it's installed and "
20 | "available on your PYTHONPATH environment variable? Did you "
21 | "forget to activate a virtual environment?"
22 | ) from exc
23 | execute_from_command_line(sys.argv)
24 |
25 |
26 | if __name__ == '__main__':
27 | main()
28 |
--------------------------------------------------------------------------------
/backend/requirements-test.txt:
--------------------------------------------------------------------------------
1 | factory_boy==3.2.1
2 | faker==17.0.0
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==4.1.4
2 | django-cors-headers==3.13.0
3 | django-filter==22.1
4 | djangorestframework==3.14.0
5 | django-rest-knox==4.2.0
6 | psycopg2-binary==2.9.5
7 | redis==4.4.0
8 | drf-spectacular==0.25.1
9 | markdown==3.4.1
10 | gunicorn==20.1.0
11 |
--------------------------------------------------------------------------------
/backend/server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SPDX-License-Identifier: BSD-2-Clause
3 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
4 | # of Oxford, and the 'Galv' Developers. All rights reserved.
5 |
6 | # init.sh
7 |
8 | # adapted from https://docs.docker.com/compose/startup-order/
9 |
10 | set -e
11 | PGUP=1
12 |
13 | cd backend_django || exit 1
14 |
15 | >&2 echo "Collecting static files"
16 | python manage.py collectstatic --noinput
17 |
18 | >&2 echo "Waiting for Postgres to start"
19 |
20 | while [ $PGUP -ne 0 ]; do
21 | pg_isready -d "postgresql://postgres:galv@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/postgres"
22 | PGUP=$?
23 | >&2 echo "Postgres is unavailable - sleeping"
24 | sleep 1
25 | done
26 |
27 | >&2 echo "Postgres ready - initialising"
28 | >&2 echo "DJANGO_TEST=${DJANGO_TEST}"
29 | >&2 echo "DJANGO_SETTINGS=${DJANGO_SETTINGS}"
30 | python manage.py makemigrations
31 | python manage.py migrate
32 | python manage.py init_db
33 | python manage.py create_superuser
34 |
35 | >&2 echo "... populating database"
36 | python manage.py loaddata galv/fixtures/DataUnit.json
37 | python manage.py loaddata galv/fixtures/DataColumnType.json
38 |
39 | >&2 echo "Initialisation complete - starting server"
40 |
41 | if [ -z "${DJANGO_TEST}" ]; then
42 | if [ "${DJANGO_SETTINGS}" = "dev" ]; then
43 | >&2 echo "Launching dev server"
44 | python manage.py runserver 0.0.0.0:80
45 | else
46 | WORKERS_PER_CPU=${GUNICORN_WORKERS_PER_CPU:-2}
47 | WORKERS=$(expr $WORKERS_PER_CPU \* $(grep -c ^processor /proc/cpuinfo))
48 | >&2 echo "Launching production server with gunicorn ($WORKERS workers [${WORKERS_PER_CPU} per CPU])"
49 | gunicorn config.wsgi \
50 | --env DJANGO_SETTINGS_MODULE=config.settings \
51 | --bind 0.0.0.0:80 \
52 | --access-logfile - \
53 | --error-logfile - \
54 | --workers=$WORKERS
55 | fi
56 | else
57 | python manage.py test --noinput
58 | fi
59 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | version: "2"
6 | services:
7 | harvester:
8 | build:
9 | dockerfile: Dockerfile
10 | context: ./harvester
11 | depends_on:
12 | - app
13 | volumes:
14 | - ./harvester:/usr/harvester
15 | - ./.harvester:/harvester_files
16 | - "${GALV_HARVESTER_TEST_PATH}:/usr/test_data"
17 | working_dir: /usr/harvester
18 | restart: unless-stopped
19 | command: python start.py --name "harvey" --url http://app/ --user_id 1 --run_foreground --restart
20 | # command: tail -F anything
21 |
22 | frontend:
23 | image: frontend_dev
24 | build:
25 | dockerfile: Dockerfile_dev
26 | context: ./frontend
27 | args:
28 | FORCE_HTTP: "true"
29 | volumes:
30 | - ./frontend:/app
31 | working_dir: /app
32 | command: >
33 | bash -c "
34 | yarn install &&
35 | yarn start -p 80
36 | "
37 | restart: unless-stopped
38 |
39 | app:
40 | volumes:
41 | - ./backend:/usr/app
42 | environment:
43 | DJANGO_SETTINGS: "dev"
44 |
45 | postgres:
46 | ports:
47 | - "5432:5432"
48 |
49 | nginx-proxy-acme-companion:
50 | restart: "no"
51 | entrypoint: [ "echo", "Service nginx-proxy-acme-companion disabled in development mode" ]
52 |
53 | # old_worker:
54 | # build:
55 | # dockerfile: Dockerfile
56 | # context: ./backend
57 | # command: python manage.py test
58 | # depends_on:
59 | # - postgres
60 | # volumes:
61 | # - ./backend:/usr/app
62 | # - "${GALV_HARVESTER_BASE_PATH}:/usr/data"
63 | # - "${GALV_HARVESTER_TEST_PATH}:/usr/test_data"
64 | # - "${CELERY_LOG_DIR}:/var/log/celery"
65 | # working_dir: /usr/app
66 | # env_file:
67 | # - ./.env
68 |
--------------------------------------------------------------------------------
/docker-compose.docs.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | # Docker-compose file for the containers that generate documentation artefacts.
6 | # Should allow generation of
7 | # - entity relationship diagram
8 | # - API schema
9 | # - API client libraries
10 | version: "2"
11 | services:
12 | app:
13 | image: app
14 | build:
15 | dockerfile: Dockerfile
16 | context: backend
17 | depends_on:
18 | - postgres
19 | expose:
20 | - 80
21 | volumes:
22 | # local volume allows us to copy generated ER diagram easily
23 | - ./backend:/usr/app
24 | working_dir: /usr/app/backend_django
25 | env_file:
26 | - ./.env
27 | - ./.env.secret
28 | environment:
29 | FRONTEND_VIRTUAL_HOST: "http://localhost"
30 | restart: unless-stopped
31 | command: ./server.sh
32 |
33 | postgres:
34 | image: "postgres"
35 | stop_signal: SIGINT # Fast Shutdown mode
36 | volumes:
37 | - "${GALV_DATA_PATH}:/var/lib/postgresql/data"
38 | env_file:
39 | - ./.env
40 | - ./.env.secret
41 | restart: unless-stopped
42 |
--------------------------------------------------------------------------------
/docker-compose.harvester.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | version: "2"
6 | services:
7 | harvester:
8 | build:
9 | dockerfile: Dockerfile
10 | context: ./harvester
11 | volumes:
12 | - ./harvester:/usr/harvester
13 | - ./.harvester:/harvester_files
14 | working_dir: /usr/harvester
15 | restart: unless-stopped
16 | stdin_open: true
17 | tty: true
18 | command: python start.py
--------------------------------------------------------------------------------
/docker-compose.test.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | version: "2"
6 | services:
7 |
8 | postgres_test:
9 | image: "postgres"
10 | stop_signal: SIGINT # Fast Shutdown mode
11 | volumes:
12 | - "${GALV_DATA_PATH}:/var/lib/postgresql/data"
13 | env_file:
14 | - .env.secret
15 | restart: unless-stopped
16 |
17 | app_test:
18 | build:
19 | dockerfile: Dockerfile-test
20 | context: backend
21 | depends_on:
22 | - postgres_test
23 | ports:
24 | - "5005:5005"
25 | volumes:
26 | - ./backend:/usr/app
27 | restart: unless-stopped
28 | working_dir: /usr/app/backend_django
29 | command: ../server.sh
30 | # command: tail -F anything
31 | env_file:
32 | - .env.secret
33 | environment:
34 | POSTGRES_PASSWORD: "galv"
35 | DJANGO_SECRET_KEY: "long-and-insecure-key-12345"
36 | FRONTEND_VIRTUAL_HOST: "http://localhost"
37 | VIRTUAL_HOST: "localhost"
38 | POSTGRES_HOST: "postgres_test"
39 |
40 | harvester_test:
41 | build:
42 | dockerfile: Dockerfile
43 | context: ./harvester
44 | volumes:
45 | - ./harvester:/usr/harvester
46 | - ./.harvester:/harvester_files
47 | - "${GALV_HARVESTER_TEST_PATH}:/usr/test_data"
48 | working_dir: /usr
49 | command: python -m unittest discover -s /usr/harvester/test
50 |
51 | frontend_test:
52 | build:
53 | dockerfile: Dockerfile_dev
54 | context: ./frontend
55 | ports:
56 | - "3000:3000"
57 | volumes:
58 | - ./frontend:/app
59 | working_dir: /app
60 | command: >
61 | bash -c "yarn start"
62 | env_file:
63 | - ./.env
64 | restart: "no"
65 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | # docker-compose automatically loads contents of .env so we can refer to those here
6 |
7 | version: "2"
8 | services:
9 | app:
10 | image: app
11 | build:
12 | dockerfile: Dockerfile
13 | context: backend
14 | depends_on:
15 | - postgres
16 | volumes:
17 | - ./.static_files:/static
18 | working_dir: /usr/app
19 | environment:
20 | VIRTUAL_HOST: "api.${VIRTUAL_HOST_ROOT}"
21 | LETSENCRYPT_HOST: "api.${VIRTUAL_HOST_ROOT}"
22 | FRONTEND_VIRTUAL_HOST: "http://${VIRTUAL_HOST_ROOT},https://${VIRTUAL_HOST_ROOT}"
23 | env_file:
24 | - ./.env
25 | - ./.env.secret
26 | restart: unless-stopped
27 | command: ./server.sh
28 |
29 | frontend:
30 | image: frontend
31 | build:
32 | dockerfile: Dockerfile
33 | context: ./frontend
34 | args:
35 | VIRTUAL_HOST_ROOT: "${VIRTUAL_HOST_ROOT}" # Required to inject API root into conf.json
36 | depends_on:
37 | - app
38 | environment:
39 | VIRTUAL_HOST: "${VIRTUAL_HOST_ROOT}"
40 | LETSENCRYPT_HOST: "${VIRTUAL_HOST_ROOT}"
41 | env_file:
42 | - ./.env
43 | restart: unless-stopped
44 |
45 | postgres:
46 | image: "postgres:14"
47 | stop_signal: SIGINT # Fast Shutdown mode
48 | volumes:
49 | - "${GALV_DATA_PATH}:/var/lib/postgresql/data"
50 | env_file:
51 | - ./.env
52 | - ./.env.secret
53 | restart: unless-stopped
54 |
55 | nginx-proxy:
56 | build: nginx-proxy
57 | container_name: nginx-proxy
58 | restart: always
59 | ports:
60 | - "443:443"
61 | - "80:80"
62 | environment:
63 | TRUST_DOWNSTREAM_PROXY: "true"
64 | DEFAULT_HOST: "${VIRTUAL_HOST_ROOT}"
65 | volumes:
66 | - ./.static_files:/app/static
67 | - ./.certs:/etc/nginx/certs
68 | - ./.html:/usr/share/nginx/html
69 | - vhost:/etc/nginx/vhost.d
70 | - /var/run/docker.sock:/tmp/docker.sock:ro
71 | depends_on:
72 | - app
73 | - frontend
74 |
75 | nginx-proxy-acme-companion:
76 | image: nginxproxy/acme-companion
77 | env_file:
78 | - .env
79 | environment:
80 | NGINX_PROXY_CONTAINER: "nginx-proxy"
81 | volumes:
82 | - /var/run/docker.sock:/var/run/docker.sock:ro
83 | - ./.certs:/etc/nginx/certs
84 | - ./.html:/usr/share/nginx/html
85 | - vhost:/etc/nginx/vhost.d
86 | - acme:/etc/acme.sh
87 | depends_on:
88 | - nginx-proxy
89 |
90 | volumes:
91 | # certs:
92 | # html:
93 | vhost:
94 | acme:
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/README.txt:
--------------------------------------------------------------------------------
1 | Documentation is done with sphinx.
2 |
3 | Run documentation build from this directory with
4 | make html
5 |
6 | Results appear in ./build/html
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = 'Galv'
10 | copyright = '2023, Oxford RSE'
11 | author = 'Oxford RSE'
12 | release = '2.0.0'
13 |
14 | # -- General configuration ---------------------------------------------------
15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16 |
17 | extensions = []
18 |
19 | templates_path = ['_templates']
20 | exclude_patterns = []
21 |
22 |
23 |
24 | # -- Options for HTML output -------------------------------------------------
25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
26 |
27 | html_theme = 'alabaster'
28 | html_static_path = ['_static']
29 |
--------------------------------------------------------------------------------
/docs/source/img/Galv-logo-lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/Galv-logo-lg.png
--------------------------------------------------------------------------------
/docs/source/img/Galv-logo-shortened.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/Galv-logo-shortened.png
--------------------------------------------------------------------------------
/docs/source/img/Galv-logo-sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/Galv-logo-sm.png
--------------------------------------------------------------------------------
/docs/source/img/GalvStructure.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/GalvStructure.PNG
--------------------------------------------------------------------------------
/docs/source/img/Galv_DB_ERD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/Galv_DB_ERD.png
--------------------------------------------------------------------------------
/docs/source/img/galv_frontend.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/galv_frontend.jpg
--------------------------------------------------------------------------------
/docs/source/img/galv_frontend_v1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/docs/source/img/galv_frontend_v1.png
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. Galv documentation master file, created by
2 | sphinx-quickstart on Thu Mar 9 11:40:09 2023.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. image:: img/Galv-logo-lg.png
7 |
8 | Welcome to Galv's documentation!
9 | ======================================================================================
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 | :caption: Contents:
14 |
15 | FirstTimeQuickSetup
16 | UserGuide
17 | DevelopmentGuide
18 |
19 |
20 |
21 | Indices and tables
22 | ======================================================================================
23 |
24 | * :ref:`genindex`
25 | * :ref:`modindex`
26 | * :ref:`search`
27 |
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | CHOKIDAR_USEPOLLING=true
2 | FAST_REFRESH=false
3 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | FROM node:lts@sha256:132309f5136555725debd57b711eb4b329fff22a00588834dbec391a3f9782cf as build
6 |
7 | ARG VIRTUAL_HOST_ROOT="localhost"
8 | ARG FORCE_HTTP=""
9 |
10 | RUN mkdir -p /app
11 | WORKDIR /app
12 | COPY package.json yarn.lock /app/
13 |
14 | # Required to get react running:
15 | ENV NODE_OPTIONS=--openssl-legacy-provider
16 |
17 | RUN yarn install
18 |
19 | COPY . /app
20 | ENV VIRTUAL_HOST_ROOT=$VIRTUAL_HOST_ROOT
21 | ENV FORCE_HTTP=$FORCE_HTTP
22 | RUN ["/bin/sh", "-c", "./inject_envvars.sh"]
23 | RUN yarn build
24 |
25 | FROM nginx:alpine
26 | COPY --from=build /app/build /usr/share/nginx/html
27 | COPY --from=build /app/nginx.conf.template /etc/nginx/conf.d/custom.conf
28 |
29 | EXPOSE 80
30 | CMD ["/bin/sh" , "-c" , "exec nginx -g 'daemon off;'"]
31 |
32 |
--------------------------------------------------------------------------------
/frontend/Dockerfile_dev:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | FROM node:lts@sha256:132309f5136555725debd57b711eb4b329fff22a00588834dbec391a3f9782cf
6 |
7 | ARG VIRTUAL_HOST_ROOT="localhost"
8 | ARG FORCE_HTTP="true"
9 |
10 | RUN mkdir -p /app
11 | WORKDIR /app
12 | COPY . /app
13 |
14 | # Required to get react running:
15 | ENV NODE_OPTIONS=--openssl-legacy-provider
16 |
17 | # Make react-scripts serve on desired port
18 | ENV PORT=80
19 |
20 | RUN yarn install
21 |
22 | ENV VIRTUAL_HOST_ROOT=$VIRTUAL_HOST_ROOT
23 | ENV FORCE_HTTP=$FORCE_HTTP
24 | # NB in dev mode we use a volume for the source code, and inject_envvars.sh won't override
25 | # the contents of conf.json.
26 | RUN ["/bin/sh", "-c", "./inject_envvars.sh"]
27 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
32 | Galv
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/frontend/inject_envvars.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # get some vars from env and write to json
3 | if [ -z "$FORCE_HTTP" ]; then
4 | API_ROOT="https://api.$VIRTUAL_HOST_ROOT/"
5 | else
6 | API_ROOT="http://api.$VIRTUAL_HOST_ROOT/"
7 | fi
8 | RUNTIME_CONF="{
9 | \"API_ROOT\": \"$API_ROOT\"
10 | }"
11 | echo $RUNTIME_CONF > ./src/conf.json
12 |
--------------------------------------------------------------------------------
/frontend/nginx.conf.template:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: BSD-2-Clause
2 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | # of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | error_log /var/log/nginx/error.log warn;
6 |
7 | server {
8 | listen 80;
9 | server_name localhost;
10 |
11 | client_max_body_size 100M;
12 |
13 | access_log /var/log/nginx/access.log main;
14 | error_log /var/log/nginx/error.log debug;
15 |
16 | location / {
17 | root /usr/share/nginx/html;
18 | index index.html index.htm;
19 | # Serve index for any route that doesn't have a file extension (e.g. /devices)
20 | # https://stackoverflow.com/a/45599233
21 | try_files $uri $uri/ /index.html;
22 | }
23 |
24 | #error_page 404 /404.html;
25 |
26 | # redirect server error pages to the static page /50x.html
27 | error_page 500 502 503 504 /50x.html;
28 | location = /50x.html {
29 | root /usr/share/nginx/html;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "proxy": "http://app",
6 | "dependencies": {
7 | "@emotion/react": "^11.10.5",
8 | "@emotion/styled": "^11.10.5",
9 | "@mui/icons-material": "^5.10.15",
10 | "@mui/lab": "^5.0.0-alpha.109",
11 | "@mui/material": "^5.10.15",
12 | "@mui/x-data-grid": "^5.17.12",
13 | "@nivo/core": "^0.80.0",
14 | "@nivo/line": "^0.80.0",
15 | "@testing-library/jest-dom": "^5.16.5",
16 | "@testing-library/react": "^14.0.0",
17 | "@testing-library/user-event": "^14.4.3",
18 | "@types/jest": "^29.2.5",
19 | "@types/node": "^18.11.18",
20 | "@types/react": "^18.0.26",
21 | "@types/react-dom": "^18.0.10",
22 | "classnames": "^2.3.2",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-hook-form": "^7.6.3",
26 | "react-router-dom": "^6.4.3",
27 | "react-scripts": "4.0.3",
28 | "react-syntax-highlighter": "^15.5.0",
29 | "react-token-auth": "^2.3.8",
30 | "tss-react": "^4.8.2",
31 | "typescript": "^4.9.4",
32 | "web-vitals": "^3.1.0"
33 | },
34 | "devDependencies": {
35 | "react-styleguidist": "^13.0.0"
36 | },
37 | "scripts": {
38 | "start": "react-scripts start",
39 | "build": "react-scripts build",
40 | "test": "react-scripts test --CI=true --watchAll=false --noStackTrace",
41 | "eject": "react-scripts eject",
42 | "styleguide": "styleguidist server",
43 | "styleguide:build": "styleguidist build"
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/public/Galvanalyser-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
32 | Galv
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Battery-Intelligence-Lab/galv/118a8a1802cf368232a7df3b928c6fefafa4c77b/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/ActionButtons.tsx:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import React, {Component} from "react";
6 | import Stack, {StackProps} from "@mui/material/Stack";
7 | import IconButton, {IconButtonProps} from "@mui/material/IconButton";
8 | import Icon from "@mui/material/Icon";
9 | import SearchIcon from "@mui/icons-material/Search";
10 | import SaveIcon from "@mui/icons-material/Save";
11 | import DeleteIcon from "@mui/icons-material/Delete";
12 | import {SvgIconProps} from "@mui/material/SvgIcon"
13 | import { withStyles } from "tss-react/mui";
14 |
15 | export type ActionButtonsProps = {
16 | classes: Record;
17 | onInspect?: () => void;
18 | onSave?: () => void;
19 | onDelete?: () => void;
20 | inspectButtonProps?: IconButtonProps;
21 | saveButtonProps?: IconButtonProps;
22 | deleteButtonProps?: IconButtonProps;
23 | inspectIconProps?: SvgIconProps & {component?: any};
24 | saveIconProps?: SvgIconProps & {component?: any};
25 | deleteIconProps?: SvgIconProps & {component?: any};
26 | wrapperElementProps?: StackProps;
27 | }
28 |
29 | /**
30 | * Group together commonly displayed action buttons.
31 | *
32 | * Buttons included are:
33 | * - Inspect
34 | * - Save
35 | * - Delete
36 | *
37 | * Buttons are included where the on[ButtonName] property is specified.
38 | * Their properties can be customised with [buttonName]ButtonProps,
39 | * and the properties of the child SvgIcon by [buttonName]IconProps.
40 | *
41 | * The wrapper element is a element and
42 | * can be customised with the wrapperElementProps prop.
43 | */
44 | class ActionButtons extends Component {
45 | render() {
46 | const classes = withStyles.getClasses(this.props);
47 | return (
48 |
49 | {
50 | this.props.onInspect !== undefined &&
51 |
56 |
62 |
63 | }
64 | {
65 | this.props.onSave !== undefined &&
66 |
71 |
77 |
78 | }
79 | {
80 | this.props.onDelete !== undefined &&
81 |
87 |
93 |
94 | }
95 |
96 | )
97 | }
98 | }
99 |
100 | const StyledActionButtons = withStyles(
101 | ActionButtons,
102 | (theme, props) => ({
103 | infoIcon: {},
104 | saveIcon: {},
105 | deleteIcon: {}
106 | })
107 | )
108 |
109 | export default StyledActionButtons
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 | */
5 |
6 | .App {
7 | text-align: center;
8 | }
9 |
10 | .App-logo {
11 | height: 40vmin;
12 | pointer-events: none;
13 | }
14 |
15 | @media (prefers-reduced-motion: no-preference) {
16 | .App-logo {
17 | animation: App-logo-spin infinite 20s linear;
18 | }
19 | }
20 |
21 | .App-header {
22 | background-color: #282c34;
23 | min-height: 100vh;
24 | display: flex;
25 | flex-direction: column;
26 | align-items: center;
27 | justify-content: center;
28 | font-size: calc(10px + 2vmin);
29 | color: white;
30 | }
31 |
32 | .App-link {
33 | color: #61dafb;
34 | }
35 |
36 | @keyframes App-logo-spin {
37 | from {
38 | transform: rotate(0deg);
39 | }
40 | to {
41 | transform: rotate(360deg);
42 | }
43 | }
--------------------------------------------------------------------------------
/frontend/src/ApproveUsers.tsx:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import React, { Fragment } from 'react';
6 | import Paper from '@mui/material/Paper';
7 | import Container from '@mui/material/Container';
8 | import HowToRegIcon from '@mui/icons-material/HowToReg';
9 | import AsyncTable from './AsyncTable';
10 | import Connection, {User} from "./APIConnection";
11 | import useStyles from "./UseStyles";
12 | import Typography from '@mui/material/Typography';
13 | import IconButton from "@mui/material/IconButton";
14 |
15 | const columns = [
16 | {label: 'Username'},
17 | {label: 'Approve', help: 'Authorize a user to access Galv'}
18 | ]
19 |
20 | export default function ApproveUsers() {
21 | const { classes } = useStyles();
22 |
23 | const approveUser = (user: User) => Connection.fetch(`${user.url}vouch_for/`);
24 |
25 | return (
26 |
27 |
28 |
29 | classes={classes}
30 | columns={columns}
31 | row_generator={(user, context) => [
32 |
33 | {user.username}
34 | ,
35 |
36 | approveUser(user).then(() => context.refresh_all_rows(false))}
40 | >
41 |
42 |
43 |
44 | ]}
45 | url={`inactive_users/`}
46 | styles={classes}
47 | />
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/Equipment.tsx:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import React, { Fragment } from 'react';
6 | import TextField from '@mui/material/TextField';
7 | import Paper from '@mui/material/Paper';
8 | import AddIcon from '@mui/icons-material/Add';
9 | import Container from '@mui/material/Container';
10 | import SaveIcon from '@mui/icons-material/Save';
11 | import AsyncTable from './AsyncTable';
12 | import Connection from "./APIConnection";
13 | import ActionButtons from "./ActionButtons";
14 | import useStyles from "./UseStyles";
15 |
16 | export type EquipmentFields = {
17 | url: string;
18 | id: number;
19 | name: string;
20 | type: string;
21 | in_use: boolean;
22 | }
23 |
24 | const columns = [
25 | {label: 'Name', help: 'Equipment Name'},
26 | {label: 'Type', help: 'Equipment Type'},
27 | {label: 'Save', help: 'Save / Delete equipment. Edits are disabled for equipment that is in use'},
28 | ]
29 |
30 | const string_fields = ['name', 'type'] as const
31 |
32 | export default function Equipment() {
33 | const { classes } = useStyles();
34 |
35 | const get_write_data: (data: EquipmentFields) => Partial = (data) => {
36 | return { name: data.name, type: data.type }
37 | }
38 |
39 | const addNewEquipment = (data: EquipmentFields) => {
40 | const insert_data = get_write_data(data)
41 | return Connection.fetch('equipment/', {body: JSON.stringify(insert_data), method: 'POST'})
42 | };
43 |
44 | const updateEquipment = (data: EquipmentFields) => {
45 | const insert_data = get_write_data(data)
46 | return Connection.fetch(data.url, {body: JSON.stringify(insert_data), method: 'PATCH'})
47 | .then(r => r.content)
48 | };
49 |
50 | const deleteEquipment = (data: EquipmentFields) => Connection.fetch(data.url, {method: 'DELETE'})
51 |
52 | return (
53 |
54 |
55 |
56 | classes={classes}
57 | columns={columns}
58 | row_generator={(equipment, context) => [
59 | ...string_fields.map(n =>
60 | {
61 | equipment.in_use ? equipment[n] :
72 | }
73 | ),
74 |
75 | addNewEquipment(equipment).then(() => context.refresh_all_rows()) :
80 | () => updateEquipment(equipment).then(context.refresh)
81 | }
82 | saveButtonProps={{disabled: !context.value_changed || equipment.in_use}}
83 | saveIconProps={{component: context.is_new_row? AddIcon : SaveIcon}}
84 | onDelete={
85 | () =>
86 | window.confirm(`Delete equipment ${equipment.name}?`) &&
87 | deleteEquipment(equipment).then(context.refresh)
88 | }
89 | deleteButtonProps={{disabled: context.is_new_row || equipment.in_use}}
90 | />
91 |
92 | ]}
93 | new_row_values={{name: '', type: ''}}
94 | url={`equipment/`}
95 | styles={classes}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/frontend/src/Files.tsx:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import React, { Fragment } from 'react';
6 | import Container from '@mui/material/Container';
7 | import Tooltip from "@mui/material/Tooltip";
8 | import Typography from "@mui/material/Typography";
9 | import Paper from '@mui/material/Paper';
10 | import AsyncTable from './AsyncTable';
11 | import Connection from "./APIConnection";
12 | import {MonitoredPathFields} from "./MonitoredPaths";
13 | import IconButton from "@mui/material/IconButton";
14 | import RefreshIcon from "@mui/icons-material/Refresh";
15 | import useStyles from "./UseStyles";
16 |
17 | export type FileFields = {
18 | url: string;
19 | id: number;
20 | path: string;
21 | state: string;
22 | last_observed_time: string;
23 | last_observed_size: number;
24 | errors: {
25 | error: string;
26 | timestamp: string;
27 | [key: string]: any;
28 | }
29 | datasets: string[];
30 | }
31 |
32 | export type FilesProps = { path: MonitoredPathFields }
33 |
34 | export default function Files(props: FilesProps) {
35 | const { classes } = useStyles();
36 |
37 | const forceReimport = (file: FileFields) => Connection.fetch(`${file.url}reimport/`)
38 |
39 | const datetimeOptions: Intl.DateTimeFormatOptions = {
40 | year: 'numeric', month: 'numeric', day: 'numeric',
41 | hour: 'numeric', minute: 'numeric', second: 'numeric',
42 | };
43 |
44 | const columns = [
45 | {label: props.path.path, help: 'File path'},
46 | {label: 'Last Observed Size', help: 'Size of the file in bytes'},
47 | {label: 'Last Observed Time', help: 'Time file was last scanned'},
48 | {label: 'State', help: 'File state'},
49 | {label: 'Datasets', help: 'View datasets linked to this file'},
50 | {label: 'Force Re-import', help: 'Retry the import operation next time the file is scanned'}
51 | ]
52 |
53 | const file_state = (file: FileFields) => {
54 | let state = file.state === 'IMPORT FAILED' ?
55 | {file.state} : {file.state}
56 | if (file.errors.length)
57 | state = {state}
58 | return state
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 | classes={classes}
66 | key="files"
67 | columns={columns}
68 | row_generator={(file, context) => [
69 | {file.path},
70 | {file.last_observed_size},
71 | {
72 | Intl.DateTimeFormat('en-GB', datetimeOptions).format(
73 | Date.parse(file.last_observed_time)
74 | )}
75 | ,
76 | {file_state(file)},
77 | {file.datasets.length},
78 |
79 | forceReimport(file).then(context.refresh)}>
82 |
83 |
84 |
85 | ]}
86 | url={`${props.path.url}files/`}
87 | styles={classes}
88 | />
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/frontend/src/FormComponents.js:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | import React from "react";
6 | import TextField from '@mui/material/TextField';
7 | import { makeStyles } from '@mui/styles'
8 | import InputLabel from '@mui/material/InputLabel';
9 | import MenuItem from '@mui/material/MenuItem';
10 | import Autocomplete from '@mui/lab/Autocomplete';
11 | import FormControl from '@mui/material/FormControl';
12 | import Select from '@mui/material/Select';
13 | import { Controller } from "react-hook-form";
14 | //import DateFnsUtils from '@date-io/date-fns'; // choose your lib
15 | //import { DateTimePicker } from '@mui/pickers';
16 | import Chip from '@mui/material/Chip';
17 |
18 | const useStyles = makeStyles((theme) => ({
19 | container: {
20 | paddingTop: theme.spacing(4),
21 | paddingBottom: theme.spacing(4),
22 | height: '100%',
23 | },
24 | formInput: {
25 | margin: theme.spacing(1),
26 | width: '100%',
27 | },
28 | chips: {
29 | display: 'flex',
30 | flexWrap: 'wrap',
31 | },
32 | chip: {
33 | margin: 2,
34 | },
35 | }));
36 |
37 | //export function FormDateTimeField({control, name, defaultValue, label, ...rest}) {
38 | // const { classes } = useStyles();
39 | // return (
40 | // (
45 | //
52 | // )}
53 | // />
54 | // )
55 | //}
56 |
57 | export function FormTextField({control, name, defaultValue, label, ...rest}) {
58 | const { classes } = useStyles();
59 | return (
60 | (
65 |
71 | )}
72 | />
73 | )
74 | }
75 |
76 | export function FormMultiSelectField({control, name, defaultValue, label, options, ...rest}) {
77 | const { classes } = useStyles();
78 | return (
79 |
80 |
81 | {label}
82 |
83 | (
88 |
111 | )}
112 | />
113 |
114 | )
115 | }
116 |
117 | export function FormAutocompleteField({control, name, defaultValue, label, options, ...rest}) {
118 | const { classes } = useStyles();
119 |
120 | return (
121 | (
126 |
132 |
133 | }
134 | onChange={(_, data) => field.onChange(data)}
135 | />
136 | )}
137 | />
138 | )
139 | }
140 |
141 | export function FormSelectField({control, name, defaultValue, label, options, ...rest}) {
142 | const { classes } = useStyles();
143 | return (
144 |
145 |
146 | {label}
147 |
148 | (
153 |
164 | )}
165 | />
166 |
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/frontend/src/Galv-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/GetDatasetMatlab.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SyntaxHighlighter from 'react-syntax-highlighter';
3 | import { docco } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
4 | import Typography from '@mui/material/Typography';
5 | import Connection from "./APIConnection";
6 |
7 |
8 | export default function GetDatasetMatlab({dataset}) {
9 | const token = Connection.user?.token;
10 |
11 | let domain = window.location.href.split('/')[2];
12 | domain = domain.split(':')[0]
13 |
14 | const host = Connection.url || `${window.location.protocol}//api.${domain}/`;
15 | const codeString = `%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
16 | %
17 | % galv REST API access
18 | % - Matt Jaquiery (Oxford RSE)
19 | %
20 | % 2022-11-21
21 | %
22 | % Download datasets from the REST API.
23 | % Downloads all data for all columns for the dataset and reads them
24 | % into a cell array. Data are under datasets{x} as Tables.
25 | % Column names are coerced to valid MATLAB variable names using
26 | % matlab.lang.makeValidName.
27 | %
28 | % Dataset and column metadata are under dataset_metadata{x} and
29 | % column_metadata{x} respectively.
30 | %
31 | % SPDX-License-Identifier: BSD-2-Clause
32 | % Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
33 | % of Oxford, and the 'Galv' Developers. All rights reserved.
34 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
35 |
36 | % login to galv > Generate API Token
37 | token = '${token}';
38 | apiURL = '${host}datasets';
39 | options = weboptions('HeaderFields', {'Authorization' ['Bearer ' token]});
40 |
41 | % Datasets are referenced by id.
42 | % You can add in additional dataset_names or dataset_ids to also
43 | % fetch the contents of those datasets.
44 | dataset_ids = [${dataset.id}]; % add additional dataset ids here if required
45 |
46 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
47 | n = max(dataset_ids);
48 | dataset_metadata = cell(n, 1);
49 | column_metadata = cell(n, 1);
50 | datasets = cell(n, 1);
51 |
52 | dataset_ids = unique(dataset_ids);
53 |
54 | for i = 1:length(dataset_ids)
55 | d = dataset_ids(i);
56 |
57 | % get data
58 | dsURL = strcat(apiURL, '/', num2str(d), '/');
59 | meta = webread(dsURL, options);
60 | dataset_metadata{d} = meta;
61 |
62 | column_metadata{i} = cell(length(meta.columns), 1);
63 | datasets{i} = table();
64 |
65 | % append column data in columns
66 | for c = 1:length(meta.columns)
67 | cURL = meta.columns{c};
68 | stream = webread(cURL, options);
69 | column_metadata{i}{c} = stream;
70 | column_content = webread(stream.values, options);
71 | % drop final newline
72 | column_content = regexprep(column_content, '\n$', '');
73 | column_content = strsplit(column_content, '\n');
74 | column_content = arrayfun(@(c) str2num(c{1}), column_content);
75 | datasets{i}.(matlab.lang.makeValidName(stream.name)) = rot90(column_content, -1);
76 | end
77 | end
78 | `
79 |
80 | return (
81 |
82 |
83 |
84 | MATLAB Code
85 |
86 |
87 |
88 | {codeString}
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/frontend/src/GetDatasetPython.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from "react";
2 | import SyntaxHighlighter from 'react-syntax-highlighter';
3 | import { docco } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
4 | import Typography from '@mui/material/Typography';
5 | import Connection from "./APIConnection";
6 | import {CircularProgress} from "@mui/material";
7 |
8 |
9 | export default function GetDatasetPython({dataset}) {
10 | const token = Connection.user?.token;
11 | const [columns, setColumns] = useState("")
12 | const [code, setCode] = useState()
13 |
14 | let domain = window.location.href.split('/')[2];
15 | domain = domain.split(':')[0]
16 |
17 | const host = `http://api.${domain}`
18 |
19 | useEffect(() => {
20 | Promise.all(dataset.columns.map(column =>
21 | Connection.fetch(column)
22 | .then(r => r.content)
23 | .then(col => ` '${col.name}': ${col.id},`)
24 | ))
25 | .then(cols => setColumns(cols.join('\n')))
26 | }, [dataset])
27 |
28 | useEffect(() => {
29 | if (!columns)
30 | setCode()
31 | else
32 | setCode(
33 | {
34 | `# SPDX-License-Identifier: BSD-2-Clause
35 | # Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
36 | # of Oxford, and the 'Galv' Developers. All rights reserved.
37 |
38 | # By Matt Jaquiery
39 |
40 | # Download datasets from the REST API.
41 | # Downloads all data for all columns for the dataset and reads them
42 | # into a Dict object. Data are under datasets[x] as DataFrames.
43 | #
44 | # Dataset and column metadata are under dataset_metadata[x] and
45 | # column_metadata[x] respectively.
46 |
47 | import urllib3 # install via pip if not available
48 |
49 | host = "${host}"
50 | headers = {'Authorization': 'Bearer ${token}'}
51 |
52 | # Configuration
53 | verbose = True
54 | if verbose:
55 | import time
56 |
57 | # Add additional dataset ids to download additional datasets
58 | dataset_ids = [${dataset.id}]
59 | dataset_metadata = {} # Will have keys=dataset_id, values=Dict of dataset metadata
60 | column_metadata = {} # Will have keys=dataset_id, values=Dict of column metadata
61 | datasets = {} # Will have keys=dataset_id, values=pandas DataFrame of data
62 |
63 | # Download data
64 | start_time = time.time()
65 | if verbose:
66 | print(f"Downloading {len(dataset_ids)} datasets from {host}")
67 |
68 | for dataset_id in dataset_ids:
69 | dataset_start_time = time.time()
70 | if verbose:
71 | print(f"Downloading dataset {dataset_id}")
72 | r = urllib3.request('GET', f"{host}/datasets/{dataset_id}/", headers=headers)
73 | try:
74 | json = r.json()
75 | except:
76 | print(f"Non-JSON response while downloading {dataset_id}: {r.status}")
77 | continue
78 | if r.status != 200:
79 | print(f"Error downloading dataset {dataset_id}: {r.status}")
80 | continue
81 | columns = json.get('columns', [])
82 | json['columns'] = []
83 |
84 | dataset_metadata[dataset_id] = json
85 |
86 | if verbose:
87 | print(f"Dataset {dataset_id} has {len(columns)} columns to download")
88 | # Download the data from all columns in the dataset
89 | datasets[dataset_id] = pandas.DataFrame()
90 | for i, column in enumerate(columns):
91 | if verbose:
92 | print(f"Downloading dataset {dataset_id} column {i}")
93 | r = urllib3.request('GET', column, headers=headers)
94 | try:
95 | json = r.json()
96 | except:
97 | print(f"Non-JSON response while downloading from {column}: {r.status}")
98 | continue
99 | if r.status != 200:
100 | print(f"Error downloading column {dataset_id}: {r.status}")
101 | continue
102 |
103 | # Download the data from all rows in the column
104 | v = urllib3.request('GET', json.get('values'), headers=headers)
105 | if v.status != 200:
106 | print(f"Error downloading values for dataset {dataset_id} column {json.get('name')}: {v.status}")
107 | continue
108 | try:
109 | datasets[dataset_id][json.get('name')] = v.data.decode('utf-8').split('\\n')
110 | except:
111 | print(f"Cannot translate JSON response into DataFrame for column values {json.get['values']}")
112 | continue
113 |
114 | column_metadata[dataset_id] = json
115 |
116 | if verbose:
117 | print(f"Finished downloading dataset {dataset_id} in {time.time() - dataset_start_time} seconds")
118 |
119 | if verbose:
120 | print(f"Finished downloading {len(dataset_ids)} datasets in {time.time() - start_time} seconds")
121 |
122 | `
123 | }
124 | )
125 | }, [columns, dataset.id, host, token])
126 |
127 | return (
128 |
129 |
130 | Python Code
131 |
132 |
133 | {code}
134 |
135 | )
136 | }
137 |
--------------------------------------------------------------------------------
/frontend/src/PaginatedTable.tsx:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: BSD-2-Clause
2 | // Copyright (c) 2020-2023, The Chancellor, Masters and Scholars of the University
3 | // of Oxford, and the 'Galv' Developers. All rights reserved.
4 |
5 | export type unused = any
6 | // /**
7 | // * Offer a table with previous/next links.
8 | // */
9 | // import classNames from 'classnames'
10 | // import React, {Component, ReactComponentElement} from "react";
11 | // import TableContainer from "@mui/material/TableContainer";
12 | // import Table from "@mui/material/Table";
13 | // import TableBody from "@mui/material/TableBody";
14 | // import Button from '@mui/material/Button';
15 | // import Connection, {APIResponse, APIObject, SingleAPIResponse} from "./APIConnection";
16 | // import TableRow from "@mui/material/TableRow";
17 | // import TableCell, {TableCellProps} from "@mui/material/TableCell";
18 | // import LoadingButton from "@mui/lab/LoadingButton";
19 | // import Tooltip, {TooltipProps} from "@mui/material/Tooltip";
20 | // import Typography, {TypographyProps} from "@mui/material/Typography";
21 | // import TableHead from "@mui/material/TableHead";
22 | // import AsyncTable from "./AsyncTable";
23 | //
24 | // type PaginatedTableProps = {
25 | // initial_url: string;
26 | // }
27 | //
28 | // type PaginatedTableState = {
29 | // links: PaginationLinks;
30 | // current_url: string;
31 | // }
32 | //
33 | // export type PaginationLinks = {
34 | // previous?: string | null,
35 | // next?: string | null,
36 | // }
37 | //
38 | // type PaginationDataFun = (url?: string | null) => APIResponse
39 | //
40 | // type PaginationProps = PaginationLinks & {
41 | // get_data_fun: PaginationDataFun;
42 | // position: string;
43 | // }
44 | //
45 | //
46 | // class Pagination extends AsyncTable {
47 | // render() {
48 | // return (
49 | //