diff --git a/.github/workflows/test_django_api.yaml b/.github/workflows/test_django_api.yaml deleted file mode 100644 index db407da..0000000 --- a/.github/workflows/test_django_api.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: Test Database Tool -on: - pull_request: - types: - - opened - - synchronize - - reopened - -jobs: - build: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:latest - env: - POSTGRES_DB: ${{secrets.DB_NAME}} - POSTGRES_USER: ${{secrets.DB_USER}} - POSTGRES_PASSWORD: ${{secrets.DB_PASSWORD}} - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - - steps: - - name: Checkout Repository - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install . -r requirements.txt - - - name: Run Django Model Tests - env: - DB_PASSWORD: ${{secrets.DB_PASSWORD}} - run: python ./src/evagram/website/backend/manage.py test api diff --git a/.github/workflows/test_evagram_api.yaml b/.github/workflows/test_evagram_api.yaml index 2c7831a..d21fe97 100644 --- a/.github/workflows/test_evagram_api.yaml +++ b/.github/workflows/test_evagram_api.yaml @@ -36,4 +36,5 @@ jobs: - name: Run Django API Tests run: python src/evagram/website/backend/manage.py test api env: + DB_HOST: 127.0.0.1 DB_PASSWORD: ${{secrets.DB_PASSWORD}} diff --git a/.github/workflows/test_evagram_input.yaml b/.github/workflows/test_evagram_input.yaml index 8b39043..6577a2f 100644 --- a/.github/workflows/test_evagram_input.yaml +++ b/.github/workflows/test_evagram_input.yaml @@ -34,11 +34,17 @@ jobs: - name: Install Evagram Input Module run: | python -m pip install --upgrade pip - pip install evagram_input@git+https://github.com/GEOS-ESM/evagram_input@feature/input_tool --upgrade + pip install evagram_input@git+https://github.com/GEOS-ESM/evagram_input --upgrade - name: Install Dependencies run: pip install . -r requirements.txt + - name: Create PGPASS File + run: | + echo 127.0.0.1:5432:test_evagram:postgres:${{secrets.DB_PASSWORD}} >> ~/.pgpass + chmod 600 ~/.pgpass + export PGPASSFILE='/home/runner/.pgpass' + - name: Run Evagram Input Tests run: python src/evagram/website/backend/manage.py test input_app.test_input_tool env: diff --git a/.gitignore b/.gitignore index c69d9e6..a64ea61 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ __pycache__/ # C extensions *.so +.prettierrc + # Distribution / packaging .Python build/ diff --git a/README.md b/README.md index c7c535a..a596965 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ # evagram + +## Getting Started + +To get started with running the Evagram application in development mode, clone the JCSDA-internal/evagram repository into your local machine. + +### Prerequisites + +The Evagram application is a full-stack application comprised of the frontend, backend, and database services that will run and be built inside Docker containers, requiring the installation of Docker into the host system. Please refer to the Docker installation guide for installing Docker onto your OS system. + +## Usage + +Once Docker is successfully installed into your system, simply open the Docker CLI to build and run the application: `docker compose build` and `docker compose up`. This will automatically build the Evagram application using the docker compose configuration file and click on https://localhost:3000 once the process has started. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..60589c1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + database: + image: postgres:latest + ports: + - 5432:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: $DB_PASSWORD + POSTGRES_DB: evagram + volumes: + - evagram:/var/lib/postgresql/data/ + + backend: + restart: always + build: src/evagram/website/backend + command: > + sh -c "python manage.py migrate && + python manage.py loaddata api/fixtures/test_data.json + python manage.py runserver 0.0.0.0:8000" + ports: + - 8000:8000 + environment: + DB_HOST: database + DB_PASSWORD: $DB_PASSWORD + depends_on: + - database + + frontend: + build: src/evagram/website/frontend + ports: + - 3000:3000 + +volumes: + evagram: diff --git a/pycodestyle.cfg b/pycodestyle.cfg index db4875f..c874899 100644 --- a/pycodestyle.cfg +++ b/pycodestyle.cfg @@ -9,4 +9,4 @@ max-line-length = 100 indent-size = 4 statistics = True ignore = W503, W504 -exclude = __pycache__, src/evagram/website/backend/api/migrations +exclude = __pycache__, src/evagram/website/backend/api/migrations, src/evagram/website/frontend/node_modules diff --git a/src/evagram/website/backend/.dockerignore b/src/evagram/website/backend/.dockerignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/src/evagram/website/backend/.dockerignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/src/evagram/website/backend/Dockerfile b/src/evagram/website/backend/Dockerfile new file mode 100644 index 0000000..8d9d20c --- /dev/null +++ b/src/evagram/website/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:latest + +WORKDIR /backend + +COPY requirements.txt . +COPY loaddata.py . + +RUN pip install --upgrade pip +RUN pip install -r requirements.txt + +COPY . . +EXPOSE 8000 diff --git a/src/evagram/website/backend/api/migrations/0001_initial.py b/src/evagram/website/backend/api/migrations/0001_initial.py index 1a67d5a..720461c 100644 --- a/src/evagram/website/backend/api/migrations/0001_initial.py +++ b/src/evagram/website/backend/api/migrations/0001_initial.py @@ -1,5 +1,3 @@ -# Generated by Django 4.2.10 on 2024-03-24 02:17 - from django.db import migrations, models import django.db.models.deletion diff --git a/src/evagram/website/backend/api/serializers.py b/src/evagram/website/backend/api/serializers.py index 6b37ecd..2dccf7c 100644 --- a/src/evagram/website/backend/api/serializers.py +++ b/src/evagram/website/backend/api/serializers.py @@ -1,35 +1,62 @@ from rest_framework import serializers from api.models import * +from django.db import models class OwnerSerializer(serializers.ModelSerializer): + key = serializers.ModelField(model_field=Owners()._meta.get_field('owner_id')) + value = serializers.ModelField(model_field=Owners()._meta.get_field('owner_id')) + content = serializers.ModelField(model_field=Owners()._meta.get_field('username')) + type = models.CharField(default='owner') + class Meta: model = Owners - fields = ['owner_id', 'first_name', 'last_name', 'username'] + # fields = ['owner_id', 'first_name', 'last_name', 'username'] + fields = ['key', 'value', 'content', 'owner_id', 'username'] class ExperimentSerializer(serializers.ModelSerializer): + key = serializers.ModelField(model_field=Experiments()._meta.get_field('experiment_id')) + value = serializers.ModelField(model_field=Experiments()._meta.get_field('experiment_id')) + content = serializers.ModelField(model_field=Experiments()._meta.get_field('experiment_name')) + type = models.CharField(default='experiment') + class Meta: model = Experiments - fields = ['experiment_id', 'experiment_name'] + fields = ['key', 'value', 'content', 'experiment_id', 'experiment_name'] class ObservationSerializer(serializers.ModelSerializer): + key = serializers.ModelField(model_field=Observations()._meta.get_field('observation_id')) + value = serializers.ModelField(model_field=Observations()._meta.get_field('observation_id')) + content = serializers.ModelField(model_field=Observations()._meta.get_field('observation_name')) + type = models.CharField(default='observation') + class Meta: model = Observations - fields = ['observation_id', 'observation_name'] + fields = ['key', 'value', 'content', 'observation_id', 'observation_name'] class VariableSerializer(serializers.ModelSerializer): + key = serializers.ModelField(model_field=Variables()._meta.get_field('variable_id')) + value = serializers.ModelField(model_field=Variables()._meta.get_field('variable_id')) + content = serializers.ModelField(model_field=Variables()._meta.get_field('variable_name')) + type = models.CharField(default='variable') + class Meta: model = Variables - fields = ['variable_id', 'variable_name', 'channel'] + fields = ['key', 'value', 'content', 'variable_id', 'variable_name', 'channel'] class GroupSerializer(serializers.ModelSerializer): + key = serializers.ModelField(model_field=Groups()._meta.get_field('group_id')) + value = serializers.ModelField(model_field=Groups()._meta.get_field('group_id')) + content = serializers.ModelField(model_field=Groups()._meta.get_field('group_name')) + type = models.CharField(default='group') + class Meta: model = Groups - fields = ['group_id', 'group_name'] + fields = ['key', 'value', 'content', 'group_id', 'group_name'] class PlotSerializer(serializers.ModelSerializer): diff --git a/src/evagram/website/backend/api/test_views.py b/src/evagram/website/backend/api/test_views.py index 2091d2c..a6f1b99 100644 --- a/src/evagram/website/backend/api/test_views.py +++ b/src/evagram/website/backend/api/test_views.py @@ -14,42 +14,84 @@ def test_initial_load(self): self.assertTrue("observations" in response.json()) self.assertTrue("variables" in response.json()) - def test_get_plot_components(self): + def test_get_single_plot(self): response = self.client.get( - "/api/get-plot-components/?experiment_id=12&observation_id=1&variable_id=1&group_id=1") - self.assertEqual("experiment_id=12&observation_id=1&variable_id=1&group_id=1", + "/api/get-plots-by-field/?owner_id=1&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1") + self.assertEqual("owner_id=1&experiment_id=12&observation_id=1&" + "variable_name=brightnessTemperature&channel=4&group_id=1", response.request['QUERY_STRING']) self.assertEqual(200, response.status_code) # check if plot components in response match with plot id in database - plot = Plots.objects.get(pk=response.json()["plot_id"]) - self.assertEqual(plot.div, response.json()["div"]) - self.assertEqual(plot.script, response.json()["script"]) + plot = Plots.objects.get(pk=response.json()[0]["plot_id"]) + self.assertEqual(plot.div, response.json()[0]["div"]) + self.assertEqual(plot.script, response.json()[0]["script"]) def test_plot_insufficient_params(self): - response = self.client.get("/api/get-plot-components/?experiment_id=12&observation_id=1") - self.assertEqual("experiment_id=12&observation_id=1", response.request['QUERY_STRING']) + response = self.client.get("/api/get-plots-by-field/?owner_id=1") + self.assertEqual("owner_id=1", response.request['QUERY_STRING']) + self.assertEqual(400, response.status_code) + + def test_plot_invalid_username(self): + response = self.client.get( + "/api/get-plots-by-field/?owner_id=null&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1") + self.assertEqual("owner_id=null&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1", + response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Missing request parameter detected: 'variable_id'", - response.json()['error']) - def test_plot_invalid_plot_fks(self): + def test_plot_username_not_found(self): response = self.client.get( - "/api/get-plot-components/?experiment_id=-12&observation_id=1&variable_id=1&group_id=1") - self.assertEqual("experiment_id=-12&observation_id=1&variable_id=1&group_id=1", + "/api/get-plots-by-field/?owner_id=-1&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1") + self.assertEqual("owner_id=-1&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Plots matching query does not exist.", response.json()['error']) - def test_plot_bad_input(self): + def test_plot_value_error(self): response = self.client.get( - "/api/get-plot-components/?experiment_id=xyz&observation_id=1&variable_id=1&group_id=1") - self.assertEqual("experiment_id=xyz&observation_id=1&variable_id=1&group_id=1", + "/api/get-plots-by-field/?owner_id=1&experiment_id=xyz&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1") + self.assertEqual("owner_id=1&experiment_id=xyz&observation_id=1" + "&variable_name=brightnessTemperature&channel=4&group_id=1", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Field 'experiment_id' expected a number but got 'xyz'.", - response.json()['error']) - def test_update_user_valid_pk(self): + def test_plot_input_cascade(self): + # Tests if the experiment_id is handled before the observation_id + response = self.client.get( + "/api/get-plots-by-field/?owner_id=1&experiment_id=null&observation_id=1" + "&variable_name=null&channel=null&group_id=null") + self.assertEqual("owner_id=1&experiment_id=null&observation_id=1" + "&variable_name=null&channel=null&group_id=null", + response.request['QUERY_STRING']) + # If diagnostics were queried out of order by observation_id, + # the queryset will be empty because experiment_id=null + self.assertNotEqual(0, len(response.data)) + + def test_plot_channel_error(self): + # Tests if channel was not provided and variable requires it + response = self.client.get( + "/api/get-plots-by-field/?owner_id=1&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=null&group_id=null") + self.assertEqual("owner_id=1&experiment_id=12&observation_id=1" + "&variable_name=brightnessTemperature&channel=null&group_id=null", + response.request['QUERY_STRING']) + self.assertEqual(404, response.status_code) + + def test_plot_channel_is_optional(self): + # Tests if channel was not provided but variable takes optional channel value + response = self.client.get( + "/api/get-plots-by-field/?owner_id=1&experiment_id=12&observation_id=1" + "&variable_name=windEastward&channel=null&group_id=null") + self.assertEqual("owner_id=1&experiment_id=12&observation_id=1" + "&variable_name=windEastward&channel=null&group_id=null", + response.request['QUERY_STRING']) + self.assertEqual(200, response.status_code) + + def test_update_user_option_valid_params(self): response = self.client.get("/api/update-user-option/?owner_id=1") self.assertEqual("owner_id=1", response.request['QUERY_STRING']) self.assertEqual(200, response.status_code) @@ -58,26 +100,23 @@ def test_update_user_valid_pk(self): self.assertTrue("variables" in response.json()) self.assertTrue("groups" in response.json()) - def test_update_user_invalid_pk(self): + def test_update_user_option_object_not_found(self): response = self.client.get("/api/update-user-option/?owner_id=-1") self.assertEqual("owner_id=-1", response.request['QUERY_STRING']) - self.assertEqual(400, response.status_code) - self.assertEqual("Owners matching query does not exist.", response.json()['error']) + self.assertEqual(404, response.status_code) - def test_update_user_bad_input(self): + def test_update_user_option_value_error(self): response = self.client.get("/api/update-user-option/?owner_id=owner") self.assertEqual("owner_id=owner", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Field 'owner_id' expected a number but got 'owner'.", - response.json()['error']) - def test_update_user_insufficient_params(self): + def test_update_user_option_insufficient_params(self): response = self.client.get("/api/update-user-option/") self.assertEqual("", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) self.assertEqual("Missing request parameter detected: 'owner_id'", response.json()['error']) - def test_update_experiment_valid_pk(self): + def test_update_experiment_option_valid_params(self): response = self.client.get("/api/update-experiment-option/?experiment_id=1") self.assertEqual("experiment_id=1", response.request['QUERY_STRING']) self.assertEqual(200, response.status_code) @@ -85,75 +124,60 @@ def test_update_experiment_valid_pk(self): self.assertTrue("variables" in response.json()) self.assertTrue("groups" in response.json()) - def test_update_experiment_invalid_pk(self): + def test_update_experiment_option_object_not_found(self): response = self.client.get("/api/update-experiment-option/?experiment_id=-1") self.assertEqual("experiment_id=-1", response.request['QUERY_STRING']) - self.assertEqual(400, response.status_code) - self.assertEqual("Experiments matching query does not exist.", response.json()['error']) + self.assertEqual(404, response.status_code) - def test_update_experiment_bad_input(self): + def test_update_experiment_option_value_error(self): response = self.client.get("/api/update-experiment-option/?experiment_id=!") self.assertEqual("experiment_id=!", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Field 'experiment_id' expected a number but got '!'.", - response.json()['error']) - def test_update_experiment_insufficient_params(self): + def test_update_experiment_option_insufficient_params(self): response = self.client.get("/api/update-experiment-option/?key1=value1") self.assertEqual("key1=value1", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Missing request parameter detected: 'experiment_id'", - response.json()['error']) - def test_update_observation_valid_pk(self): + def test_update_observation_option_valid_params(self): response = self.client.get("/api/update-observation-option/?observation_id=1") self.assertEqual("observation_id=1", response.request['QUERY_STRING']) self.assertEqual(200, response.status_code) self.assertTrue("groups" in response.json()) self.assertTrue("variables" in response.json()) + self.assertTrue("variablesMap" in response.json()) - def test_update_observation_invalid_pk(self): + def test_update_observation_option_object_not_found(self): response = self.client.get("/api/update-observation-option/?observation_id=-1") self.assertEqual("observation_id=-1", response.request['QUERY_STRING']) - self.assertEqual(400, response.status_code) - self.assertEqual("Observations matching query does not exist.", response.json()['error']) + self.assertEqual(404, response.status_code) - def test_update_observation_bad_input(self): + def test_update_observation_option_value_error(self): response = self.client.get("/api/update-observation-option/?observation_id=~") self.assertEqual("observation_id=~", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Field 'observation_id' expected a number but got '~'.", - response.json()['error']) - def test_update_observation_insufficient_params(self): + def test_update_observation_option_insufficient_params(self): response = self.client.get("/api/update-observation-option/") self.assertEqual("", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Missing request parameter detected: 'observation_id'", - response.json()['error']) - def test_update_variable_valid_pk(self): - response = self.client.get("/api/update-variable-option/?variable_id=1") - self.assertEqual("variable_id=1", response.request['QUERY_STRING']) + def test_update_variable_option_valid_params(self): + response = self.client.get( + "/api/update-variable-option/?variable_name=brightnessTemperature&channel=4") + self.assertEqual("variable_name=brightnessTemperature" + "&channel=4", response.request['QUERY_STRING']) self.assertEqual(200, response.status_code) self.assertTrue("groups" in response.json()) + self.assertTrue("channel" in response.json()) - def test_update_variable_invalid_pk(self): - response = self.client.get("/api/update-variable-option/?variable_id=-1") - self.assertEqual("variable_id=-1", response.request['QUERY_STRING']) - self.assertEqual(400, response.status_code) - self.assertEqual("Variables matching query does not exist.", response.json()['error']) - - def test_update_variable_bad_input(self): - response = self.client.get("/api/update-variable-option/?variable_id=~") - self.assertEqual("variable_id=~", response.request['QUERY_STRING']) - self.assertEqual(400, response.status_code) - self.assertEqual("Field 'variable_id' expected a number but got '~'.", - response.json()['error']) + def test_update_variable_option_object_not_found(self): + response = self.client.get( + "/api/update-variable-option/?variable_name=variable&channel=null") + self.assertEqual("variable_name=variable&channel=null", response.request['QUERY_STRING']) + self.assertEqual(404, response.status_code) - def test_update_variable_insufficient_params(self): + def test_update_variable_option_insufficient_params(self): response = self.client.get("/api/update-variable-option/") self.assertEqual("", response.request['QUERY_STRING']) self.assertEqual(400, response.status_code) - self.assertEqual("Missing request parameter detected: 'variable_id'", - response.json()['error']) diff --git a/src/evagram/website/backend/api/urls.py b/src/evagram/website/backend/api/urls.py index 034bd32..f65fcda 100644 --- a/src/evagram/website/backend/api/urls.py +++ b/src/evagram/website/backend/api/urls.py @@ -3,7 +3,7 @@ urlpatterns = [ path("initial-load/", views.initial_load), - path("get-plot-components/", views.get_plot_components), + path("get-plots-by-field/", views.get_plots_by_field), path("update-user-option/", views.update_user_option), path("update-experiment-option/", views.update_experiment_option), path("update-observation-option/", views.update_observation_option), diff --git a/src/evagram/website/backend/api/views.py b/src/evagram/website/backend/api/views.py index c6dcf3f..2098b94 100644 --- a/src/evagram/website/backend/api/views.py +++ b/src/evagram/website/backend/api/views.py @@ -32,20 +32,74 @@ def initial_load(request): @api_view(['GET']) -def get_plot_components(request): +def get_plots_by_field(request): try: + owner_id = request.GET["owner_id"] experiment_id = request.GET["experiment_id"] observation_id = request.GET["observation_id"] - variable_id = request.GET["variable_id"] + variable_name = request.GET["variable_name"] + channel = request.GET["channel"] group_id = request.GET["group_id"] - plot = Plots.objects.get(experiment=experiment_id, - group=group_id, - observation=observation_id, - variable=variable_id) - serializer = PlotSerializer(plot) + plots = Plots.objects.none() + + # invalid input + if owner_id == "null": + serializer = PlotSerializer(plots, many=True) + return Response( + {"error": "Please specify a username. The 'null' value is not a valid username."}, + status=400) + + # get plots by owner field + elif experiment_id == "null": + experiments = Experiments.objects.filter(owner_id=owner_id) + plots = Plots.objects.filter(experiment_id__in=experiments) + + # check if owner and experiment are selected + if owner_id != "null" and experiment_id != "null": + # verify experiment is part of owner + experiments = Experiments.objects.filter(owner_id=owner_id) + current_experiment = Experiments.objects.get(experiment_id=experiment_id) + error_msg = ("The selected experiment cannot be found with the given username. " + "Please make sure both the username and experiment exists " + "and experiment is a part of that username.") + assert current_experiment in experiments, error_msg + # get plots by experiment field + if observation_id == "null": + plots = Plots.objects.filter(experiment_id=experiment_id) + + # get plots by observation field + elif variable_name == "null": + plots = Plots.objects.filter(experiment_id=experiment_id, + observation_id=observation_id) + + # get plots by variable field + elif group_id == "null": + # lookup variable id by variable name and channel + if channel == "null": + channel = None + variable_id = Variables.objects.get( + variable_name=variable_name, channel=channel).variable_id + plots = Plots.objects.filter( + experiment_id=experiment_id, + observation_id=observation_id, + variable_id=variable_id) + + elif group_id != "": + if channel == "null": + channel = None + variable_id = Variables.objects.get(variable_name=variable_name, channel=channel) + plots = Plots.objects.filter(experiment=experiment_id, + group=group_id, + observation=observation_id, + variable_id=variable_id) + + serializer = PlotSerializer(plots, many=True) return Response(serializer.data) + except AssertionError as e: + return Response({"error": str(e)}, status=400) + except ValueError as e: return Response({"error": str(e)}, status=400) @@ -54,7 +108,7 @@ def get_plot_components(request): return Response({"error": error_msg}, status=400) except ObjectDoesNotExist as e: - return Response({"error": str(e)}, status=400) + return Response({"error": str(e)}, status=404) @api_view(['GET']) @@ -87,7 +141,7 @@ def update_user_option(request): return Response({"error": error_msg}, status=400) except ObjectDoesNotExist as e: - return Response({"error": str(e)}, status=400) + return Response({"error": str(e)}, status=404) @api_view(['GET']) @@ -116,7 +170,7 @@ def update_experiment_option(request): return Response({"error": error_msg}, status=400) except ObjectDoesNotExist as e: - return Response({"error": str(e)}, status=400) + return Response({"error": str(e)}, status=404) @api_view(['GET']) @@ -126,9 +180,15 @@ def update_observation_option(request): Observations.objects.get(pk=observation_id) data = { "variables": [], - "groups": [] + "groups": [], + "variablesMap": {} } data["variables"] = get_variables_by_observation(observation_id) + variablesMap = {} + for variable in data["variables"]: + variablesMap[variable['variable_name']] = variablesMap.get( + variable['variable_name'], []) + [variable['channel']] + data["variablesMap"] = variablesMap if len(data["variables"]) > 0: data["groups"] = get_groups_by_variable(data["variables"][0]["variable_id"]) return Response(data) @@ -141,20 +201,45 @@ def update_observation_option(request): return Response({"error": error_msg}, status=400) except ObjectDoesNotExist as e: - return Response({"error": str(e)}, status=400) + return Response({"error": str(e)}, status=404) @api_view(['GET']) def update_variable_option(request): try: - variable_id = request.GET["variable_id"] - Variables.objects.get(pk=variable_id) + variable_name = request.GET["variable_name"] + channel = request.GET["channel"] + variable_id = None + + if channel == "null": + # check if variable does not include a channel by default, + # otherwise it has not been configured yet in the PlotMenu + queryset = Variables.objects.filter(variable_name=variable_name, channel=None) + if len(queryset) == 1: + variable_id = queryset[0].variable_id + # pull the top channel from variable name + else: + queryset = Variables.objects.filter(variable_name=variable_name) + assert len(queryset) > 0 + channel = queryset[0].channel + variable_id = queryset[0].variable_id + # get variable id from variable name and channel + else: + variable_id = Variables.objects.get( + variable_name=variable_name, channel=channel).variable_id + data = { - "groups": [] + "groups": [], + "channel": channel } data["groups"] = get_groups_by_variable(variable_id) return Response(data) + except AssertionError as e: + error_msg = ("Unable to locate the given variable name. " + "Make sure a channel option is selected if exists.") + return Response({"error": error_msg}, status=404) + except ValueError as e: return Response({"error": str(e)}, status=400) @@ -163,7 +248,7 @@ def update_variable_option(request): return Response({"error": error_msg}, status=400) except ObjectDoesNotExist as e: - return Response({"error": str(e)}, status=400) + return Response({"error": str(e)}, status=404) def get_owners(): diff --git a/src/evagram/website/backend/backend/settings.py b/src/evagram/website/backend/backend/settings.py index 3b02a52..54fe6d5 100644 --- a/src/evagram/website/backend/backend/settings.py +++ b/src/evagram/website/backend/backend/settings.py @@ -15,6 +15,7 @@ import os load_dotenv() +pg_host = os.environ.get('DB_HOST') pg_password = os.environ.get('DB_PASSWORD') # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -90,8 +91,8 @@ 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'evagram', 'USER': 'postgres', - 'HOST': '127.0.0.1', 'PORT': 5432, + 'HOST': pg_host, 'PASSWORD': pg_password } } diff --git a/src/evagram/website/backend/loaddata.py b/src/evagram/website/backend/loaddata.py new file mode 100644 index 0000000..6633bb5 --- /dev/null +++ b/src/evagram/website/backend/loaddata.py @@ -0,0 +1,3 @@ +from evagram_input import * + +input_data(owner='postgres', experiment='experiment1', eva_directory='tests/new') diff --git a/src/evagram/website/backend/requirements.txt b/src/evagram/website/backend/requirements.txt new file mode 100644 index 0000000..c9eb4d5 --- /dev/null +++ b/src/evagram/website/backend/requirements.txt @@ -0,0 +1,12 @@ +asgiref==3.7.2 +autopep8==2.0.4 +Django==4.2.10 +django-cors-headers==4.3.1 +djangorestframework==3.14.0 +psycopg2-binary==2.9.9 +pycodestyle>=2.8.0 +python-dotenv==1.0.0 +pytz==2023.3.post1 +setuptools>=59.4.0 +sqlparse==0.4.4 +tzdata==2023.4 diff --git a/src/evagram/website/frontend/.gitignore b/src/evagram/website/frontend/.gitignore index 4d29575..e8ea278 100644 --- a/src/evagram/website/frontend/.gitignore +++ b/src/evagram/website/frontend/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +.prettierrc + # dependencies /node_modules /.pnp diff --git a/src/evagram/website/frontend/Dockerfile b/src/evagram/website/frontend/Dockerfile new file mode 100644 index 0000000..8b0fb0e --- /dev/null +++ b/src/evagram/website/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:latest + +WORKDIR /frontend + +COPY package*.json . +RUN npm install + +COPY public public +COPY src src + +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/src/evagram/website/frontend/package-lock.json b/src/evagram/website/frontend/package-lock.json index 62f5908..1b20601 100644 --- a/src/evagram/website/frontend/package-lock.json +++ b/src/evagram/website/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@bokeh/bokehjs": "^3.4.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -2018,6 +2019,74 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@bokeh/bokehjs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@bokeh/bokehjs/-/bokehjs-3.4.1.tgz", + "integrity": "sha512-s7eCG9gEcGE5/e+eSCIS26rtqJ5G5gilnIOjPRwmulQ5I8XafZvRlCOZBuZmTvrI1jtK3QOdqC/YX4+GYMfHhw==", + "license": "BSD-3-Clause", + "workspaces": [ + "./make", + "./src/compiler", + "./src/lib", + "./src/server", + "./test" + ], + "dependencies": { + "@bokeh/numbro": "^1.6.2", + "@bokeh/slickgrid": "~2.4.4103", + "@types/geojson": "^7946.0.13", + "@types/google.maps": "^3.54.10", + "@types/proj4": "^2.5.5", + "@types/sprintf-js": "^1.1.4", + "choices.js": "^10.2.0", + "flatbush": "^4.2.0", + "flatpickr": "^4.6.13", + "mathjax-full": "^3.2.2", + "nouislider": "^15.7.1", + "proj4": "^2.9.2", + "regl": "^2.1.0", + "sprintf-js": "^1.1.3", + "timezone": "^1.0.23", + "tslib": "^2.6.2", + "underscore.template": "^0.1.7" + }, + "engines": { + "node": ">=16.0", + "npm": ">=8.0" + } + }, + "node_modules/@bokeh/bokehjs/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/@bokeh/numbro": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@bokeh/numbro/-/numbro-1.6.2.tgz", + "integrity": "sha512-owIECPc3T3QXHCb2v5Ez+/uE9SIxI7N4nd9iFlWnfBrOelr0/omvFn09VisRn37AAFAY39sJiCVgECwryHWUPA==", + "engines": { + "node": "*" + } + }, + "node_modules/@bokeh/slickgrid": { + "version": "2.4.4103", + "resolved": "https://registry.npmjs.org/@bokeh/slickgrid/-/slickgrid-2.4.4103.tgz", + "integrity": "sha512-wPitQJNUNUHw+86BPemvb1kIuV6UB4CpjEEDIXpU8G6Jges+8nx/iGHW8w9BwvICdeyhAmfX9Hmn5vRJMbe+uw==", + "license": "MIT", + "dependencies": { + "@types/slickgrid": "^2.1.30", + "jquery": ">=3.4.0", + "jquery-ui": ">=1.8.0", + "tslib": "^1.10.0" + } + }, + "node_modules/@bokeh/slickgrid/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@csstools/normalize.css": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", @@ -4114,6 +4183,18 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.55.10", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.10.tgz", + "integrity": "sha512-XbDu2MIvcKgN+MBrufjWcsQRtXTbrBGBKperbhMLnPSq4770+pvlR66Oqq/Ub4AVkmGc9QciCfwPZpVCLaKAOw==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4397,6 +4478,15 @@ "node": ">=8" } }, + "node_modules/@types/jquery": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.30.tgz", + "integrity": "sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==", + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4438,6 +4528,12 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, + "node_modules/@types/proj4": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.5.tgz", + "integrity": "sha512-y4tHUVVoMEOm2nxRLQ2/ET8upj/pBmoutGxFw2LZJTQWPgWXI+cbxVEUFFmIzr/bpFR83hGDOTSXX6HBeObvZA==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -4526,6 +4622,21 @@ "@types/node": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "license": "MIT" + }, + "node_modules/@types/slickgrid": { + "version": "2.1.40", + "resolved": "https://registry.npmjs.org/@types/slickgrid/-/slickgrid-2.1.40.tgz", + "integrity": "sha512-+HWaipRr4bw0cy4eS8PCWxo+Fi66aL+mh36unRLAWtatAvMnxpldTKfpe9T7zdNtgN+rq+/faux3JzFR1mj/dg==", + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -4534,6 +4645,12 @@ "@types/node": "*" } }, + "node_modules/@types/sprintf-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.4.tgz", + "integrity": "sha512-aWK1reDYWxcjgcIIPmQi3u+OQDuYa9b+lr6eIsGWrekJ9vr1NSjr4Eab8oQ1iKuH1ltFHpXGyerAv1a3FMKxzQ==", + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -6032,6 +6149,17 @@ "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" }, + "node_modules/choices.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-10.2.0.tgz", + "integrity": "sha512-8PKy6wq7BMjNwDTZwr3+Zry6G2+opJaAJDDA/j3yxvqSCnvkKe7ZIFfIyOhoc7htIWFhsfzF9tJpGUATcpUtPg==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "fuse.js": "^6.6.2", + "redux": "^4.2.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -8027,6 +8155,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8435,6 +8572,30 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatbush": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/flatbush/-/flatbush-4.4.0.tgz", + "integrity": "sha512-cf6G+sfy/+/FLH7Ls1URQ5GCRlXgwgqUZiEsMNrMZqb3Us3EkKmzUlKbnyoBy/4wI4oLJ+8cyCQoKJIVm92Fmg==", + "license": "ISC", + "dependencies": { + "flatqueue": "^2.0.3" + } + }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, + "node_modules/flatqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-2.0.3.tgz", + "integrity": "sha512-RZCWZNkmxzUOh8jqEcEGZCycb3B8KAfpPwg3H//cURasunYxsg1eIvE+QDSjX+ZPHTIVfINfK1aLTrVKKO0i4g==", + "license": "ISC", + "engines": { + "node": ">= 12.17.0" + } + }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -8759,6 +8920,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12241,6 +12411,21 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/jquery-ui": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.3.tgz", + "integrity": "sha512-D2YJfswSJRh/B8M/zCowDpNFfwsDmtfnMPwjJTyvl+CBqzpYwQ+gFYIbUUlzijy/Qvoy30H1YhoSui4MNYpRwA==", + "license": "MIT", + "dependencies": { + "jquery": ">=1.8.0 <4.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12627,6 +12812,18 @@ "tmpl": "1.0.5" } }, + "node_modules/mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12677,6 +12874,18 @@ "node": ">= 0.6" } }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -12834,6 +13043,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -12966,6 +13181,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nouislider": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.0.tgz", + "integrity": "sha512-E7Rvt2Uc5IIl7Hv7+mO8XH8uFyqe3ofxi5j2QMi2B7w8JHVLAEzTpgDBk0oYZBaplsk60QOso3qrq8qk3JPHEQ==", + "license": "MIT" + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -14823,6 +15044,16 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/proj4": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz", + "integrity": "sha512-SasuTkAx8HnWQHfIyhkdUNJorSJqINHAN3EyMWYiQRVorftz9DHz650YraFgczwgtHOxqnfuDxSNv3C8MUnHeg==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.3.3" + } + }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -15314,6 +15545,15 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -15418,6 +15658,12 @@ "jsesc": "bin/jsesc" } }, + "node_modules/regl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.0.tgz", + "integrity": "sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg==", + "license": "MIT" + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -16199,6 +16445,29 @@ "wbuf": "^1.7.3" } }, + "node_modules/speech-rule-engine": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz", + "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==", + "license": "Apache-2.0", + "dependencies": { + "commander": "9.2.0", + "wicked-good-xpath": "1.3.0", + "xmldom-sre": "0.1.31" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", + "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -16993,6 +17262,12 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/timezone": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/timezone/-/timezone-1.0.23.tgz", + "integrity": "sha512-yhQgk6qmSLB+TF8HGmApZAVI5bfzR1CoKUGr+WMZWmx75ED1uDewAZA8QMGCQ70TEv4GmM8pDB9jrHuxdaQ1PA==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -17265,6 +17540,12 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/underscore.template": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/underscore.template/-/underscore.template-0.1.7.tgz", + "integrity": "sha512-wQ25n9/J0Olo3A92QDbSCkD04FKRrjVOn6wooaFDKsrmV9alkc7vZ3MmldbVo4NbJCk9ycWSEcfGn4NzFZIcvA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -17950,6 +18231,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, + "node_modules/wkt-parser": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz", + "integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -18395,6 +18688,15 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xmldom-sre": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", + "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==", + "license": "(LGPL-2.0 or MIT)", + "engines": { + "node": ">=0.1" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/src/evagram/website/frontend/package.json b/src/evagram/website/frontend/package.json index 0d29dbe..5ba3ae8 100644 --- a/src/evagram/website/frontend/package.json +++ b/src/evagram/website/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@bokeh/bokehjs": "^3.4.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/src/evagram/website/frontend/public/index.html b/src/evagram/website/frontend/public/index.html index 020e4ac..03e6d50 100644 --- a/src/evagram/website/frontend/public/index.html +++ b/src/evagram/website/frontend/public/index.html @@ -10,7 +10,10 @@ content="Web site created using create-react-app" /> - + + + +