Compare commits
193 Commits
87a2430afa
...
jpd-feat-a
Author | SHA1 | Date | |
---|---|---|---|
a90cdefa1e
|
|||
10d7f15598 | |||
b00c96fa5c
|
|||
77c51b176d | |||
5941f7fe6c | |||
5074a28507
|
|||
f23bbcb557
|
|||
d687c8f467 | |||
9294c274dc
|
|||
f111cc1d2d | |||
05a1336477 | |||
d7b3a3b87e
|
|||
01ff79373d
|
|||
3797c0592f | |||
541d29ae77 | |||
0ee1a5b76c | |||
99685d5a83
|
|||
68d8b986ac | |||
0385353b54 | |||
f978b5bdfb | |||
0e9ca8330c | |||
b340092dc1
|
|||
3cb77cdf46 | |||
37e05d2c0a | |||
64143b3537 | |||
380a1dc974
|
|||
c481c42e5b
|
|||
49e990935c
|
|||
3a0bc3621f | |||
d4299d4d7a | |||
f54755df10
|
|||
b9bb80fac7 | |||
613c028a67 | |||
4c0aeaed3a | |||
1873dcbd2e
|
|||
70914b9c2f
|
|||
068bccf7d9 | |||
801624232a | |||
eba513c929 | |||
f0c78f6dff | |||
a196b77864 | |||
33a65e77ac
|
|||
1ed2d89cd4 | |||
0bf11bb809
|
|||
706f065dc8
|
|||
8d07a04b95
|
|||
acf2143177 | |||
1d9dd6b3ad | |||
9e01c57dc2 | |||
50e51715dc | |||
aa94db5590 | |||
7478829cbd | |||
04c80e314c | |||
ab2ade294e | |||
81ea8bc06b
|
|||
8a6c0d73df | |||
30a913bca5 | |||
d2b7ac73aa | |||
18759bf234
|
|||
d385451a5e
|
|||
eadd2f5988 | |||
395ee2eca7 | |||
27ff265d47 | |||
bd894e4aac | |||
dec73629e9 | |||
1967275b7e | |||
d916795d1d | |||
2521072af1 | |||
97b1ef9a6b
|
|||
c43d1774ec | |||
42a5712d38 | |||
0ca674f76c | |||
4b643202c3
|
|||
adfb85533b
|
|||
d0114d9c46 | |||
fb001fc8b5 | |||
f5d001d419
|
|||
70582003de | |||
aef062dcf5
|
|||
02540e1046
|
|||
ab97a6c14f | |||
69cd36d7e1
|
|||
f6cfff14e7
|
|||
216015fb82 | |||
ffa2a68b7a
|
|||
26b72afd5a | |||
fb38910137
|
|||
6ba171343d
|
|||
0fc8c56c41
|
|||
c77d4eb97d
|
|||
df16f2ce1c
|
|||
5e029670eb | |||
25cb7508bb
|
|||
84cf6758bb
|
|||
7e041ae6c9
|
|||
82d7009b34
|
|||
fa65a36852
|
|||
1e36c29180
|
|||
8c89b55487
|
|||
a427e11958
|
|||
c9e3bf68ae
|
|||
7670dce630
|
|||
f3846cad24
|
|||
27a7a05584
|
|||
fc0d9db7a5
|
|||
0ae8bf53a2 | |||
d9dcfa3846
|
|||
235396f041 | |||
dbb9fed9f8 | |||
3a69b8d4ff
|
|||
bb149d1137 | |||
7c15e1fae6
|
|||
8328bbe9fb
|
|||
90613635fb | |||
9be7566cbb | |||
0a4e262b91 | |||
bcdabe41c3 | |||
dc6e00d54b | |||
5c044e69d9
|
|||
824a70452f
|
|||
4d545074f5 | |||
b696cc129d | |||
4195fc615f | |||
c923b2efb7
|
|||
8e9907a3e8
|
|||
39fd238ed8 | |||
3914bd5240
|
|||
2fa1a773f2 | |||
8dffb86d01
|
|||
866730238e
|
|||
eeb6964348
|
|||
3f83808335 | |||
0d7a4cc9c3
|
|||
7fab746ea8
|
|||
c81e4fda67
|
|||
bc9c2124f1
|
|||
c14a26a184
|
|||
79aec9f7e5
|
|||
2545a796c3
|
|||
88b8e811a8
|
|||
fa022b9592
|
|||
0ac434c61d
|
|||
06b35cbceb
|
|||
28f0546bcb
|
|||
c175c3edfd
|
|||
4234b2e917
|
|||
04fff60541
|
|||
b415b38184
|
|||
412250d7ef
|
|||
d3c1aa9b6b
|
|||
025ab1537d
|
|||
52c1d0c028
|
|||
902a22c947
|
|||
a8466f0c1d
|
|||
|
bb79aa2f3a | ||
500d0e89bb
|
|||
8be9fa38bb
|
|||
9c8e1d20ee
|
|||
2fedb4ecf0
|
|||
58f14c0346
|
|||
ece17e3cf9 | |||
e616d01e63 | |||
9aeaaabc35 | |||
de9fa663c9 | |||
037f4d7504 | |||
9bd41ba3b8 | |||
dbce23e51a | |||
2fca7bc67d | |||
0f4cedf078 | |||
24d75a2fd8 | |||
1b21556d2c | |||
4d09f20e69 | |||
42ae54b17e | |||
93e1719d0d | |||
7420139650 | |||
62219ecc9f | |||
88de0beeac | |||
d7c99b099d | |||
e771d55589 | |||
1c0a0b6853 | |||
2bd91423a4 | |||
ba1a8c7baa | |||
5f3a5a1df1 | |||
748d7f3381 | |||
dc9a7a500b | |||
42f8276204 | |||
fbba19f4a7 | |||
f8cb4ddfbf | |||
2039b43c53 | |||
22b3823d8e | |||
7befc86adc | |||
39ff0979c7 | |||
a30611f310 |
33
.dockerignore
Normal file
33
.dockerignore
Normal file
@@ -0,0 +1,33 @@
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
docker-compose.*.yml
|
||||
|
||||
# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
|
||||
#
|
||||
# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
|
||||
# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
|
||||
.git
|
||||
!.git/HEAD
|
||||
!.git/refs
|
||||
|
||||
# Common development/test artifacts
|
||||
/cover/
|
||||
/doc/
|
||||
/test/
|
||||
/tmp/
|
||||
/.elixir_ls
|
||||
/.elixir-tools/
|
||||
|
||||
# Mix artifacts
|
||||
/_build/
|
||||
/deps/
|
||||
*.ez
|
||||
|
||||
# Generated on crash by the VM
|
||||
erl_crash.dump
|
||||
|
||||
# Static artifacts - These should be fetched and built inside the Docker image
|
||||
/assets/node_modules/
|
||||
/priv/plts/
|
||||
/priv/static/assets/
|
||||
/priv/static/cache_manifest.json
|
140
.drone.yml
140
.drone.yml
@@ -1,24 +1,26 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: test
|
||||
name: test and lint
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
|
||||
steps:
|
||||
- name: database healthcheck
|
||||
image: 'postgres:14.5-alpine'
|
||||
image: &postgres 'postgres:17.5-alpine'
|
||||
environment:
|
||||
PGUSER: postgres
|
||||
PGPASSWORD: postgres
|
||||
PGHOST: db
|
||||
PGUSER: &db_user postgres
|
||||
PGPASSWORD: &db_pass postgres
|
||||
PGHOST: &db_host db
|
||||
commands:
|
||||
- while ! pg_isready; do sleep 1; done
|
||||
depends_on:
|
||||
- 'db'
|
||||
|
||||
- name: restore cache
|
||||
image: 'meltwater/drone-cache:v1.3.0'
|
||||
image: &drone_cache 'meltwater/drone-cache:v1.4.0'
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: minio_user
|
||||
@@ -27,7 +29,7 @@ steps:
|
||||
settings:
|
||||
archive_format: gzip
|
||||
bucket: trainlog-cache
|
||||
cache_key: '{{ .Repo.Name }}-{{ checksum "mix.lock" }}'
|
||||
cache_key: '{{ .Repo.Name }}-{{ checksum ".tool-versions" }}-{{ checksum "mix.lock" }}-{{ checksum "Dockerfile" }}'
|
||||
endpoint: minio:9000
|
||||
mount:
|
||||
- _build
|
||||
@@ -35,26 +37,100 @@ steps:
|
||||
path_style: true
|
||||
region: us-east-1
|
||||
restore: true
|
||||
volumes: &volumes
|
||||
- name: app_build
|
||||
path: /drone/src/_build
|
||||
- name: app_deps
|
||||
path: /drone/src/deps
|
||||
|
||||
- name: dependencies and compile
|
||||
image: &elixir 'hexpm/elixir:1.18.4-erlang-27.3.4-debian-bookworm-20250520-slim'
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force, deps.get, deps.compile
|
||||
- mix compile --force --all-warnings --warnings-as-errors
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'restore cache'
|
||||
|
||||
- name: test
|
||||
image: 'elixir:1.14.4'
|
||||
image: *elixir
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASS: postgres
|
||||
POSTGRES_HOST: *db_host
|
||||
POSTGRES_USER: *db_user
|
||||
POSTGRES_PASS: *db_pass
|
||||
commands:
|
||||
- mix do local.rebar --force, local.hex --force, deps.get, deps.compile
|
||||
- make test
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix test --cover --trace --slowest 10
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'database healthcheck'
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: lint
|
||||
image: 'elixir:1.14.4'
|
||||
- name: audit deps
|
||||
image: *elixir
|
||||
commands:
|
||||
- mix do local.rebar --force, local.hex --force, deps.get, deps.compile
|
||||
- make static_code_analysis
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix hex.audit
|
||||
- mix deps.audit
|
||||
- mix deps.unlock --check-unused
|
||||
# - mix hex.outdated
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: format check
|
||||
image: *elixir
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix format --dry-run --check-formatted
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: credo check
|
||||
image: *elixir
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix credo suggest --strict --format=flycheck
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: dialyzer check
|
||||
image: *elixir
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix dialyzer --no-check --quiet --ignore-exit-status --format short
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: sobelow check
|
||||
image: *elixir
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y git make
|
||||
- mix do local.rebar --force, local.hex --force
|
||||
- mix sobelow
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'dependencies and compile'
|
||||
|
||||
- name: rebuild cache
|
||||
image: 'meltwater/drone-cache:v1.3.0'
|
||||
image: *drone_cache
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: minio_user
|
||||
@@ -63,7 +139,7 @@ steps:
|
||||
settings:
|
||||
archive_format: gzip
|
||||
bucket: trainlog-cache
|
||||
cache_key: '{{ .Repo.Name }}-{{ checksum "mix.lock" }}'
|
||||
cache_key: '{{ .Repo.Name }}-{{ checksum ".tool-versions" }}-{{ checksum "mix.lock" }}-{{ checksum "Dockerfile" }}'
|
||||
endpoint: minio:9000
|
||||
exit_code: true
|
||||
mount:
|
||||
@@ -72,10 +148,24 @@ steps:
|
||||
path_style: true
|
||||
rebuild: true
|
||||
region: us-east-1
|
||||
volumes: *volumes
|
||||
depends_on:
|
||||
- 'test'
|
||||
- 'audit deps'
|
||||
- 'format check'
|
||||
- 'credo check'
|
||||
- 'dialyzer check'
|
||||
- 'sobelow check'
|
||||
|
||||
services:
|
||||
- name: db
|
||||
image: 'postgres:14.5-alpine'
|
||||
- name: *db_host
|
||||
image: *postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: *db_user
|
||||
POSTGRES_PASSWORD: *db_pass
|
||||
|
||||
volumes:
|
||||
- name: app_build
|
||||
temp: {}
|
||||
- name: app_deps
|
||||
temp: {}
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
# paths
|
||||
/.elixir_ls/
|
||||
/.elixir-tools/
|
||||
/_build/
|
||||
/cover/
|
||||
/report/
|
||||
@@ -13,5 +15,5 @@ erl_crash.dump
|
||||
*.ez
|
||||
wabanex-*.tar
|
||||
docker-compose.*.yml
|
||||
priv/docker/pgcli/history
|
||||
priv/docker/pgcli/log
|
||||
/priv/docker/pgcli/history
|
||||
/priv/docker/pgcli/log
|
||||
|
@@ -1,2 +1,3 @@
|
||||
erlang 24.3.4
|
||||
elixir 1.14.5
|
||||
erlang 27.3.4
|
||||
elixir 1.18.4
|
||||
lefthook 1.11.14
|
||||
|
70
Dockerfile
Normal file
70
Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
||||
ARG ELIXIR_VERSION=1.18.4
|
||||
ARG OTP_VERSION=27.3.4
|
||||
ARG DEBIAN_VERSION=bookworm-20250520-slim
|
||||
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
|
||||
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
||||
|
||||
FROM ${BUILDER_IMAGE} AS builder
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install \
|
||||
build-essential \
|
||||
git \
|
||||
make \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/ap/lists/*_*
|
||||
WORKDIR /opt/app
|
||||
# install hex + rebar
|
||||
RUN mix do local.hex --force, local.rebar --force
|
||||
# set build ENV
|
||||
ARG BUILD_MIX_ENV=prod
|
||||
ENV MIX_ENV=${BUILD_MIX_ENV}
|
||||
# install mix dependencies
|
||||
COPY mix.exs mix.lock ./
|
||||
RUN mix deps.get --only ${MIX_ENV}
|
||||
RUN mkdir config
|
||||
# copy compile-time config files before we compile dependencies
|
||||
# to ensure any relevant config change will trigger the dependencies
|
||||
# to be re-compiled.
|
||||
COPY config/config.exs config/${BUILD_MIX_ENV}.exs config/
|
||||
RUN mix deps.compile
|
||||
COPY priv priv
|
||||
COPY lib lib
|
||||
# Compile the release
|
||||
RUN mix compile
|
||||
# Changes to config/runtime.exs don't require recompiling the code
|
||||
COPY config/runtime.exs config/
|
||||
COPY rel rel
|
||||
RUN mix release
|
||||
COPY ./mix.exs ./
|
||||
EXPOSE 4000
|
||||
ENTRYPOINT ["./priv/docker/service/docker-entrypoint.sh"]
|
||||
CMD ["sample-cookie"]
|
||||
|
||||
# start a new build stage so that the final image will only contain
|
||||
# the compiled release and other runtime necessities
|
||||
FROM ${RUNNER_IMAGE}
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y \
|
||||
ca-certificates \
|
||||
libncurses5 \
|
||||
libstdc++6 \
|
||||
locales \
|
||||
openssl \
|
||||
tini \
|
||||
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
|
||||
# Set the locale
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
WORKDIR /opt/app
|
||||
RUN chown nobody /opt/app
|
||||
# set runner ENV
|
||||
ARG BUILD_MIX_ENV=prod
|
||||
ENV MIX_ENV=${BUILD_MIX_ENV}
|
||||
# Only copy the final release from the build stage
|
||||
COPY --from=builder --chown=nobody:root /app/_build/${BUILD_MIX_ENV}/rel/wabanex ./
|
||||
USER nobody
|
||||
EXPOSE 4000
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["/app/bin/server"]
|
55
Makefile
55
Makefile
@@ -1,9 +1,13 @@
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
COMPOSE = docker-compose -f docker-compose.yml -f docker-compose.override.yml
|
||||
COMPOSE = docker compose
|
||||
|
||||
.PHONY: system_setup
|
||||
system_setup: ## setup system deps
|
||||
@lefthook install
|
||||
|
||||
.PHONY: setup
|
||||
setup: ## setup project
|
||||
setup: system_setup ## setup project
|
||||
@mkdir -p priv/plts
|
||||
@mix do local.rebar --force, local.hex --force
|
||||
@mix do deps.get, deps.compile
|
||||
@@ -13,33 +17,64 @@ setup: ## setup project
|
||||
check_format: ## run format checker
|
||||
@mix format --check-formatted
|
||||
|
||||
.PHONY: check_compile
|
||||
check_compile: ## run compile
|
||||
@mix compile --warnings-as-errors --force
|
||||
|
||||
.PHONY: credo
|
||||
credo: ## run credo
|
||||
@mix credo suggest --strict --format=flycheck
|
||||
|
||||
.PHONY: dialyzer
|
||||
dialyzer: ## run dialyzer
|
||||
@mix dialyzer --no-check --quiet --ignore-exit-status --format short
|
||||
@mix dialyzer --format short
|
||||
|
||||
.PHONY: static_code_analysis
|
||||
static_code_analysis: check_format credo dialyzer ## run static code analysis
|
||||
static_code_analysis: check_format check_compile credo dialyzer ## run static code analysis
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## create documentation files
|
||||
@mix docs
|
||||
|
||||
.PHONY: test
|
||||
test: ## run tests
|
||||
@mix test --cover --trace --slowest 10
|
||||
|
||||
.PHONY: compose_up
|
||||
compose_up: ## start containers for this service
|
||||
@$(COMPOSE) up -d
|
||||
.PHONY: compose_database_create
|
||||
compose_database_create:
|
||||
@$(COMPOSE) run --rm --entrypoint mix app ecto.create
|
||||
|
||||
.PHONY: compose_test
|
||||
compose_test: ## run tests in containers
|
||||
@$(COMPOSE) run -e MIX_ENV=test --entrypoint make app test
|
||||
.PHONY: compose_database_migrate
|
||||
compose_database_migrate: ## apply migrations to our database
|
||||
@$(COMPOSE) run --rm --entrypoint mix app ecto.migrate
|
||||
|
||||
.PHONY: compose_database_setup
|
||||
compose_database_setup: compose_database_create compose_database_migrate ## create and apply migrations
|
||||
|
||||
.PHONY: compose_ps
|
||||
compose_ps: ## status of containers
|
||||
@$(COMPOSE) ps
|
||||
|
||||
.PHONY: compose_remote
|
||||
compose_remote: ## connect to remote node
|
||||
@$(COMPOSE) exec app ./priv/docker/service/docker-remote.sh
|
||||
|
||||
.PHONY: compose_test
|
||||
compose_test: ## execute test on docker environment
|
||||
@$(COMPOSE) --profile test run --build --rm test
|
||||
|
||||
.PHONY: compose_test_shell
|
||||
compose_test_shell: ## enter test environment shell
|
||||
@$(COMPOSE) --profile test run --rm --entrypoint bash test -c bash
|
||||
|
||||
.PHONY: compose_up
|
||||
compose_up: ## start containers for this service
|
||||
@$(COMPOSE) up -d
|
||||
|
||||
.PHONY: compose_watch
|
||||
compose_watch: ## start containers for this service watching for updates in filesystem
|
||||
@$(COMPOSE) up -w
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
@@ -1,10 +1,3 @@
|
||||
# This file is responsible for configuring your application
|
||||
# and its dependencies with the aid of the Mix.Config module.
|
||||
#
|
||||
# This configuration file is loaded before any dependency and
|
||||
# is restricted to this project.
|
||||
|
||||
# General application configuration
|
||||
import Config
|
||||
|
||||
config :wabanex,
|
||||
@@ -21,7 +14,6 @@ config :wabanex, Wabanex.PromEx,
|
||||
grafana: :disabled,
|
||||
metrics_server: :disabled
|
||||
|
||||
# Configures the endpoint
|
||||
config :wabanex, WabanexWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "wkyuhU+mCGwXUSBYVKZSRGoFmDYCbOFzdokbVmBBI9JgCWOqGPfuA/3JI5/b4Wdl",
|
||||
@@ -29,14 +21,10 @@ config :wabanex, WabanexWeb.Endpoint,
|
||||
pubsub_server: Wabanex.PubSub,
|
||||
live_view: [signing_salt: "SXtw7DzV"]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
# Use Jason for JSON parsing in Phoenix
|
||||
config :phoenix, :json_library, Jason
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{config_env()}.exs"
|
||||
|
@@ -1,17 +1,14 @@
|
||||
import Config
|
||||
|
||||
# Configure your database
|
||||
config :wabanex,
|
||||
dns_cluster_query: :ignore,
|
||||
dns_cluster_resolver: Wabanex.DevDNSClusterResolver
|
||||
|
||||
config :wabanex, Wabanex.Repo,
|
||||
database: "wabanex_dev",
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we use it
|
||||
# with webpack to recompile .js and .css sources.
|
||||
config :wabanex, WabanexWeb.Endpoint,
|
||||
http: [port: 4000],
|
||||
debug_errors: true,
|
||||
@@ -19,36 +16,8 @@ config :wabanex, WabanexWeb.Endpoint,
|
||||
check_origin: false,
|
||||
watchers: []
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# In order to use HTTPS in development, a self-signed
|
||||
# certificate can be generated by running the following
|
||||
# Mix task:
|
||||
#
|
||||
# mix phx.gen.cert
|
||||
#
|
||||
# Note that this task requires Erlang/OTP 20 or later.
|
||||
# Run `mix help phx.gen.cert` for more information.
|
||||
#
|
||||
# The `http:` config above can be replaced with:
|
||||
#
|
||||
# https: [
|
||||
# port: 4001,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: "priv/cert/selfsigned_key.pem",
|
||||
# certfile: "priv/cert/selfsigned.pem"
|
||||
# ],
|
||||
#
|
||||
# If desired, both `http:` and `https:` keys can be
|
||||
# configured to run both http and https servers on
|
||||
# different ports.
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
@@ -1,55 +1,7 @@
|
||||
import Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
# when generating URLs.
|
||||
#
|
||||
# Note we also include the path to a cache manifest
|
||||
# containing the digested version of static files. This
|
||||
# manifest is generated by the `mix phx.digest` task,
|
||||
# which you should run after static files are built and
|
||||
# before starting your production server.
|
||||
config :wabanex, WabanexWeb.Endpoint,
|
||||
url: [host: "example.com", port: 80],
|
||||
cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
# to the previous section and set your `:url` port to 443:
|
||||
#
|
||||
# config :wabanex, WabanexWeb.Endpoint,
|
||||
# ...
|
||||
# url: [host: "example.com", port: 443],
|
||||
# https: [
|
||||
# port: 443,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
|
||||
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
|
||||
# transport_options: [socket_opts: [:inet6]]
|
||||
# ]
|
||||
#
|
||||
# The `cipher_suite` is set to `:strong` to support only the
|
||||
# latest and more secure SSL ciphers. This means old browsers
|
||||
# and clients may not be supported. You can set it to
|
||||
# `:compatible` for wider support.
|
||||
#
|
||||
# `:keyfile` and `:certfile` expect an absolute path to the key
|
||||
# and cert in disk or a relative path inside priv, for example
|
||||
# "priv/ssl/server.key". For all supported SSL configuration
|
||||
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
|
||||
#
|
||||
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
||||
# no data is ever sent via http, always redirecting to https:
|
||||
#
|
||||
# config :wabanex, WabanexWeb.Endpoint,
|
||||
# force_ssl: [hsts: true]
|
||||
#
|
||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||
|
||||
# Finally import the config/prod.secret.exs which loads secrets
|
||||
# and configuration from environment variables.
|
||||
import_config "prod.secret.exs"
|
||||
|
@@ -1,5 +1,35 @@
|
||||
import Config
|
||||
|
||||
# [warn] Conditional IPv6 support missing from runtime configuration.
|
||||
#
|
||||
# Add the following to your config/runtime.exs:
|
||||
#
|
||||
# maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
|
||||
#
|
||||
# config :wabanex, Wabanex.Repo,
|
||||
# ...,
|
||||
# socket_options: maybe_ipv6
|
||||
#
|
||||
# [warn] Conditional server startup is missing from runtime configuration.
|
||||
#
|
||||
# Add the following to the top of your config/runtime.exs:
|
||||
#
|
||||
# if System.get_env("PHX_SERVER") do
|
||||
# config :wabanex, WabanexWeb.Endpoint, server: true
|
||||
# end
|
||||
#
|
||||
# [warn] Environment based URL export is missing from runtime configuration.
|
||||
#
|
||||
# Add the following to your config/runtime.exs:
|
||||
#
|
||||
# host = System.get_env("PHX_HOST") || "example.com"
|
||||
#
|
||||
# config :wabanex, WabanexWeb.Endpoint,
|
||||
# ...,
|
||||
# url: [host: host, port: 443]
|
||||
|
||||
config :wabanex, dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY") || :ignore
|
||||
|
||||
config :wabanex, Wabanex.Repo,
|
||||
username: System.get_env("POSTGRES_USER") || "postgres",
|
||||
password: System.get_env("POSTGRES_PASS") || "postgres",
|
||||
|
@@ -1,22 +1,17 @@
|
||||
import Config
|
||||
|
||||
# Configure your database
|
||||
#
|
||||
# The MIX_TEST_PARTITION environment variable can be used
|
||||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :wabanex, Growth.Indicators.Download,
|
||||
who_req_options: [plug: {Req.Test, Growth.Indicators.Download.WHO}]
|
||||
|
||||
config :wabanex, Wabanex.Repo,
|
||||
database: "wabanex_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :wabanex, WabanexWeb.Endpoint,
|
||||
http: [port: 4002],
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
config :logger, level: :warning
|
||||
|
||||
config :junit_formatter,
|
||||
report_file: "test_report.xml",
|
||||
|
@@ -1,40 +1,90 @@
|
||||
---
|
||||
version: '3.7'
|
||||
name: ${EX_TRAINER_PROJECT_NAME:-ex_trainer}
|
||||
|
||||
services:
|
||||
db:
|
||||
image: 'postgres:14.5-alpine'
|
||||
image: 'postgres:17.5-alpine'
|
||||
hostname: &db_host db
|
||||
init: true
|
||||
environment:
|
||||
POSTGRES_USER: &db_user postgres
|
||||
POSTGRES_PASSWORD: &db_pass postgres
|
||||
POSTGRES_DB: &db_name postgres
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- './priv/docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql'
|
||||
- 'db_data:/var/lib/postgresql/data'
|
||||
restart: unless-stopped
|
||||
|
||||
app:
|
||||
image: 'elixir:1.14.4'
|
||||
hostname: app
|
||||
image: 'joaodubas/ex_trainer:${EX_TRAINER_TAG:-dev}'
|
||||
build:
|
||||
target: builder
|
||||
context: .
|
||||
args:
|
||||
BUILD_MIX_ENV: dev
|
||||
pull_policy: never
|
||||
hostname: &app_host ex_trainer
|
||||
depends_on:
|
||||
- db
|
||||
init: true
|
||||
develop:
|
||||
watch:
|
||||
- path: ./mix.lock
|
||||
action: rebuild
|
||||
- path: ./
|
||||
ignore:
|
||||
- ./build/
|
||||
- ./deps/
|
||||
- ./.elixir_ls/
|
||||
- ./cover/
|
||||
- ./report/
|
||||
target: /opt/app
|
||||
action: sync+restart
|
||||
environment:
|
||||
DNS_CLUSTER_QUERY: *app_host
|
||||
POSTGRES_HOST: *db_host
|
||||
POSTGRES_USER: *db_user
|
||||
POSTGRES_PASS: *db_pass
|
||||
POSTGRES_NAME: wabanex_dev
|
||||
volumes:
|
||||
- '.:/opt/app'
|
||||
- 'app_build:/opt/app/_build'
|
||||
- 'app_deps:/opt/app/deps'
|
||||
working_dir: /opt/app
|
||||
restart: unless-stopped
|
||||
entrypoint: sleep
|
||||
command: infinity
|
||||
scale: 3
|
||||
entrypoint: ./priv/docker/service/docker-entrypoint.sh
|
||||
command: local-cookie
|
||||
|
||||
test:
|
||||
image: 'joaodubas/ex_trainer:test'
|
||||
build:
|
||||
target: builder
|
||||
context: .
|
||||
args:
|
||||
BUILD_MIX_ENV: test
|
||||
pull_policy: never
|
||||
profiles:
|
||||
- test
|
||||
hostname: ex_trainer_test
|
||||
depends_on:
|
||||
- db
|
||||
init: true
|
||||
volumes:
|
||||
- './mix.exs:/opt/app/mix.exs:ro'
|
||||
- './mix.lock:/opt/app/mix.lock:ro'
|
||||
- './students.csv:/opt/app/students.csv:ro'
|
||||
- './config:/opt/app/config'
|
||||
- './cover:/opt/app/cover'
|
||||
- './lib:/opt/app/lib'
|
||||
- './priv:/opt/app/priv'
|
||||
- './report:/opt/app/report'
|
||||
- './test:/opt/app/test'
|
||||
environment:
|
||||
POSTGRES_HOST: *db_host
|
||||
POSTGRES_USER: *db_user
|
||||
POSTGRES_PASS: *db_pass
|
||||
POSTGRES_NAME: wabanex_test
|
||||
working_dir: /opt/app
|
||||
restart: never
|
||||
entrypoint: mix test
|
||||
command: --trace --cover --slowest 10
|
||||
|
||||
volumes:
|
||||
db_data: {}
|
||||
app_build: {}
|
||||
app_deps: {}
|
||||
|
20
lefthook.yml
Normal file
20
lefthook.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
compiling:
|
||||
glob: &elixir_files '*.{ex,exs}'
|
||||
run: make check_compile
|
||||
formating:
|
||||
glob: *elixir_files
|
||||
run: make check_format
|
||||
linting:
|
||||
glob: *elixir_files
|
||||
run: make credo
|
||||
typing:
|
||||
glob: *elixir_files
|
||||
run: make dialyzer
|
||||
pre-push:
|
||||
commands:
|
||||
testing:
|
||||
run: make compose_test
|
48
lib/growth/calc/age.ex
Normal file
48
lib/growth/calc/age.ex
Normal file
@@ -0,0 +1,48 @@
|
||||
defmodule Growth.Calc.Age do
|
||||
@moduledoc """
|
||||
Calculate the age in months.
|
||||
"""
|
||||
|
||||
@type precision :: :day | :week | :month | :year
|
||||
|
||||
# NOTE: (jpd): based on [WHO instructions][0]
|
||||
# [0]: https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/instructions-en.pdf
|
||||
@day_in_month 30.4375
|
||||
|
||||
@day_in_week 7.0
|
||||
|
||||
@doc """
|
||||
Calculate the age with the precision of `:day`, or `:week`, or `:month`, considering the measurement date as today.
|
||||
|
||||
For age in weeks or months, considers completed ones, removing decimal value.
|
||||
"""
|
||||
@spec calculate(precision(), Date.t(), Date.t()) :: pos_integer()
|
||||
|
||||
def calculate(precision, date_of_birth) do
|
||||
calculate(precision, date_of_birth, Date.utc_today())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the age with the precision of `:day`, or `:week`, or `:month`.
|
||||
|
||||
For age in weeks or months, considers completed ones, removing decimal value.
|
||||
"""
|
||||
|
||||
def calculate(:month, date_of_birth, date_of_measurement) do
|
||||
:day
|
||||
|> calculate(date_of_birth, date_of_measurement)
|
||||
|> Kernel./(@day_in_month)
|
||||
|> floor()
|
||||
end
|
||||
|
||||
def calculate(:week, date_of_birth, date_of_measurement) do
|
||||
:day
|
||||
|> calculate(date_of_birth, date_of_measurement)
|
||||
|> Kernel./(@day_in_week)
|
||||
|> floor()
|
||||
end
|
||||
|
||||
def calculate(:day, date_of_birth, date_of_measurement) do
|
||||
Date.diff(date_of_measurement, date_of_birth)
|
||||
end
|
||||
end
|
29
lib/growth/calc/bmi.ex
Normal file
29
lib/growth/calc/bmi.ex
Normal file
@@ -0,0 +1,29 @@
|
||||
defmodule Growth.Calc.BMI do
|
||||
@moduledoc """
|
||||
Calculate body mass index
|
||||
"""
|
||||
|
||||
@inch_to_centimeter 2.54
|
||||
@pound_to_kilogram 0.453592
|
||||
|
||||
@doc """
|
||||
Calculate the body mass index for a given weight and height.
|
||||
|
||||
Measurements taken in english unit (pounds and inches) are converted to their metric equivalents (kilograms and centimeters).
|
||||
"""
|
||||
@spec calculate(:english | :metric, number, number) :: number
|
||||
|
||||
def calculate(:english, weight, height) do
|
||||
metric_weight = pound_to_kilogram(weight)
|
||||
metric_height = inches_to_centimeters(height)
|
||||
calculate(:metric, metric_weight, metric_height)
|
||||
end
|
||||
|
||||
def calculate(:metric, weight, height) do
|
||||
weight / :math.pow(height / 100.0, 2)
|
||||
end
|
||||
|
||||
defp inches_to_centimeters(height), do: height * @inch_to_centimeter
|
||||
|
||||
defp pound_to_kilogram(weight), do: weight * @pound_to_kilogram
|
||||
end
|
45
lib/growth/calc/centile.ex
Normal file
45
lib/growth/calc/centile.ex
Normal file
@@ -0,0 +1,45 @@
|
||||
defmodule Growth.Calc.Centile do
|
||||
@moduledoc """
|
||||
Calculate the value of measurement at a given z-score and fitted values of Box-Cox by age/height:
|
||||
|
||||
* **power** `t:Growth.Calc.ZScore.l/0`
|
||||
* **median** `t:Growth.Calc.ZScore.m/0`
|
||||
* **coefficientof variation** `t:Growth.Calc.ZScore.s/0`
|
||||
|
||||
This calculation is described in the [instructions provided by the World Health Organization](https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/computation.pdf).
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> measures =
|
||||
...> [
|
||||
...> [-3.0, -1.7862, 16.9392, 0.1107],
|
||||
...> [2, -1.3592, 20.4951, 0.12579],
|
||||
...> [-1.2, -1.6318, 16.049, 0.10038]
|
||||
...> ]
|
||||
iex> Enum.map(measures, &apply(Growth.Calc.Centile, :compute, &1))
|
||||
[13.05127032828574, 27.884359024082663, 14.37765739914362]
|
||||
|
||||
"""
|
||||
|
||||
alias Growth.Calc.ZScore
|
||||
|
||||
@spec compute(number(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na
|
||||
@doc """
|
||||
Compute the measurement value for a given z-score and the Box-Cox values: power (`l`), median (`m`), and coefficient of variation (`s`).
|
||||
|
||||
Returns the measurement based on the z-score when -3 <= z-score <= 3; otherwise, `:na.`
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> Growth.Calc.Centile.compute(0.0, -1.6318, 16.049, 0.10038)
|
||||
16.049
|
||||
|
||||
"""
|
||||
def compute(z_score, l, m, s) when -3 <= z_score and z_score <= 3 do
|
||||
m * :math.pow(1 + l * s * z_score, 1 / l)
|
||||
end
|
||||
|
||||
def compute(_z_score, _l, _m, _s) do
|
||||
:na
|
||||
end
|
||||
end
|
30
lib/growth/calc/percentile.ex
Normal file
30
lib/growth/calc/percentile.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule Growth.Calc.Percentile do
|
||||
@moduledoc """
|
||||
Convert the z-score of a given measurement into a cumulative percentile.
|
||||
|
||||
This calculation is described in the [cumulative distribution function][https://en.wikipedia.org/wiki/Error_function#Cumulative_distribution_function].
|
||||
|
||||
## Examples
|
||||
|
||||
iex> z_scores = [-1.0, 0.0, 1.0]
|
||||
iex> Enum.map(z_scores, &apply(Growth.Calc.Percentile, :compute, [&1]))
|
||||
[0.15865525393145707, 0.5, 0.8413447460685429]
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Convert the z-score of a given measurement into a percentile representation, ranging from 0 to 1.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Growth.Calc.Percentile.compute(2.0)
|
||||
0.9772498680518208
|
||||
iex> Growth.Calc.Percentile.compute(-2.0)
|
||||
0.02275013194817921
|
||||
|
||||
"""
|
||||
@spec compute(number()) :: number()
|
||||
def compute(z_score) do
|
||||
0.5 * (:math.erf(z_score / :math.sqrt(2)) + 1)
|
||||
end
|
||||
end
|
116
lib/growth/calc/z_score.ex
Normal file
116
lib/growth/calc/z_score.ex
Normal file
@@ -0,0 +1,116 @@
|
||||
defmodule Growth.Calc.ZScore do
|
||||
@moduledoc """
|
||||
Calculate z-score for a given measurement and the fitted values of Box-Cox by age/height:
|
||||
|
||||
* **power** `t:l/0`
|
||||
* **median** `t:m/0`
|
||||
* **coefficientof variation** `t:s/0`
|
||||
|
||||
This calculation is described in the [instructions provided by the World Health Organization](https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/computation.pdf).
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> measures =
|
||||
...> [
|
||||
...> [30, -1.7862, 16.9392, 0.1107],
|
||||
...> [14, -1.3592, 20.4951, 0.12579],
|
||||
...> [19, -1.6318, 16.049, 0.10038]
|
||||
...> ]
|
||||
iex> Enum.map(measures, &apply(Growth.Calc.ZScore, :compute, &1))
|
||||
[3.35390255606726, -3.7985108865993493, 1.4698319520484722]
|
||||
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
The **power** value in Box-Cox transformation.
|
||||
"""
|
||||
@type l :: number()
|
||||
@typedoc """
|
||||
The **median** value in Box-Cox transformation.
|
||||
"""
|
||||
@type m :: number()
|
||||
@typedoc """
|
||||
The **coefficient of variation** in Box-Cox transformation.
|
||||
"""
|
||||
@type s :: number()
|
||||
|
||||
@spec compute(number(), l(), m(), s()) :: number()
|
||||
@doc """
|
||||
Compute the adjusted z-score for a given measurement (`y`) and the Box-Cox values: power (`l`), median (`m`), and
|
||||
coefficient of variation (`s`).
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> Growth.Calc.ZScore.compute(30, -1.7862, 16.9392, 0.1107)
|
||||
3.35390255606726
|
||||
iex> Growth.Calc.ZScore.compute(14, -1.3592, 20.4951, 0.12579)
|
||||
-3.7985108865993493
|
||||
iex> Growth.Calc.ZScore.compute(19, -1.6318, 16.049, 0.10038)
|
||||
1.4698319520484722
|
||||
|
||||
"""
|
||||
def compute(y, l, m, s) do
|
||||
y
|
||||
|> raw(l, m, s)
|
||||
|> adjust(y, l, m, s)
|
||||
end
|
||||
|
||||
@spec raw(number(), l(), m(), s()) :: number()
|
||||
@doc """
|
||||
Compute the raw z-score for a given measurement (`y`) and the Box-Cox values, power (`l`), median (`m`), and
|
||||
coefficient of variation (`s`).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Growth.Calc.ZScore.raw(30, -1.7862, 16.9392, 0.1107)
|
||||
3.2353902101095855
|
||||
iex> Growth.Calc.ZScore.raw(14, -1.3592, 20.4951, 0.12579)
|
||||
-3.969714730727475
|
||||
iex> Growth.Calc.ZScore.compute(19, -1.6318, 16.049, 0.10038)
|
||||
1.4698319520484722
|
||||
|
||||
"""
|
||||
def raw(y, l, m, s) do
|
||||
(:math.pow(y / m, l) - 1) / (s * l)
|
||||
end
|
||||
|
||||
@spec adjust(number(), number(), l(), m(), s()) :: number()
|
||||
@doc """
|
||||
Adjust the raw z-score considering that extreme values, beyond -3 and 3 standard deviation, must be handled differently.
|
||||
"""
|
||||
def adjust(zscore, y, l, m, s) when zscore > 3 do
|
||||
[sd2, sd3, _, _] = cutoffs(l, m, s)
|
||||
sd_delta = sd3 - sd2
|
||||
3 + (y - sd3) / sd_delta
|
||||
end
|
||||
|
||||
def adjust(zscore, y, l, m, s) when zscore < -3 do
|
||||
[_, _, sd2, sd3] = cutoffs(l, m, s)
|
||||
sd_delta = sd2 - sd3
|
||||
-3 + (y - sd3) / sd_delta
|
||||
end
|
||||
|
||||
def adjust(zscore, _, _, _, _) do
|
||||
zscore
|
||||
end
|
||||
|
||||
@spec cutoffs(number(), number(), number()) :: [number()]
|
||||
@doc """
|
||||
Calculate the standard deviations cutoffs (2, 3, -2, and -3) for a given set of fitted Box-Cox values: power (`l`),
|
||||
median (`m`), and coefficient of variation (`s`).
|
||||
|
||||
These cutoffs are used to calculate the adjusted z-score.
|
||||
"""
|
||||
def cutoffs(l, m, s) do
|
||||
Enum.map([2, 3, -2, -3], &measure_at_standard_deviation(&1, l, m, s))
|
||||
end
|
||||
|
||||
@spec measure_at_standard_deviation(integer(), number(), number(), number()) :: number()
|
||||
@doc """
|
||||
Calculate the measure value at a given standard deviation (`sd`), considering the fitted Box-Cox values: power (`l`),
|
||||
median (`m`), and coefficient of variation (`s`).
|
||||
"""
|
||||
def measure_at_standard_deviation(sd, l, m, s) do
|
||||
m * :math.pow(1 + l * s * sd, 1 / l)
|
||||
end
|
||||
end
|
225
lib/growth/growth.ex
Normal file
225
lib/growth/growth.ex
Normal file
@@ -0,0 +1,225 @@
|
||||
defmodule Growth do
|
||||
@moduledoc """
|
||||
Follow up child growth from 0 to 19 years, calculating z-scores for the following measurements:
|
||||
|
||||
* weight between ages 0 and 10 years
|
||||
* height between ages 0 and 19 years
|
||||
* body mass index (bmi) between ages 0 and 19 years
|
||||
* head circumference between ages 0 and 5 years
|
||||
* arm circumference between ages 3 months and 5 years
|
||||
* subscapular skinfold between ages 3 months and 5 years
|
||||
* triceps skinfold between ages 3 months and 5 years
|
||||
"""
|
||||
|
||||
alias __MODULE__.Calc.Age
|
||||
alias __MODULE__.Calc.BMI
|
||||
alias __MODULE__.Score
|
||||
|
||||
@type gender :: :male | :female
|
||||
|
||||
@type measure :: number() | nil
|
||||
|
||||
@type opts :: [
|
||||
date_of_measurement: Date.t(),
|
||||
weight: measure(),
|
||||
height: measure(),
|
||||
arm_circumference: measure(),
|
||||
head_circumference: measure(),
|
||||
subscapular_skinfold: measure(),
|
||||
triceps_skinfold: measure()
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
name: String.t(),
|
||||
gender: gender(),
|
||||
date_of_birth: Date.t(),
|
||||
date_of_measurement: Date.t(),
|
||||
age_in_days: pos_integer(),
|
||||
age_in_weeks: pos_integer(),
|
||||
age_in_months: pos_integer(),
|
||||
weight: measure(),
|
||||
height: measure(),
|
||||
arm_circumference: measure(),
|
||||
head_circumference: measure(),
|
||||
subscapular_skinfold: measure(),
|
||||
triceps_skinfold: measure(),
|
||||
bmi: measure(),
|
||||
results: list()
|
||||
}
|
||||
|
||||
@enforce_keys [:name, :gender, :date_of_birth]
|
||||
defstruct [
|
||||
:name,
|
||||
:gender,
|
||||
:date_of_birth,
|
||||
:date_of_measurement,
|
||||
:age_in_days,
|
||||
:age_in_weeks,
|
||||
:age_in_months,
|
||||
:weight,
|
||||
:height,
|
||||
:arm_circumference,
|
||||
:head_circumference,
|
||||
:subscapular_skinfold,
|
||||
:triceps_skinfold,
|
||||
:bmi,
|
||||
:results
|
||||
]
|
||||
|
||||
@valid_measures [
|
||||
:date_of_measurement,
|
||||
:weight,
|
||||
:height,
|
||||
:arm_circumference,
|
||||
:head_circumference,
|
||||
:subscapular_skinfold,
|
||||
:triceps_skinfold
|
||||
]
|
||||
|
||||
@doc """
|
||||
Create a new growth measurement for a children with name, gender, date of birth, and the following optional arguments:
|
||||
|
||||
* `:date_of_measurement`: date when the measures were collected, defaults to today.
|
||||
* `:weight`: weight in kilograms, defaults to `nil`.
|
||||
* `:height`: height in centimeters, defaults to `nil`.
|
||||
* `:arm_circumference`: arm circumference in centimeters, defaults to `nil`.
|
||||
* `:head_circumference`: head circumference in centimeters, defaults to `nil`.
|
||||
* `:subscapular_skinfold`: subscapular skinfold in milimeters, defaults to `nil`.
|
||||
* `:triceps_skinfold`: triceps skinfold in milimeters, defaults to `nil`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Growth.new(
|
||||
...> "child a",
|
||||
...> :male,
|
||||
...> ~D[2024-01-01],
|
||||
...> date_of_measurement: ~D[2024-04-01],
|
||||
...> weight: 8,
|
||||
...> height: 65.4,
|
||||
...> arm_circumference: 15.5,
|
||||
...> head_circumference: 42.8,
|
||||
...> subscapular_skinfold: 10.9,
|
||||
...> triceps_skinfold: 13.5
|
||||
...> )
|
||||
%Growth{
|
||||
name: "child a",
|
||||
gender: :male,
|
||||
date_of_birth: ~D[2024-01-01],
|
||||
date_of_measurement: ~D[2024-04-01],
|
||||
age_in_days: 91,
|
||||
age_in_weeks: 13,
|
||||
age_in_months: 2,
|
||||
weight: 8,
|
||||
height: 65.4,
|
||||
arm_circumference: 15.5,
|
||||
head_circumference: 42.8,
|
||||
subscapular_skinfold: 10.9,
|
||||
triceps_skinfold: 13.5,
|
||||
bmi: 18.703999850368,
|
||||
results: [
|
||||
head_circumference: [
|
||||
day: {1.945484886994137, 0.9741416765426315},
|
||||
week: {1.945484886994137, 0.9741416765426315},
|
||||
month: {3.130859582465616, 0.9991285226182205}
|
||||
],
|
||||
arm_circumference: [day: {1.9227031505630465, 0.9727413295221268}],
|
||||
subscapular_skinfold: [day: {1.9437372448689536, 0.9740364275897885}],
|
||||
triceps_skinfold: [day: {1.950277062993091, 0.974428447506235}],
|
||||
weight: [
|
||||
day: {1.982458622036091, 0.9762860329545557},
|
||||
week: {1.982458622036091, 0.9762860329545557},
|
||||
month: {3.0355951313091745, 0.9987996926038037}
|
||||
],
|
||||
height: [
|
||||
day: {1.956263992749136, 0.9747829682259178},
|
||||
week: {1.956263992749136, 0.9747829682259178},
|
||||
month: {3.4867331002754054, 0.9997555204670452}
|
||||
],
|
||||
bmi: [
|
||||
day: {1.1977344927294398, 0.8844898016950435},
|
||||
week: {1.1977344927294398, 0.8844898016950435},
|
||||
month: {1.5837461190318038, 0.9433742474306444}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
@spec new(String.t(), gender(), Date.t(), opts()) :: t()
|
||||
def new(name, gender, date_of_birth, opts \\ []) do
|
||||
default_opts()
|
||||
|> Enum.reduce(
|
||||
%__MODULE__{
|
||||
name: name,
|
||||
gender: gender,
|
||||
date_of_birth: date_of_birth,
|
||||
results: []
|
||||
},
|
||||
fn {key, default_value}, measurement ->
|
||||
opts
|
||||
|> Keyword.get(key, default_value)
|
||||
|> then(&Map.put(measurement, key, &1))
|
||||
end
|
||||
)
|
||||
|> with_age_in_days()
|
||||
|> with_age_in_weeks()
|
||||
|> with_age_in_months()
|
||||
|> with_bmi()
|
||||
|> with_results()
|
||||
end
|
||||
|
||||
for precision <- [:day, :week, :month] do
|
||||
@doc """
|
||||
Add age with given precision in growth measurement.
|
||||
"""
|
||||
@spec unquote(:"with_age_in_#{precision}s")(t()) :: t()
|
||||
|
||||
def unquote(:"with_age_in_#{precision}s")(
|
||||
%__MODULE__{date_of_birth: date_of_birth, date_of_measurement: date_of_measurement} =
|
||||
growth
|
||||
)
|
||||
when not is_nil(date_of_birth) and not is_nil(date_of_measurement) do
|
||||
age = Age.calculate(unquote(precision), date_of_birth, date_of_measurement)
|
||||
Map.put(growth, unquote(:"age_in_#{precision}s"), age)
|
||||
end
|
||||
|
||||
def unquote(:"with_age_in_#{precision}s")(%__MODULE__{date_of_birth: date_of_birth} = growth)
|
||||
when not is_nil(date_of_birth) do
|
||||
unquote(:"with_age_in_#{precision}s")(%{growth | date_of_measurement: Date.utc_today()})
|
||||
end
|
||||
|
||||
def unquote(:"with_age_in_#{precision}s")(growth) do
|
||||
growth
|
||||
end
|
||||
end
|
||||
|
||||
def with_bmi(%__MODULE__{weight: weight, height: height} = growth)
|
||||
when is_number(weight) and is_number(height) do
|
||||
%{growth | bmi: BMI.calculate(:metric, weight, height)}
|
||||
end
|
||||
|
||||
def with_bmi(growth) do
|
||||
growth
|
||||
end
|
||||
|
||||
def with_results(growth) do
|
||||
Score.Scorer.results(growth, [
|
||||
Score.BMI,
|
||||
Score.Height,
|
||||
Score.Weight,
|
||||
Score.TricepsSkinfold,
|
||||
Score.SubscapularSkinfold,
|
||||
Score.ArmCircumference,
|
||||
Score.HeadCircumference
|
||||
])
|
||||
end
|
||||
|
||||
defp default_opts do
|
||||
Enum.map(@valid_measures, fn
|
||||
:date_of_measurement = key ->
|
||||
{key, Date.utc_today()}
|
||||
|
||||
key ->
|
||||
{key, nil}
|
||||
end)
|
||||
end
|
||||
end
|
328
lib/growth/indicators/download.ex
Normal file
328
lib/growth/indicators/download.ex
Normal file
@@ -0,0 +1,328 @@
|
||||
defmodule Growth.Indicators.Download do
|
||||
@moduledoc """
|
||||
To calculate z-scores for the different growth measurements, the system must:
|
||||
|
||||
1. Fetch indicators from World Health Organization
|
||||
2. Extract data from excel sheets
|
||||
3. Convert the data into proper format, specially, handle with decimal values
|
||||
3. Add metadata to make search for the parameters possible
|
||||
|
||||
The following indicators to construct z-scores are fetched:
|
||||
|
||||
* [height for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/length-height-for-age)
|
||||
* [height for age 5 to 19 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/height-for-age)
|
||||
* [weight for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/weight-for-age)
|
||||
* [weight for age 5 to 10 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/weight-for-age-5to10-years)
|
||||
* [weight for height](https://www.who.int/tools/child-growth-standards/standards/weight-for-length-height)
|
||||
* [body mass index (bmi) for age 0 to 5 years](https://www.who.int/toolkits/child-growth-standards/standards/body-mass-index-for-age-bmi-for-age)
|
||||
* [body mass index (bmi) for age 5 to 19 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/bmi-for-age)
|
||||
* [head circumference for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/head-circumference-for-age)
|
||||
* [arm circumference for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/arm-circumference-for-age)
|
||||
* [subscapular skinfold for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/subscapular-skinfold-for-age)
|
||||
* [triceps skinfold for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/triceps-skinfold-for-age)
|
||||
"""
|
||||
|
||||
NimbleCSV.define(IndicatorParser, separator: ",", escape: "\"")
|
||||
|
||||
@urls %{
|
||||
height_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_2-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-girls-z-who-2007-exp.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-girls-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-girls-z-who-2007-exp.xlsx
|
||||
]
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-2-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_2-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-boys-z-who-2007-exp.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-boys-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-boys-z-who-2007-exp.xlsx
|
||||
]
|
||||
}
|
||||
},
|
||||
weight_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_girls_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_girls_0-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-girls-z-who-2007-exp_7ea58763-36a2-436d-bef0-7fcfbadd2820.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/expanded-tables/wfa-girls-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-girls-z-who-2007-exp_7ea58763-36a2-436d-bef0-7fcfbadd2820.xlsx
|
||||
]
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_boys_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_boys_0-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-boys-z-who-2007-exp_0ff9c43c-8cc0-4c23-9fc6-81290675e08b.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/expanded-tables/wfa-boys-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-boys-z-who-2007-exp_0ff9c43c-8cc0-4c23-9fc6-81290675e08b.xlsx
|
||||
]
|
||||
}
|
||||
},
|
||||
weight_for_height: %{
|
||||
female: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfl_girls_0-to-2-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfh_girls_2-to-5-years_zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfl-girls-zscore-expanded-table.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfh-girls-zscore-expanded-tables.xlsx
|
||||
)
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfl_boys_0-to-2-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfh_boys_2-to-5-years_zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfl-boys-zscore-expanded-table.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfh-boys-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
},
|
||||
bmi_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_0-to-2-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_2-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-girls-z-who-2007-exp.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/expanded-tables/bfa-girls-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-girls-z-who-2007-exp.xlsx
|
||||
]
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_0-to-2-years_zcores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_2-to-5-years_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-boys-z-who-2007-exp.xlsx
|
||||
],
|
||||
expanded_tables: ~w[
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/expanded-tables/bfa-boys-zscore-expanded-tables.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-boys-z-who-2007-exp.xlsx
|
||||
]
|
||||
}
|
||||
},
|
||||
head_circumference_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-girls-0-13-zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-girls-0-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/expanded-tables/hcfa-girls-zscore-expanded-tables.xlsx
|
||||
)
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-boys-0-13-zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-boys-0-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/expanded-tables/hcfa-boys-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
},
|
||||
arm_circumference_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/acfa-girls-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/expanded-tables/acfa-girls-zscore-expanded-tables.xlsx
|
||||
)
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/acfa-boys-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/expanded-tables/acfa-boys-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
},
|
||||
subscapular_skinfold_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/ssfa-girls-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/expanded-tables/ssfa-girls-zscore-expanded-table.xlsx
|
||||
)
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/ssfa-boys-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/expanded-tables/ssfa-boys-zscore-expanded-table.xlsx
|
||||
)
|
||||
}
|
||||
},
|
||||
triceps_skinfold_for_age: %{
|
||||
female: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/tsfa-girls-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/expanded-tables/tsfa-girls-zscore-expanded-tables.xlsx
|
||||
)
|
||||
},
|
||||
male: %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/tsfa-boys-3-5-zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/expanded-tables/tsfa-boys-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def process_all do
|
||||
@urls
|
||||
|> Enum.map(&Task.async(__MODULE__, :process_measure, [&1]))
|
||||
|> Task.await_many()
|
||||
end
|
||||
|
||||
def process_measure({measure, urls}) do
|
||||
urls
|
||||
|> process_genders()
|
||||
|> as_csv()
|
||||
|> save(measure)
|
||||
end
|
||||
|
||||
def process_genders(%{
|
||||
female: %{age_tables: female_urls, expanded_tables: e_female_urls},
|
||||
male: %{age_tables: male_urls, expanded_tables: e_male_urls}
|
||||
}) do
|
||||
[
|
||||
{:female, :age, female_urls},
|
||||
{:male, :age, male_urls},
|
||||
{:female, :expanded, e_female_urls},
|
||||
{:male, :expanded, e_male_urls}
|
||||
]
|
||||
|> Enum.map(&Task.async(__MODULE__, :process_gender, [&1]))
|
||||
|> Task.await_many()
|
||||
|> merge()
|
||||
end
|
||||
|
||||
def process_gender({gender, category, urls}) do
|
||||
urls
|
||||
|> Enum.map(&Task.async(__MODULE__, :process, [gender, category, &1]))
|
||||
|> Task.await_many()
|
||||
|> merge()
|
||||
end
|
||||
|
||||
def process(gender, category, url) do
|
||||
url
|
||||
|> fetch!()
|
||||
|> extract!(url)
|
||||
|> convert(gender, category, url)
|
||||
end
|
||||
|
||||
def fetch!(url) do
|
||||
req =
|
||||
[url: url]
|
||||
|> Keyword.merge(
|
||||
:wabanex
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:who_req_options, [])
|
||||
)
|
||||
|> Req.new()
|
||||
|
||||
case Req.get(req) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
body
|
||||
|
||||
_ ->
|
||||
raise("fetch failed for url #{url}")
|
||||
end
|
||||
end
|
||||
|
||||
def extract!(content, url) do
|
||||
with {:ok, package} <- XlsxReader.open(content, source: :binary),
|
||||
[sheet_name | _] <- XlsxReader.sheet_names(package),
|
||||
{:ok, data} <- XlsxReader.sheet(package, sheet_name) do
|
||||
data
|
||||
else
|
||||
_ -> raise("failed to extract excel for #{url}")
|
||||
end
|
||||
end
|
||||
|
||||
@common_header ~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3)
|
||||
|
||||
# FIX: (jpd) weight for lenght/height does not have an age in the header row
|
||||
def convert([header | rows], gender, category, url) do
|
||||
age_unit = header |> hd() |> String.downcase()
|
||||
|
||||
fixed_header = header |> tl() |> Enum.map(&String.downcase/1) |> Enum.map(&String.trim/1)
|
||||
|
||||
parsed_header = ["source" | ["category" | ["gender" | ["age_unit" | ["age" | fixed_header]]]]]
|
||||
|
||||
# NOTE: (jpd): parsing the rows consist in:
|
||||
# 1. convert row values to decimal
|
||||
# 2. prepend the values url source, gender, and age unit
|
||||
# 3. convert row to keyword list using the parsed header
|
||||
# 4. convert from keyword list to map
|
||||
# 5. fetch common values based on common headers
|
||||
# 6. sort row values based on common headers
|
||||
parsed_rows =
|
||||
rows
|
||||
|> Stream.map(fn row -> Enum.map(row, &Decimal.new/1) end)
|
||||
|> Stream.map(&[url | [category | [gender | [age_unit | &1]]]])
|
||||
|> Stream.map(&Enum.zip(parsed_header, &1))
|
||||
|> Stream.map(&Map.new/1)
|
||||
|> Stream.map(&Map.take(&1, @common_header))
|
||||
|> Enum.map(fn row ->
|
||||
Enum.map(@common_header, fn key -> Map.get(row, key) end)
|
||||
end)
|
||||
|
||||
[@common_header | parsed_rows]
|
||||
end
|
||||
|
||||
def merge(datum) do
|
||||
datum
|
||||
|> Stream.with_index()
|
||||
|> Stream.map(fn
|
||||
{data, 0} ->
|
||||
data
|
||||
|
||||
{[_ | data], _} ->
|
||||
data
|
||||
end)
|
||||
|> Enum.reduce([], fn data, accum ->
|
||||
Enum.concat(accum, data)
|
||||
end)
|
||||
end
|
||||
|
||||
def as_csv(data) do
|
||||
IndicatorParser.dump_to_iodata(data)
|
||||
end
|
||||
|
||||
def save(data, measurement) do
|
||||
:wabanex
|
||||
|> Application.app_dir(["priv", "growth", "indicators", "#{measurement}.csv"])
|
||||
|> File.write(data)
|
||||
end
|
||||
end
|
190
lib/growth/indicators/load.ex
Normal file
190
lib/growth/indicators/load.ex
Normal file
@@ -0,0 +1,190 @@
|
||||
defmodule Growth.Indicators.Load do
|
||||
@moduledoc """
|
||||
Load local indicators csv files into ets.
|
||||
|
||||
For each measurement an ets table is created, so the system has the following tables:
|
||||
|
||||
* `Growth.Score.ArmCircumference`
|
||||
* `Growth.Score.BMI`
|
||||
* `Growth.Score.HeadCircumference`
|
||||
* `Growth.Score.Height`
|
||||
* `Growth.Score.SubscapularSkinfold`
|
||||
* `Growth.Score.TricepsSkinfold`
|
||||
* `Growth.Score.Weight`
|
||||
* `Growth.Score.WeightForHeight`
|
||||
|
||||
The rows in the csv files are converted to two tuple, representing the a key and value, with the following format:
|
||||
|
||||
* **key**: `{gender, unit, t-value}`
|
||||
* **value**: map with the keys:
|
||||
* l
|
||||
* m
|
||||
* s
|
||||
* sd3neg
|
||||
* sd2neg
|
||||
* sd1neg
|
||||
* sd0
|
||||
* sd1
|
||||
* sd2
|
||||
* sd3
|
||||
* source
|
||||
* category
|
||||
|
||||
"""
|
||||
|
||||
use Task
|
||||
|
||||
require Logger
|
||||
|
||||
alias Growth.Score
|
||||
|
||||
@measure_to_score [
|
||||
arm_circumference_for_age: Score.ArmCircumference,
|
||||
bmi_for_age: Score.BMI,
|
||||
head_circumference_for_age: Score.HeadCircumference,
|
||||
height_for_age: Score.Height,
|
||||
subscapular_skinfold_for_age: Score.SubscapularSkinfold,
|
||||
triceps_skinfold_for_age: Score.TricepsSkinfold,
|
||||
weight_for_age: Score.Weight,
|
||||
weight_for_height: Score.WeightForHeight
|
||||
]
|
||||
|
||||
@doc false
|
||||
def start_link(_) do
|
||||
_ =
|
||||
Enum.map(@measure_to_score, fn {_, measure} ->
|
||||
create_ets(measure)
|
||||
end)
|
||||
|
||||
Task.start_link(__MODULE__, :all, [])
|
||||
end
|
||||
|
||||
@spec all :: [[boolean()]]
|
||||
@doc """
|
||||
Load indicators csv files into their own ets tables.
|
||||
"""
|
||||
def all do
|
||||
Logger.debug("load growth indicators")
|
||||
|
||||
:wabanex
|
||||
|> Application.app_dir(["priv", "growth", "indicators", "*.csv"])
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&create_ets_from_filename/1)
|
||||
|> Enum.map(&Task.async(__MODULE__, :load_measure, [&1]))
|
||||
|> Task.await_many()
|
||||
end
|
||||
|
||||
@spec create_ets(module()) :: module()
|
||||
@doc """
|
||||
Create a public ets table for the growth module, using it as the table name.
|
||||
|
||||
Returns the given module.
|
||||
"""
|
||||
def create_ets(measure) do
|
||||
try do
|
||||
:ets.new(measure, [:set, :public, :named_table])
|
||||
rescue
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
measure
|
||||
end
|
||||
|
||||
@spec create_ets_from_filename(String.t()) :: {atom(), String.t()}
|
||||
@doc """
|
||||
Create ets table based on filename and return a tuple with the ets table name and filename.
|
||||
"""
|
||||
def create_ets_from_filename(filename) do
|
||||
measure =
|
||||
filename
|
||||
|> Path.basename()
|
||||
|> Path.rootname()
|
||||
|> String.to_atom()
|
||||
|> then(&Keyword.get(@measure_to_score, &1, &1))
|
||||
|> create_ets()
|
||||
|
||||
{measure, filename}
|
||||
end
|
||||
|
||||
@spec load_measure({atom(), String.t()}) :: [boolean()]
|
||||
@doc """
|
||||
Read, convert, and load a measure/filename into the proper ets table.
|
||||
"""
|
||||
def load_measure({measure, filename}) do
|
||||
Logger.debug("load data from #{filename} into #{measure}")
|
||||
|
||||
filename
|
||||
|> read()
|
||||
|> convert()
|
||||
|> load(measure)
|
||||
end
|
||||
|
||||
@spec read(String.t()) :: Enumerable.t()
|
||||
@doc false
|
||||
def read(filename) do
|
||||
filename
|
||||
|> File.stream!()
|
||||
|> IndicatorParser.parse_stream()
|
||||
end
|
||||
|
||||
@spec convert(Enumerable.t()) :: Enumerable.t()
|
||||
@doc false
|
||||
def convert(data) do
|
||||
Stream.map(data, fn [
|
||||
source,
|
||||
category,
|
||||
gender,
|
||||
unit,
|
||||
t,
|
||||
l,
|
||||
m,
|
||||
s,
|
||||
sd3neg,
|
||||
sd2neg,
|
||||
sd1neg,
|
||||
sd0,
|
||||
sd1,
|
||||
sd2,
|
||||
sd3
|
||||
] ->
|
||||
converted_t =
|
||||
if unit in ~w(day week month) do
|
||||
as_integer(t)
|
||||
else
|
||||
as_float(t)
|
||||
end
|
||||
|
||||
key = {String.to_atom(gender), String.to_atom(unit), converted_t}
|
||||
|
||||
value = %{
|
||||
l: as_float(l),
|
||||
m: as_float(m),
|
||||
s: as_float(s),
|
||||
sd3neg: as_float(sd3neg),
|
||||
sd2neg: as_float(sd2neg),
|
||||
sd1neg: as_float(sd1neg),
|
||||
sd0: as_float(sd0),
|
||||
sd1: as_float(sd1),
|
||||
sd2: as_float(sd2),
|
||||
sd3: as_float(sd3),
|
||||
source: source,
|
||||
category: category
|
||||
}
|
||||
|
||||
{key, value}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec load(Enumerable.t(), atom()) :: [boolean()]
|
||||
@doc false
|
||||
def load(data, ets_table) do
|
||||
Enum.map(data, fn {key, value} ->
|
||||
:ets.insert_new(ets_table, {key, value})
|
||||
end)
|
||||
end
|
||||
|
||||
defp as_integer(value), do: value |> Integer.parse() |> elem(0)
|
||||
|
||||
defp as_float(value), do: value |> Float.parse() |> elem(0)
|
||||
end
|
18
lib/growth/score/arm_circumference.ex
Normal file
18
lib/growth/score/arm_circumference.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.ArmCircumference do
|
||||
@moduledoc """
|
||||
Calculate z-score for arm circumference for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in arm circumference indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:arm_circumference
|
||||
end
|
||||
end
|
18
lib/growth/score/bmi.ex
Normal file
18
lib/growth/score/bmi.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.BMI do
|
||||
@moduledoc """
|
||||
Calculate z-score for body mass index for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in BMI indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:bmi
|
||||
end
|
||||
end
|
18
lib/growth/score/head_circumference.ex
Normal file
18
lib/growth/score/head_circumference.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.HeadCircumference do
|
||||
@moduledoc """
|
||||
Calculate z-score for head circumference for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in head circumference indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:head_circumference
|
||||
end
|
||||
end
|
18
lib/growth/score/height.ex
Normal file
18
lib/growth/score/height.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.Height do
|
||||
@moduledoc """
|
||||
Calculate z-score for height for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in height indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:height
|
||||
end
|
||||
end
|
96
lib/growth/score/scorer.ex
Normal file
96
lib/growth/score/scorer.ex
Normal file
@@ -0,0 +1,96 @@
|
||||
defmodule Growth.Score.Scorer do
|
||||
@moduledoc """
|
||||
Behaviour defining common interface to calculate z-score and percentile for a given measurement.
|
||||
"""
|
||||
|
||||
alias Growth.Calc.Percentile
|
||||
alias Growth.Calc.ZScore
|
||||
|
||||
@callback measure_name() :: atom()
|
||||
|
||||
@spec results(Growth.t(), [module()]) :: Growth.t()
|
||||
@doc """
|
||||
Add z-score and centile values in growth measurements `results` for each indicator.
|
||||
"""
|
||||
def results(growth, indicators) do
|
||||
Enum.reduce(indicators, growth, &result/2)
|
||||
end
|
||||
|
||||
@spec result(module(), Growth.t()) :: Growth.t()
|
||||
@doc """
|
||||
Calculate z-score and percentile values for the given indicator and add them to the growth measurement `results`.
|
||||
"""
|
||||
def result(indicator, growth) do
|
||||
result =
|
||||
growth
|
||||
|> lms(indicator)
|
||||
|> Enum.map(fn {precision, {l, m, s}} ->
|
||||
{precision, scores(indicator, growth, l, m, s)}
|
||||
end)
|
||||
|
||||
%{growth | results: Keyword.put(growth.results, indicator.measure_name(), result)}
|
||||
end
|
||||
|
||||
@spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}]
|
||||
@doc """
|
||||
Get the indicaator fitted values of Box-Cox transformation:
|
||||
|
||||
* power (`l`)
|
||||
* median (`m`)
|
||||
* coefficient of variation (`s`)
|
||||
|
||||
"""
|
||||
def lms(growth, indicator) do
|
||||
[
|
||||
{growth.gender, :day, growth.age_in_days},
|
||||
{growth.gender, :week, growth.age_in_weeks},
|
||||
{growth.gender, :month, growth.age_in_months}
|
||||
]
|
||||
|> Enum.map(fn {_, precision, _} = key ->
|
||||
case :ets.lookup(indicator, key) do
|
||||
[{^key, %{l: l, m: m, s: s}} | _] ->
|
||||
{precision, {l, m, s}}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculate the z-score and percentile of an indicator measurement.
|
||||
"""
|
||||
@spec scores(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) ::
|
||||
{Growth.measure(), Growth.measure()}
|
||||
def scores(indicator, growth, l, m, s) do
|
||||
growth
|
||||
|> Map.get(indicator.measure_name())
|
||||
|> z_score(l, m, s)
|
||||
|> then(fn score -> {score, percentile(score)} end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check `Growth.Calc.ZScore.compute/4`.
|
||||
"""
|
||||
@spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure()
|
||||
def z_score(nil, _, _, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
def z_score(value, l, m, s) do
|
||||
ZScore.compute(value, l, m, s)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Check `Growth.Calc.Percentile.compute/1`.
|
||||
"""
|
||||
@spec percentile(Growth.measure()) :: Growth.measure()
|
||||
def percentile(nil) do
|
||||
nil
|
||||
end
|
||||
|
||||
def percentile(score) do
|
||||
Percentile.compute(score)
|
||||
end
|
||||
end
|
18
lib/growth/score/subscapular_skinfold.ex
Normal file
18
lib/growth/score/subscapular_skinfold.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.SubscapularSkinfold do
|
||||
@moduledoc """
|
||||
Calculate z-score for subscapular skinfold for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in subscapular skinfold indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:subscapular_skinfold
|
||||
end
|
||||
end
|
18
lib/growth/score/triceps_skinfold.ex
Normal file
18
lib/growth/score/triceps_skinfold.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.TricepsSkinfold do
|
||||
@moduledoc """
|
||||
Calculate z-score for triceps skinfold for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in triceps skinfold indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:triceps_skinfold
|
||||
end
|
||||
end
|
18
lib/growth/score/weight.ex
Normal file
18
lib/growth/score/weight.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Growth.Score.Weight do
|
||||
@moduledoc """
|
||||
Calculate z-score for weight for age.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in weight indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:weight
|
||||
end
|
||||
end
|
95
lib/growth/score/weight_for_height.ex
Normal file
95
lib/growth/score/weight_for_height.ex
Normal file
@@ -0,0 +1,95 @@
|
||||
defmodule Growth.Score.WeightForHeight do
|
||||
@moduledoc """
|
||||
Calculate z-score for weight for height.
|
||||
|
||||
This module calculates the z-score for weight relative to height, which is an important
|
||||
indicator for assessing whether a child's weight is appropriate for their height,
|
||||
regardless of age.
|
||||
|
||||
**Limitation**: the memasurements do not differentiate between length and height, and
|
||||
always assume height.
|
||||
"""
|
||||
|
||||
@behaviour Growth.Score.Scorer
|
||||
|
||||
alias Growth.Score.Scorer
|
||||
|
||||
@impl Scorer
|
||||
@spec measure_name() :: atom()
|
||||
@doc """
|
||||
Name of the measurement used in weight for height indicator.
|
||||
"""
|
||||
def measure_name do
|
||||
:weight_for_height
|
||||
end
|
||||
|
||||
@doc """
|
||||
Custom implementation for weight-for-height lookup, as it requires both weight and height.
|
||||
|
||||
This overrides the default implementation in the Scorer module.
|
||||
"""
|
||||
@spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}]
|
||||
def lms(%Growth{gender: gender, weight: weight, height: height}, _indicator)
|
||||
when is_number(weight) and is_number(height) do
|
||||
# For weight-for-height, we use height as the lookup value instead of age
|
||||
key = {{gender, :height, height}, :_}
|
||||
|
||||
case :ets.match_object(__MODULE__, key) do
|
||||
[{{^gender, _, ^height}, %{l: l, m: m, s: s}} | _] ->
|
||||
[{"height", {l, m, s}}]
|
||||
|
||||
_ ->
|
||||
# Try to find the closest height value
|
||||
find_closest_height(gender, height)
|
||||
end
|
||||
end
|
||||
|
||||
def lms(_growth, _indicator) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Find the closest height value in the ETS table when an exact match isn't found.
|
||||
"""
|
||||
@spec find_closest_height(Growth.gender(), number()) :: [
|
||||
{String.t(), {number(), number(), number()}}
|
||||
]
|
||||
def find_closest_height(gender, height) do
|
||||
# Fetch all entries for the given gender and height measurement
|
||||
matcher = [
|
||||
{
|
||||
{{:"$1", :"$2", :"$3"}, :_},
|
||||
[
|
||||
{:andalso,
|
||||
{:andalso, {:andalso, {:==, :"$1", gender}, {:==, :"$2", :height}},
|
||||
{:>, :"$3", height - 1.0}}, {:<, :"$3", height + 1.0}}
|
||||
],
|
||||
[:"$_"]
|
||||
}
|
||||
]
|
||||
|
||||
gender_height_entries = :ets.select(__MODULE__, matcher)
|
||||
|
||||
case gender_height_entries do
|
||||
[] ->
|
||||
# No entries found for this gender
|
||||
[]
|
||||
|
||||
entries ->
|
||||
# Find the entry with the height closest to the target height
|
||||
closest_entry =
|
||||
entries
|
||||
|> Enum.filter(fn {{_, _, entry_height}, _lms_data} ->
|
||||
abs(entry_height - height) <= 0.5
|
||||
end)
|
||||
|> Enum.min_by(fn {{_, _, entry_height}, _lms_data} ->
|
||||
abs(entry_height - height)
|
||||
end)
|
||||
|
||||
# Extract the LMS data from the closest entry
|
||||
{_, %{l: l, m: m, s: s}} = closest_entry
|
||||
|
||||
[{"height", {l, m, s}}]
|
||||
end
|
||||
end
|
||||
end
|
@@ -1,34 +1,26 @@
|
||||
defmodule Wabanex.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Start the PromEx supervisor
|
||||
Wabanex.PromEx,
|
||||
# Start the Ecto repository
|
||||
Wabanex.Repo,
|
||||
# Start the Telemetry supervisor
|
||||
WabanexWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Wabanex.PubSub},
|
||||
# Start the Endpoint (http/https)
|
||||
{Growth.Indicators.Load, []},
|
||||
{DNSCluster,
|
||||
query: Application.get_env(:wabanex, :dns_cluster_query) || :ignore,
|
||||
log: :info,
|
||||
resolver: Application.get_env(:wabanex, :dns_cluster_resolver) || DNSCluster.Resolver},
|
||||
WabanexWeb.Endpoint
|
||||
# Start a worker by calling: Wabanex.Worker.start_link(arg)
|
||||
# {Wabanex.Worker, arg}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Wabanex.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
def config_change(changed, _new, removed) do
|
||||
WabanexWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
|
26
lib/wabanex/dev_dns_cluster_resolver.ex
Normal file
26
lib/wabanex/dev_dns_cluster_resolver.ex
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Wabanex.DevDNSClusterResolver do
|
||||
@moduledoc """
|
||||
Local DNS resolver for cluster query.
|
||||
"""
|
||||
require Record
|
||||
|
||||
Record.defrecord(:hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl"))
|
||||
|
||||
def basename(node_name) when is_atom(node_name) do
|
||||
[basename, _] = node_name |> to_string() |> String.split("@")
|
||||
basename
|
||||
end
|
||||
|
||||
def connect_node(node_name) when is_atom(node_name) do
|
||||
Node.connect(node_name)
|
||||
end
|
||||
|
||||
def list_nodes, do: Node.list(:visible)
|
||||
|
||||
def lookup(query, type) when is_binary(query) and type in [:a, :aaaa] do
|
||||
case :inet_res.getbyname(~c"#{query}", type) do
|
||||
{:ok, hostent(h_addr_list: addr_list)} -> addr_list
|
||||
{:error, _} -> []
|
||||
end
|
||||
end
|
||||
end
|
28
lib/wabanex/release.ex
Normal file
28
lib/wabanex/release.ex
Normal file
@@ -0,0 +1,28 @@
|
||||
defmodule Wabanex.Release do
|
||||
@moduledoc """
|
||||
Used for executing DB release tasks when run in production without Mix
|
||||
installed.
|
||||
"""
|
||||
@app :wabanex
|
||||
|
||||
def migrate do
|
||||
load_app()
|
||||
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
def rollback(repo, version) do
|
||||
load_app()
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||
end
|
||||
|
||||
defp repos do
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
|
||||
defp load_app do
|
||||
Application.load(@app)
|
||||
end
|
||||
end
|
@@ -19,10 +19,10 @@ defmodule WabanexWeb do
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Gettext, backend: WabanexWeb.Gettext
|
||||
use Phoenix.Controller, namespace: WabanexWeb
|
||||
|
||||
import Plug.Conn
|
||||
import WabanexWeb.Gettext
|
||||
alias WabanexWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
@@ -54,17 +54,18 @@ defmodule WabanexWeb do
|
||||
def channel do
|
||||
quote do
|
||||
use Phoenix.Channel
|
||||
import WabanexWeb.Gettext
|
||||
use Gettext, backend: WabanexWeb.Gettext
|
||||
end
|
||||
end
|
||||
|
||||
defp view_helpers do
|
||||
quote do
|
||||
use Gettext, backend: WabanexWeb.Gettext
|
||||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
|
||||
import WabanexWeb.ErrorHelpers
|
||||
import WabanexWeb.Gettext
|
||||
alias WabanexWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
@@ -5,7 +5,7 @@ defmodule WabanexWeb.Gettext do
|
||||
By using [Gettext](https://hexdocs.pm/gettext),
|
||||
your module gains a set of macros for translations, for example:
|
||||
|
||||
import WabanexWeb.Gettext
|
||||
use Gettext, backend: WabanexWeb.Gettext
|
||||
|
||||
# Simple translation
|
||||
gettext("Here is the string to translate")
|
||||
@@ -20,5 +20,5 @@ defmodule WabanexWeb.Gettext do
|
||||
|
||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||
"""
|
||||
use Gettext, otp_app: :wabanex
|
||||
use Gettext.Backend, otp_app: :wabanex
|
||||
end
|
||||
|
50
mix.exs
50
mix.exs
@@ -7,7 +7,7 @@ defmodule Wabanex.MixProject do
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.7",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
compilers: Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
dialyzer: [plt_core_path: "priv/plts"],
|
||||
test_coverage: [tool: LcovEx, output: "cover"],
|
||||
@@ -29,25 +29,37 @@ defmodule Wabanex.MixProject do
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:absinthe, "~> 1.5"},
|
||||
{:absinthe_plug, "~> 1.5"},
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
{:absinthe, "~> 1.7.0"},
|
||||
{:absinthe_plug,
|
||||
git: "https://github.com/absinthe-graphql/absinthe_plug.git",
|
||||
ref: "24ec7aa3b513c7c1aa79e5cad1197cb138603972"},
|
||||
{:credo, "~> 1.7.0", only: [:dev, :test], runtime: false},
|
||||
{:crudry, "~> 2.4.0"},
|
||||
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
{:gettext, "~> 0.20"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:junit_formatter, "~> 3.1", only: [:test]},
|
||||
{:lcov_ex, "~> 0.2", only: [:dev, :test], runtime: false},
|
||||
{:pg_ranges, "~> 1.1"},
|
||||
{:phoenix, "~> 1.6.0"},
|
||||
{:phoenix_ecto, "~> 4.1"},
|
||||
{:phoenix_live_dashboard, "~> 0.6"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:prom_ex, "~> 1.7"},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
{:telemetry_poller, "~> 1.0"}
|
||||
{:decimal, "~> 2.3.0"},
|
||||
{:dialyxir, "~> 1.4.0", only: [:dev, :test], runtime: false},
|
||||
{:dns_cluster, "~> 0.2.0"},
|
||||
{:ecto_sql, "~> 3.13.0"},
|
||||
{:elixlsx, "~> 0.6.0", only: :test},
|
||||
{:ex_doc, "~> 0.38.0", only: :dev, runtime: false},
|
||||
{:gettext, "~> 0.26.0"},
|
||||
{:jason, "~> 1.4.0"},
|
||||
{:junit_formatter, "~> 3.4.0", only: [:test]},
|
||||
{:lcov_ex, "~> 0.3.0", only: [:dev, :test], runtime: false},
|
||||
{:mix_audit, "~> 2.1.0", only: [:dev, :test], runtime: false},
|
||||
{:nimble_csv, "~> 1.2.0"},
|
||||
{:pg_ranges, "~> 1.1.0"},
|
||||
{:phoenix, "~> 1.7.0"},
|
||||
{:phoenix_ecto, "~> 4.6.0"},
|
||||
{:phoenix_live_dashboard, "~> 0.8.0"},
|
||||
{:phoenix_view, "~> 2.0.0"},
|
||||
{:plug_cowboy, "~> 2.7.0"},
|
||||
{:postgrex, "~> 0.20.0"},
|
||||
{:prom_ex, "~> 1.11.0"},
|
||||
{:req, "~> 0.5.0"},
|
||||
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
||||
{:telemetry_metrics, "~> 1.1.0"},
|
||||
{:telemetry_poller, "~> 1.2.0"},
|
||||
{:xlsx_reader, "~> 0.8.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
109
mix.lock
109
mix.lock
@@ -1,48 +1,69 @@
|
||||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.7.0", "36819e7b1fd5046c9c734f27fe7e564aed3bda59f0354c37cd2df88fd32dd014", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "566a5b5519afc9b29c4d367f0c6768162de3ec03e9bf9916f9dc2bcbe7c09643"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
|
||||
"castore": {:hex, :castore, "0.1.16", "2675f717adc700475345c5512c381ef9273eb5df26bdd3f8c13e2636cf4cc175", [:mix], [], "hexpm", "28ed2c43d83b5c25d35c51bc0abf229ac51359c170cba76171a462ced2e4b651"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||
"absinthe": {:hex, :absinthe, "1.7.10", "b33471b593260f148d05e4d771d1857e07b70a680f89cfa75184098bef4ec893", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffda95735364c041a65a4b0e02ffb04eabb1e52ab664fa7eeecefb341449e8c2"},
|
||||
"absinthe_plug": {:git, "https://github.com/absinthe-graphql/absinthe_plug.git", "24ec7aa3b513c7c1aa79e5cad1197cb138603972", [ref: "24ec7aa3b513c7c1aa79e5cad1197cb138603972"]},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
|
||||
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"},
|
||||
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
|
||||
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
|
||||
"crudry": {:hex, :crudry, "2.4.0", "d175f1a8ee44456e852ae6b4d75997642a3e2a9f62ac28e4736656856c80dff5", [:mix], [{:absinthe, ">= 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, ">= 0.0.0", [hex: :gettext, repo: "hexpm", optional: false]}], "hexpm", "a14150b3f82e060e602d67f2c52c6195f0fefb49aee7d226d68dc3b83b8f58dc"},
|
||||
"db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
|
||||
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
|
||||
"ecto": {:hex, :ecto, "3.7.2", "44c034f88e1980754983cc4400585970b4206841f6f3780967a65a9150ef09a8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a600da5772d1c31abbf06f3e4a1ffb150e74ed3e2aa92ff3cee95901657a874e"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.7.2", "55c60aa3a06168912abf145c6df38b0295c34118c3624cf7a6977cd6ce043081", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c218ea62f305dcaef0b915fb56583195e7b91c91dcfb006ba1f669bfacbff2a"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.10.2", "9ad27d68270d879f73f26604bb2e573d40f29bf0e907064a9a337f90a16a0312", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dd8b11b282072cec2ef30852283949c248bd5d2820c88d8acc89402b81db7550"},
|
||||
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
|
||||
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
|
||||
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
|
||||
"junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"},
|
||||
"lcov_ex": {:hex, :lcov_ex, "0.2.0", "4ae06fc23e7dc7f06cddabf44b97901bb4a79cd7b783bce411d07131d6d5edd2", [:mix], [], "hexpm", "c5e33def56f9ae9de40171012c1c47fcbd4abaae430193aad4d4062a8d336970"},
|
||||
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
|
||||
"mint": {:hex, :mint, "1.4.1", "49b3b6ea35a9a38836d2ad745251b01ca9ec062f7cb66f546bf22e6699137126", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "cd261766e61011a9079cccf8fa9d826e7a397c24fbedf0e11b49312bea629b58"},
|
||||
"nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
|
||||
"pg_ranges": {:hex, :pg_ranges, "1.1.0", "cfac1ee59958213cd6b066cc8bb5d63f6af7f830fa05fc633f33ee5da7876c94", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto_sql, ">= 3.3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "2750f923666f548572e73b1636066196d7bd47ea2c63a1ad2c31692d16fd8590"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.7", "f1de32418bbbcd471f4fe74d3860ee9c8e8c6c36a0ec173be8ff468a5d72ac90", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b354a4f11d9a2f3a380fb731042dae064f22d7aed8c7e7c024a2459f12994aad"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.9", "36b5aa812bc3ccd64c9630f6b3234d9ea21105493237e927aae19d0ba758f0db", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f7ebc3e0ba0c5f6b6996ed6c901ddbfdaba59a6d09b569e7cb2f2f7d693b4455"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
|
||||
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"postgrex": {:hex, :postgrex, "0.16.2", "0f83198d0e73a36e8d716b90f45f3bde75b5eebf4ade4f43fa1f88c90a812f74", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9ea589754d9d4d076121090662b7afe155b374897a6550eb288f11d755acfa0"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.7.1", "39331ee3fe6f9a8587d8208bf9274a253bb80281700e127dd18786cda5e08c37", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.10.2", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "4c978872b88a929833925a0f4d0561824804c671fdd04581e765509ed0a6ed08"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.0.2", "c98b1c580de637bfeac00db41b9fb91fb4c3548ee3d512a8ed7299172312eaf3", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "48351a0d56f80e38c997b44232b1043e0a081670d16766eee920e6254175b730"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
|
||||
"ecto": {:hex, :ecto, "3.13.1", "ebb11c2f0307ff62e8aaba57def59ad920a3cbd89d002b1118944cbf598c13c7", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d9ea5075a6f3af9cd2cdbabe8a0759eb73b485e981fd7c03014f79479ac85340"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.1", "d3d76d78afd2757644b5c4f7ca37f90bcf1e05d05a06cca8526e30cefb0034a1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fba0174284fd339f69376b0405942036ce5f0ff7d59402a6ccf3b7ce2903198"},
|
||||
"elixlsx": {:hex, :elixlsx, "0.6.0", "858c2c821ab52f4ca0988adce188d19f3b239a4fff8b36b26cd81ec8af9b2ab3", [:mix], [], "hexpm", "c4766f47afea075a85950a5c6fe981e98b8b8a30cc076382aaacf2bb8dbcd25d"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
|
||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
|
||||
"lcov_ex": {:hex, :lcov_ex, "0.3.4", "f48aed787db0d1cff1409db391f442cdbc0af0f860dbc326430030027e6ded36", [:mix], [], "hexpm", "c3987b6aeadd78d4b7933fa9cc4de3bd69d34f0fae39bad908f79a7ceea957e5"},
|
||||
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
|
||||
"peep": {:hex, :peep, "3.5.0", "9f6ead7b0f2c684494200c8fc02e7e62e8c459afe861b29bd859e4c96f402ed8", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5a73a99c6e60062415efeb7e536a663387146463a3d3df1417da31fd665ac210"},
|
||||
"pg_ranges": {:hex, :pg_ranges, "1.1.1", "304da41118282eb81ffaefc3195e379afe139867fdc5171626aec9c83d360537", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "d49c28628c2276d4964f0ced6cc8c140a62af6eeb05c1b86a23afed8b3a5db8c"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
|
||||
"saxy": {:hex, :saxy, "1.6.0", "02cb4e9bd045f25ac0c70fae8164754878327ee393c338a090288210b02317ee", [:mix], [], "hexpm", "ef42eb4ac983ca77d650fbdb68368b26570f6cc5895f0faa04d34a6f384abad3"},
|
||||
"sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||
"xlsx_reader": {:hex, :xlsx_reader, "0.8.8", "fbb29049548ff687f03a2873f2eb0d9057e47eb69cafb07f44988f030fb620b7", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:saxy, "~> 1.5", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "642d979a3a156b150bb76a89998a130483e1c399fa32e8d3a66abc1d9799dbd7"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
|
||||
}
|
||||
|
2
priv/docker/service/docker-entrypoint.sh
Executable file
2
priv/docker/service/docker-entrypoint.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
elixir --name ${HOSTNAME}@$(hostname -i | cut -d ' ' -f1) --cookie ${1} -S mix phx.server
|
2
priv/docker/service/docker-remote.sh
Executable file
2
priv/docker/service/docker-remote.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
iex --name ex_trainer_remote@127.0.0.1 --hidden --cookie local-cookie --remsh ${HOSTNAME}@$(hostname -i | cut -d ' ' -f1)
|
3649
priv/growth/indicators/arm_circumference_for_age.csv
Normal file
3649
priv/growth/indicators/arm_circumference_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
4539
priv/growth/indicators/bmi_for_age.csv
Normal file
4539
priv/growth/indicators/bmi_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
3865
priv/growth/indicators/head_circumference_for_age.csv
Normal file
3865
priv/growth/indicators/head_circumference_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
4539
priv/growth/indicators/height_for_age.csv
Normal file
4539
priv/growth/indicators/height_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
3649
priv/growth/indicators/subscapular_skinfold_for_age.csv
Normal file
3649
priv/growth/indicators/subscapular_skinfold_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
3649
priv/growth/indicators/triceps_skinfold_for_age.csv
Normal file
3649
priv/growth/indicators/triceps_skinfold_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
4105
priv/growth/indicators/weight_for_age.csv
Normal file
4105
priv/growth/indicators/weight_for_age.csv
Normal file
File diff suppressed because it is too large
Load Diff
2889
priv/growth/indicators/weight_for_height.csv
Normal file
2889
priv/growth/indicators/weight_for_height.csv
Normal file
File diff suppressed because it is too large
Load Diff
5
rel/overlays/bin/migrate
Executable file
5
rel/overlays/bin/migrate
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
exec ./wabanex eval Wabanex.Release.migrate
|
1
rel/overlays/bin/migrate.bat
Executable file
1
rel/overlays/bin/migrate.bat
Executable file
@@ -0,0 +1 @@
|
||||
call "%~dp0\wabanex" eval Wabanex.Release.migrate
|
5
rel/overlays/bin/server
Executable file
5
rel/overlays/bin/server
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd -P -- "$(dirname -- "$0")"
|
||||
PHX_SERVER=true exec ./wabanex start
|
2
rel/overlays/bin/server.bat
Executable file
2
rel/overlays/bin/server.bat
Executable file
@@ -0,0 +1,2 @@
|
||||
set PHX_SERVER=true
|
||||
call "%~dp0\wabanex" start
|
46
test/growth/calc/age_test.exs
Normal file
46
test/growth/calc/age_test.exs
Normal file
@@ -0,0 +1,46 @@
|
||||
defmodule Growth.Calc.AgeTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Growth.Calc.Age
|
||||
|
||||
describe "calculate/2" do
|
||||
test "consider today as date of measurement" do
|
||||
date_of_birth = Date.add(Date.utc_today(), -7)
|
||||
assert 7 == Age.calculate(:day, date_of_birth)
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculate/3" do
|
||||
test "calculate age in days" do
|
||||
date_of_measurement = ~D[2024-06-09]
|
||||
days_of_birth = [{-1, 1}, {-7, 7}, {-30, 30}]
|
||||
|
||||
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
|
||||
date_of_birth = Date.add(date_of_measurement, days_ago)
|
||||
assert expected_age == Age.calculate(:day, date_of_birth, date_of_measurement)
|
||||
end)
|
||||
end
|
||||
|
||||
test "calculate age in completed weeks" do
|
||||
date_of_measurement = ~D[2024-06-09]
|
||||
days_of_birth = [{-1, 0}, {-7, 1}, {-16, 2}, {-30, 4}]
|
||||
|
||||
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
|
||||
date_of_birth = Date.add(date_of_measurement, days_ago)
|
||||
assert expected_age == Age.calculate(:week, date_of_birth, date_of_measurement)
|
||||
end)
|
||||
end
|
||||
|
||||
test "calculate age in completed months" do
|
||||
date_of_measurement = ~D[2024-06-09]
|
||||
days_of_birth = [{-1, 0}, {-16, 0}, {-31, 1}, {-70, 2}, {-93, 3}]
|
||||
|
||||
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
|
||||
date_of_birth = Date.add(date_of_measurement, days_ago)
|
||||
assert expected_age == Age.calculate(:month, date_of_birth, date_of_measurement)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
23
test/growth/calc/bmi_test.exs
Normal file
23
test/growth/calc/bmi_test.exs
Normal file
@@ -0,0 +1,23 @@
|
||||
defmodule Growth.Calc.BMITest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Growth.Calc.BMI
|
||||
|
||||
describe "calculate/3" do
|
||||
test "in metric system" do
|
||||
weight_kg = 78.5
|
||||
height_cm = 168.2
|
||||
|
||||
assert 27.74710475751505 == BMI.calculate(:metric, weight_kg, height_cm)
|
||||
end
|
||||
|
||||
test "in english system" do
|
||||
weight_lb = 173.1
|
||||
height_in = 66.2
|
||||
|
||||
assert 27.770202207557876 == BMI.calculate(:english, weight_lb, height_in)
|
||||
end
|
||||
end
|
||||
end
|
24
test/growth/calc/centile_test.exs
Normal file
24
test/growth/calc/centile_test.exs
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule Growth.Calc.CentileTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Growth.Data, only: [sample: 0]
|
||||
|
||||
doctest Growth.Calc.Centile
|
||||
|
||||
alias Growth.Calc.Centile
|
||||
|
||||
describe "compute/4" do
|
||||
for %{key: key} = params <- sample() do
|
||||
@tag params: params
|
||||
test "returns the measure given a z-score and box-cox fitted values #{key}", %{
|
||||
params: params
|
||||
} do
|
||||
%{zscore: zscore, measure: measure, l: l, m: m, s: s} = params
|
||||
|
||||
assert_in_delta Centile.compute(zscore, l, m, s), measure, 0.05
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
26
test/growth/calc/percentile_test.exs
Normal file
26
test/growth/calc/percentile_test.exs
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Growth.Calc.PercentileTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
doctest Growth.Calc.Percentile
|
||||
|
||||
alias Growth.Calc.Percentile
|
||||
|
||||
describe "compute/1" do
|
||||
for {zscore, _} = params <- [
|
||||
{-3, 0.0013498125},
|
||||
{-2, 0.0227502617},
|
||||
{-1, 0.1586553192},
|
||||
{0, 0.5000000000},
|
||||
{1, 0.8413446808},
|
||||
{2, 0.9772497383},
|
||||
{3, 0.9986501875}
|
||||
] do
|
||||
@tag params: params
|
||||
test "returns the percentile for z-score #{zscore}", %{params: {zscore, percentile}} do
|
||||
assert_in_delta Percentile.compute(zscore), percentile, 0.0000005
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
24
test/growth/calc/z_score_test.exs
Normal file
24
test/growth/calc/z_score_test.exs
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule Growth.Calc.ZScoreTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import Growth.Data, only: [sample: 0]
|
||||
|
||||
doctest Growth.Calc.ZScore
|
||||
|
||||
alias Growth.Calc.ZScore
|
||||
|
||||
describe "compute/4" do
|
||||
for %{key: key} = params <- sample() do
|
||||
@tag params: params
|
||||
test "returns a z-score given a measurement and box-cox fitted values #{key}", %{
|
||||
params: params
|
||||
} do
|
||||
%{zscore: zscore, measure: measure, l: l, m: m, s: s} = params
|
||||
|
||||
assert_in_delta ZScore.compute(measure, l, m, s), zscore, 0.12
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
131
test/growth/growth_test.exs
Normal file
131
test/growth/growth_test.exs
Normal file
@@ -0,0 +1,131 @@
|
||||
defmodule GrowthTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
doctest Growth
|
||||
|
||||
@child %{
|
||||
name: "Jane Doe",
|
||||
gender: :female,
|
||||
date_of_measurement: Date.utc_today(),
|
||||
age_in_days: 91,
|
||||
weight: 5.84,
|
||||
height: 59.78,
|
||||
head_circumference: 39.51,
|
||||
arm_circumference: 13.02,
|
||||
subscapular_skinfold: 7.79,
|
||||
triceps_skinfold: 9.75
|
||||
}
|
||||
@child_date_of_birth Date.shift(@child.date_of_measurement, day: @child.age_in_days * -1)
|
||||
|
||||
describe "new/4" do
|
||||
test "create a new growth measurement" do
|
||||
growth =
|
||||
Growth.new(@child.name, @child.gender, @child_date_of_birth,
|
||||
date_of_measurement: @child.date_of_measurement,
|
||||
weight: @child.weight,
|
||||
height: @child.height,
|
||||
head_circumference: @child.head_circumference,
|
||||
arm_circumference: @child.arm_circumference,
|
||||
subscapular_skinfold: @child.subscapular_skinfold,
|
||||
triceps_skinfold: @child.triceps_skinfold
|
||||
)
|
||||
|
||||
assert @child.name == growth.name
|
||||
assert @child.gender == growth.gender
|
||||
assert @child_date_of_birth == growth.date_of_birth
|
||||
assert @child.date_of_measurement == growth.date_of_measurement
|
||||
assert @child.age_in_days == growth.age_in_days
|
||||
assert 13 == growth.age_in_weeks
|
||||
assert 2 == growth.age_in_months
|
||||
assert @child.weight == growth.weight
|
||||
assert @child.height == growth.height
|
||||
assert @child.head_circumference == growth.head_circumference
|
||||
assert @child.arm_circumference == growth.arm_circumference
|
||||
assert @child.subscapular_skinfold == growth.subscapular_skinfold
|
||||
assert @child.triceps_skinfold == growth.triceps_skinfold
|
||||
end
|
||||
end
|
||||
|
||||
describe "with_bmi/1" do
|
||||
test "calculate bmi from measurement" do
|
||||
growth =
|
||||
%Growth{
|
||||
name: @child.name,
|
||||
gender: @child.gender,
|
||||
date_of_birth: @child_date_of_birth,
|
||||
date_of_measurement: @child.date_of_measurement,
|
||||
weight: @child.weight,
|
||||
height: @child.height,
|
||||
head_circumference: @child.head_circumference,
|
||||
arm_circumference: @child.arm_circumference,
|
||||
subscapular_skinfold: @child.subscapular_skinfold,
|
||||
triceps_skinfold: @child.triceps_skinfold
|
||||
}
|
||||
|> Growth.with_age_in_days()
|
||||
|> Growth.with_age_in_weeks()
|
||||
|> Growth.with_age_in_months()
|
||||
|> Growth.with_bmi()
|
||||
|
||||
assert 16.341842694989243 == growth.bmi
|
||||
end
|
||||
end
|
||||
|
||||
describe "with_results/1" do
|
||||
test "calculate z-score and percentiles from measurement" do
|
||||
growth =
|
||||
%Growth{
|
||||
name: @child.name,
|
||||
gender: @child.gender,
|
||||
date_of_birth: @child_date_of_birth,
|
||||
date_of_measurement: @child.date_of_measurement,
|
||||
weight: @child.weight,
|
||||
height: @child.height,
|
||||
head_circumference: @child.head_circumference,
|
||||
arm_circumference: @child.arm_circumference,
|
||||
subscapular_skinfold: @child.subscapular_skinfold,
|
||||
triceps_skinfold: @child.triceps_skinfold,
|
||||
results: []
|
||||
}
|
||||
|> Growth.with_age_in_days()
|
||||
|> Growth.with_age_in_weeks()
|
||||
|> Growth.with_age_in_months()
|
||||
|> Growth.with_bmi()
|
||||
|> Growth.with_results()
|
||||
|
||||
assert [
|
||||
day: {9.496948997971584e-4, 0.5003788733920584},
|
||||
week: {9.496948997971584e-4, 0.5003788733920584},
|
||||
month: {1.0060928051683196, 0.8428145353253619}
|
||||
] == Keyword.get(growth.results, :weight)
|
||||
|
||||
assert [
|
||||
day: {0.0012831717968983271, 0.5005119113423219},
|
||||
week: {0.0012831717968983271, 0.5005119113423219},
|
||||
month: {1.3322618635180914, 0.9086129228760054}
|
||||
] == Keyword.get(growth.results, :height)
|
||||
|
||||
assert [
|
||||
day: {-0.007440424136462911, 0.4970317276150869},
|
||||
week: {-0.007440424136462911, 0.4970317276150869},
|
||||
month: {0.3827194919327071, 0.6490361198066796}
|
||||
] == Keyword.get(growth.results, :bmi)
|
||||
|
||||
assert [
|
||||
day: {-0.008864109494641385, 0.4964637782528006},
|
||||
week: {-0.008864109494641385, 0.4964637782528006},
|
||||
month: {1.0380198575748647, 0.8503695948597997}
|
||||
] == Keyword.get(growth.results, :head_circumference)
|
||||
|
||||
assert [day: {-0.004182676756535293, 0.49833135826198854}] ==
|
||||
Keyword.get(growth.results, :arm_circumference)
|
||||
|
||||
assert [day: {0.001811404894950268, 0.5007226456043324}] ==
|
||||
Keyword.get(growth.results, :subscapular_skinfold)
|
||||
|
||||
assert [day: {-0.0019309186728254878, 0.49922967538007906}] ==
|
||||
Keyword.get(growth.results, :triceps_skinfold)
|
||||
end
|
||||
end
|
||||
end
|
297
test/growth/indicators/download_test.exs
Normal file
297
test/growth/indicators/download_test.exs
Normal file
@@ -0,0 +1,297 @@
|
||||
defmodule Growth.Indicators.DownloadTest do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Elixlsx.Sheet
|
||||
alias Elixlsx.Workbook
|
||||
|
||||
alias Growth.Indicators.Download
|
||||
|
||||
setup do
|
||||
mock_who_request()
|
||||
end
|
||||
|
||||
describe "process/3" do
|
||||
test "fetch excel from url and convert it to a list of lists" do
|
||||
gender = :female
|
||||
category = :age_tables
|
||||
|
||||
url =
|
||||
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx"
|
||||
|
||||
content = Download.process(gender, category, url)
|
||||
|
||||
expected_content =
|
||||
[
|
||||
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
|
||||
[
|
||||
url,
|
||||
category,
|
||||
gender,
|
||||
"week",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.6"),
|
||||
Decimal.new("45.4"),
|
||||
Decimal.new("47.3"),
|
||||
Decimal.new("49.1"),
|
||||
Decimal.new("51"),
|
||||
Decimal.new("52.9"),
|
||||
Decimal.new("54.7")
|
||||
]
|
||||
]
|
||||
|
||||
assert expected_content == content
|
||||
end
|
||||
end
|
||||
|
||||
describe "process_gender/1" do
|
||||
test "merge multiple contents into a single one" do
|
||||
category = :age_tables
|
||||
gender = :female
|
||||
|
||||
urls =
|
||||
[
|
||||
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx",
|
||||
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx"
|
||||
]
|
||||
|
||||
content = Download.process_gender({gender, category, urls})
|
||||
|
||||
expected_content =
|
||||
[
|
||||
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
|
||||
[
|
||||
List.first(urls),
|
||||
category,
|
||||
gender,
|
||||
"week",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.6"),
|
||||
Decimal.new("45.4"),
|
||||
Decimal.new("47.3"),
|
||||
Decimal.new("49.1"),
|
||||
Decimal.new("51"),
|
||||
Decimal.new("52.9"),
|
||||
Decimal.new("54.7")
|
||||
],
|
||||
[
|
||||
List.last(urls),
|
||||
category,
|
||||
gender,
|
||||
"month",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.6"),
|
||||
Decimal.new("45.4"),
|
||||
Decimal.new("47.3"),
|
||||
Decimal.new("49.1"),
|
||||
Decimal.new("51"),
|
||||
Decimal.new("52.9"),
|
||||
Decimal.new("54.7")
|
||||
]
|
||||
]
|
||||
|
||||
assert expected_content == content
|
||||
end
|
||||
end
|
||||
|
||||
describe "process_genders/1" do
|
||||
test "merge multiple genders and contents into a single one" do
|
||||
female_urls = %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-girls-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
|
||||
male_urls = %{
|
||||
age_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-13-weeks_zscores.xlsx
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-2-years_zscores.xlsx
|
||||
),
|
||||
expanded_tables: ~w(
|
||||
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-boys-zscore-expanded-tables.xlsx
|
||||
)
|
||||
}
|
||||
|
||||
content = Download.process_genders(%{female: female_urls, male: male_urls})
|
||||
|
||||
expected_content =
|
||||
[
|
||||
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
|
||||
[
|
||||
female_urls |> Map.get(:age_tables) |> List.first(),
|
||||
:age,
|
||||
:female,
|
||||
"week",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.6"),
|
||||
Decimal.new("45.4"),
|
||||
Decimal.new("47.3"),
|
||||
Decimal.new("49.1"),
|
||||
Decimal.new("51"),
|
||||
Decimal.new("52.9"),
|
||||
Decimal.new("54.7")
|
||||
],
|
||||
[
|
||||
female_urls |> Map.get(:age_tables) |> List.last(),
|
||||
:age,
|
||||
:female,
|
||||
"month",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.6"),
|
||||
Decimal.new("45.4"),
|
||||
Decimal.new("47.3"),
|
||||
Decimal.new("49.1"),
|
||||
Decimal.new("51"),
|
||||
Decimal.new("52.9"),
|
||||
Decimal.new("54.7")
|
||||
],
|
||||
[
|
||||
male_urls |> Map.get(:age_tables) |> List.first(),
|
||||
:age,
|
||||
:male,
|
||||
"week",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.8842"),
|
||||
Decimal.new("0.03795"),
|
||||
Decimal.new("44.2"),
|
||||
Decimal.new("46.1"),
|
||||
Decimal.new("48"),
|
||||
Decimal.new("49.9"),
|
||||
Decimal.new("51.8"),
|
||||
Decimal.new("53.7"),
|
||||
Decimal.new("55.6")
|
||||
],
|
||||
[
|
||||
male_urls |> Map.get(:age_tables) |> List.last(),
|
||||
:age,
|
||||
:male,
|
||||
"month",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.8842"),
|
||||
Decimal.new("0.03795"),
|
||||
Decimal.new("44.2"),
|
||||
Decimal.new("46.1"),
|
||||
Decimal.new("48"),
|
||||
Decimal.new("49.9"),
|
||||
Decimal.new("51.8"),
|
||||
Decimal.new("53.7"),
|
||||
Decimal.new("55.6")
|
||||
],
|
||||
[
|
||||
female_urls |> Map.get(:expanded_tables) |> List.first(),
|
||||
:expanded,
|
||||
:female,
|
||||
"day",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.1477"),
|
||||
Decimal.new("0.0379"),
|
||||
Decimal.new("43.56"),
|
||||
Decimal.new("45.422"),
|
||||
Decimal.new("47.285"),
|
||||
Decimal.new("49.148"),
|
||||
Decimal.new("51.01"),
|
||||
Decimal.new("52.873"),
|
||||
Decimal.new("54.736")
|
||||
],
|
||||
[
|
||||
male_urls |> Map.get(:expanded_tables) |> List.first(),
|
||||
:expanded,
|
||||
:male,
|
||||
"day",
|
||||
Decimal.new("0"),
|
||||
Decimal.new("1"),
|
||||
Decimal.new("49.8842"),
|
||||
Decimal.new("0.03795"),
|
||||
Decimal.new("44.205"),
|
||||
Decimal.new("46.098"),
|
||||
Decimal.new("47.991"),
|
||||
Decimal.new("49.884"),
|
||||
Decimal.new("51.777"),
|
||||
Decimal.new("53.67"),
|
||||
Decimal.new("55.564")
|
||||
]
|
||||
]
|
||||
|
||||
assert expected_content == content
|
||||
end
|
||||
end
|
||||
|
||||
defp mock_who_request do
|
||||
Req.Test.stub(Growth.Indicators.Download.WHO, fn %Plug.Conn{path_info: path_info} = conn ->
|
||||
filename = List.last(path_info)
|
||||
|
||||
rows =
|
||||
case filename do
|
||||
"lhfa_girls_0-to-13-weeks_zscores.xlsx" ->
|
||||
[
|
||||
~w(Week L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
|
||||
~w(0 1 49.1477 0.0379 1.8627 43.6 45.4 47.3 49.1 51 52.9 54.7)
|
||||
]
|
||||
|
||||
"lhfa_girls_0-to-2-years_zscores.xlsx" ->
|
||||
[
|
||||
~w(Month L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
|
||||
~w(0 1 49.1477 0.0379 1.8627 43.6 45.4 47.3 49.1 51 52.9 54.7)
|
||||
]
|
||||
|
||||
"lhfa-girls-zscore-expanded-tables.xlsx" ->
|
||||
[
|
||||
~w(Day L M S SD4neg SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3 SD4),
|
||||
~w(0 1 49.1477 0.0379 41.697 43.56 45.422 47.285 49.148 51.01 52.873 54.736 56.598)
|
||||
]
|
||||
|
||||
"lhfa_boys_0-to-13-weeks_zscores.xlsx" ->
|
||||
[
|
||||
~w(Week L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
|
||||
~w(0 1 49.8842 0.03795 1.8931 44.2 46.1 48 49.9 51.8 53.7 55.6)
|
||||
]
|
||||
|
||||
"lhfa_boys_0-to-2-years_zscores.xlsx" ->
|
||||
[
|
||||
~w(Month L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
|
||||
~w(0 1 49.8842 0.03795 1.8931 44.2 46.1 48 49.9 51.8 53.7 55.6)
|
||||
]
|
||||
|
||||
"lhfa-boys-zscore-expanded-tables.xlsx" ->
|
||||
[
|
||||
~w(Day L M S SD4neg SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3 SD4),
|
||||
~w(0 1 49.8842 0.03795 42.312 44.205 46.098 47.991 49.884 51.777 53.67 55.564 57.457)
|
||||
]
|
||||
end
|
||||
|
||||
{:ok, {_charlist_content, content}} =
|
||||
%Sheet{name: "zscore", rows: rows}
|
||||
|> then(&%Workbook{sheets: [&1]})
|
||||
|> Elixlsx.write_to_memory(filename)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
)
|
||||
|> Plug.Conn.send_resp(200, content)
|
||||
end)
|
||||
end
|
||||
end
|
142
test/growth/score/weight_for_height_test.exs
Normal file
142
test/growth/score/weight_for_height_test.exs
Normal file
@@ -0,0 +1,142 @@
|
||||
# test/growth/score/weight_for_height_test.exs
|
||||
defmodule Growth.Score.WeightForHeightTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Growth
|
||||
alias Growth.Score.WeightForHeight
|
||||
|
||||
describe "measure_name/0" do
|
||||
test "returns the correct measure name atom" do
|
||||
assert WeightForHeight.measure_name() == :weight_for_height
|
||||
end
|
||||
end
|
||||
|
||||
describe "lms/2" do
|
||||
test "returns LMS values for exact height match" do
|
||||
growth = %Growth{
|
||||
gender: :male,
|
||||
weight: 7.3,
|
||||
height: 65.0,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth, WeightForHeight) == [
|
||||
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns LMS values for closest height match when exact is not found" do
|
||||
growth = %Growth{
|
||||
gender: :male,
|
||||
weight: 7.3,
|
||||
height: 64.9,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth, WeightForHeight) == [
|
||||
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
|
||||
]
|
||||
|
||||
growth_closer_to_higher = %Growth{
|
||||
gender: :male,
|
||||
weight: 7.4,
|
||||
height: 65.09,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth_closer_to_higher, WeightForHeight) == [
|
||||
{"height", {-0.35210000000000002, 7.4562999999999997, 0.082159999999999997}}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns empty list if weight or height is missing" do
|
||||
growth_no_weight = %Growth{
|
||||
gender: :male,
|
||||
height: 60.0,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth_no_weight, WeightForHeight) == []
|
||||
|
||||
growth_no_height = %Growth{
|
||||
gender: :male,
|
||||
weight: 5.0,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth_no_height, WeightForHeight) == []
|
||||
|
||||
growth_nil_values = %Growth{
|
||||
gender: :male,
|
||||
weight: nil,
|
||||
height: nil,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth_nil_values, WeightForHeight) == []
|
||||
end
|
||||
|
||||
test "returns empty list if no matching gender data exists" do
|
||||
# Assuming no :unknown gender data was inserted
|
||||
growth = %Growth{
|
||||
gender: :unknown,
|
||||
weight: 5.0,
|
||||
height: 60.0,
|
||||
name: "Joe Doe",
|
||||
date_of_measurement: Date.utc_today(),
|
||||
date_of_birth: Date.shift(Date.utc_today(), day: -91)
|
||||
}
|
||||
|
||||
assert WeightForHeight.lms(growth, WeightForHeight) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "find_closest_height/2" do
|
||||
test "finds the lower closest height" do
|
||||
assert WeightForHeight.find_closest_height(:male, 64.99) == [
|
||||
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
|
||||
]
|
||||
end
|
||||
|
||||
test "finds the higher closest height" do
|
||||
assert WeightForHeight.find_closest_height(:male, 65.61) == [
|
||||
{"height", {-0.35210000000000002, 7.5738000000000003, 0.082140000000000005}}
|
||||
]
|
||||
end
|
||||
|
||||
test "finds the lower closest height when exactly midway" do
|
||||
expected_lms =
|
||||
[
|
||||
[{"height", {-0.35210000000000002, 7.5034000000000001, 0.082150000000000001}}],
|
||||
[{"height", {-0.35210000000000002, 7.4798999999999998, 0.082159999999999997}}]
|
||||
]
|
||||
|
||||
lms = WeightForHeight.find_closest_height(:male, 65.25)
|
||||
assert Enum.any?(expected_lms, fn entity_lms -> entity_lms == lms end)
|
||||
end
|
||||
|
||||
test "do not match when below minimum height" do
|
||||
assert WeightForHeight.find_closest_height(:male, 44.0) == []
|
||||
end
|
||||
|
||||
test "do not match when above maximum height" do
|
||||
assert WeightForHeight.find_closest_height(:male, 121.0) == []
|
||||
end
|
||||
|
||||
test "returns empty list when no data exists for the gender" do
|
||||
assert WeightForHeight.find_closest_height(:unknown, 60.0) == []
|
||||
end
|
||||
end
|
||||
end
|
123
test/support/growth_data.ex
Normal file
123
test/support/growth_data.ex
Normal file
@@ -0,0 +1,123 @@
|
||||
defmodule Growth.Data do
|
||||
@moduledoc """
|
||||
Sample data for growth tests based on WHO csv files containing:
|
||||
|
||||
* source measure
|
||||
* gender
|
||||
* age_unit
|
||||
* box-cox fitted params:
|
||||
* l
|
||||
* m
|
||||
* s
|
||||
* expeceted measure values at given z-scores:
|
||||
* -3
|
||||
* -2
|
||||
* -1
|
||||
* 0
|
||||
* 1
|
||||
* 2
|
||||
* 3
|
||||
"""
|
||||
|
||||
NimbleCSV.define(Growth.DataCase, separator: ",", escape: "\"")
|
||||
|
||||
# NOTE: (jpd) this combined table will be used on zscore and centile test to cross-validate the results using
|
||||
# different genders, ages units, and measures.
|
||||
@sample_combined_csv """
|
||||
source,gender,age_unit,age,l,m,s,sd3neg,sd2neg,sd1neg,sd0,sd1,sd2,sd3
|
||||
arm-circumference-for-age,female,month,3,-0.17330000000000001,13.0284,0.082629999999999995,10.199999999999999,11.1,12,13,14.2,15.4,16.8
|
||||
arm-circumference-for-age,female,day,91,-0.17330000000000001,13.0245,0.082619999999999999,10.218,11.066000000000001,11.999000000000001,13.023999999999999,14.154999999999999,15.401999999999999,16.78
|
||||
arm-circumference-for-age,male,month,3,0.39279999999999998,13.4817,0.074749999999999997,10.7,11.6,12.5,13.5,14.5,15.6,16.7
|
||||
arm-circumference-for-age,male,day,91,0.39329999999999998,13.4779,0.074740000000000001,10.657999999999999,11.554,12.493,13.478,14.507999999999999,15.585000000000001,16.709
|
||||
body-mass-index-for-age,female,day,28,0.36370000000000002,14.4208,0.095769999999999994,10.646000000000001,11.824,13.081,14.420999999999999,15.843999999999999,17.353999999999999,18.952999999999999
|
||||
body-mass-index-for-age,female,week,4,0.36370000000000002,14.4208,0.095769999999999994,10.6,11.8,13.1,14.4,15.8,17.399999999999999,19
|
||||
body-mass-index-for-age,female,month,1,0.3448,14.5679,0.095560000000000006,10.8,12,13.2,14.6,16,17.5,19.100000000000001
|
||||
body-mass-index-for-age,male,day,28,0.28810000000000002,14.7714,0.090719999999999995,11.125999999999999,12.26,13.474,14.771000000000001,16.155000000000001,17.629000000000001,19.196000000000002
|
||||
body-mass-index-for-age,male,week,4,0.28810000000000002,14.7714,0.090719999999999995,11.1,12.3,13.5,14.8,16.2,17.600000000000001,19.2
|
||||
body-mass-index-for-age,male,month,1,0.27079999999999999,14.944100000000001,0.090270000000000003,11.3,12.4,13.6,14.9,16.3,17.8,19.399999999999999
|
||||
head-circumference-for-age,female,day,28,1,36.376100000000001,0.032149999999999998,32.868000000000002,34.036999999999999,35.207000000000001,36.375999999999998,37.545999999999999,38.715000000000003,39.884999999999998
|
||||
head-circumference-for-age,female,week,4,1,36.376100000000001,0.032149999999999998,32.9,34,35.200000000000003,36.4,37.5,38.700000000000003,39.9
|
||||
head-circumference-for-age,female,month,1,1,36.546300000000002,0.032099999999999997,33,34.200000000000003,35.4,36.5,37.700000000000003,38.9,40.1
|
||||
head-circumference-for-age,male,day,28,1,37.092599999999997,0.031480000000000001,33.590000000000003,34.756999999999998,35.924999999999997,37.093000000000004,38.26,39.427999999999997,40.595999999999997
|
||||
head-circumference-for-age,male,week,4,1,37.092599999999997,0.031480000000000001,33.6,34.799999999999997,35.9,37.1,38.299999999999997,39.4,40.6
|
||||
head-circumference-for-age,male,month,1,1,37.2759,0.031329999999999997,33.799999999999997,34.9,36.1,37.299999999999997,38.4,39.6,40.799999999999997
|
||||
length-height-for-age,female,day,28,1,53.380899999999997,0.036470000000000002,47.54,49.487000000000002,51.433999999999997,53.381,55.328000000000003,57.274999999999999,59.220999999999997
|
||||
length-height-for-age,female,week,4,1,53.380899999999997,0.036470000000000002,47.5,49.5,51.4,53.4,55.3,57.3,59.2
|
||||
length-height-for-age,female,month,1,1,53.687199999999997,0.036400000000000002,47.8,49.8,51.7,53.7,55.6,57.6,59.5
|
||||
length-height-for-age,male,day,28,1,54.388100000000001,0.035700000000000003,48.563000000000002,50.505000000000003,52.445999999999998,54.387999999999998,56.33,58.271000000000001,60.213000000000001
|
||||
length-height-for-age,male,week,4,1,54.388100000000001,0.035700000000000003,48.6,50.5,52.4,54.4,56.3,58.3,60.2
|
||||
length-height-for-age,male,month,1,1,54.724400000000003,0.035569999999999997,48.9,50.8,52.8,54.7,56.7,58.6,60.6
|
||||
subscapular-skinfold-for-age,female,day,91,-0.2019,7.7873999999999999,0.18428,4.6109999999999998,5.4580000000000002,6.4989999999999997,7.7869999999999999,9.3960000000000008,11.422000000000001,13.994999999999999
|
||||
subscapular-skinfold-for-age,female,month,3,-0.2026,7.7846000000000002,0.18428,4.5999999999999996,5.5,6.5,7.8,9.4,11.4,14
|
||||
subscapular-skinfold-for-age,male,day,91,-0.30299999999999999,7.6920000000000002,0.17019000000000001,4.7850000000000001,5.5640000000000001,6.516,7.6920000000000002,9.1609999999999996,11.016999999999999,13.395
|
||||
subscapular-skinfold-for-age,male,month,3,-0.30330000000000001,7.6898999999999997,0.17019999999999999,4.8,5.6,6.5,7.7,9.1999999999999993,11,13.4
|
||||
triceps-skinfold-for-age,female,day,91,0.18820000000000001,9.7532999999999994,0.17524999999999999,5.6070000000000002,6.7869999999999999,8.1609999999999996,9.7530000000000001,11.589,13.695,16.102
|
||||
triceps-skinfold-for-age,female,month,3,0.1875,9.7515999999999998,0.17535000000000001,5.6,6.8,8.1999999999999993,9.8000000000000007,11.6,13.7,16.100000000000001
|
||||
triceps-skinfold-for-age,male,day,91,0.0030000000000000001,9.7658000000000005,0.16611000000000001,5.931,7.0039999999999996,8.2710000000000008,9.766,11.53,13.612,16.068000000000001
|
||||
triceps-skinfold-for-age,male,month,3,0.0027000000000000001,9.7638999999999996,0.16617999999999999,5.9,7,8.3000000000000007,9.8000000000000007,11.5,13.6,16.100000000000001
|
||||
weight-for-age,female,day,28,0.1789,4.0987,0.13805000000000001,2.665,3.0880000000000001,3.5640000000000001,4.0990000000000002,4.6980000000000004,5.3659999999999997,6.1120000000000001
|
||||
weight-for-age,female,week,4,0.1789,4.0987,0.13805000000000001,2.7,3.1,3.6,4.0999999999999996,4.7,5.4,6.1
|
||||
weight-for-age,female,month,1,0.1714,4.1872999999999996,0.13724,2.7,3.2,3.6,4.2,4.8,5.5,6.2
|
||||
weight-for-age,male,day,28,0.2331,4.3670999999999998,0.13497000000000001,2.8540000000000001,3.3050000000000002,3.8069999999999999,4.367,4.9880000000000004,5.6740000000000004,6.43
|
||||
weight-for-age,male,week,4,0.2331,4.3670999999999998,0.13497000000000001,2.9,3.3,3.8,4.4000000000000004,5,5.7,6.4
|
||||
weight-for-age,male,month,1,0.22969999999999999,4.4709000000000003,0.13395000000000001,2.9,3.4,3.9,4.5,5.0999999999999996,5.8,6.6
|
||||
weight-for-height,female,length,65,-0.38329999999999997,7.0811999999999999,0.091189999999999993,5.4589999999999996,5.9370000000000003,6.4740000000000002,7.0810000000000004,7.77,8.5549999999999997,9.4540000000000006
|
||||
weight-for-height,female,length,65.099999999999994,-0.38329999999999997,7.1040999999999999,0.091179999999999997,5.4770000000000003,5.9560000000000004,6.4950000000000001,7.1040000000000001,7.7949999999999999,8.5820000000000007,9.484
|
||||
weight-for-height,female,height,65,-0.38329999999999997,7.2401999999999997,0.091130000000000003,5.5830000000000002,6.0709999999999997,6.62,7.24,7.944,8.7460000000000004,9.6639999999999997
|
||||
weight-for-height,female,height,65.099999999999994,-0.38329999999999997,7.2626999999999997,0.091120000000000007,5.6,6.09,6.64,7.2629999999999999,7.9690000000000003,8.7729999999999997,9.6940000000000008
|
||||
weight-for-height,male,length,65,-0.35210000000000002,7.2666000000000004,0.082229999999999998,5.7359999999999998,6.1929999999999996,6.7009999999999996,7.2670000000000003,7.899,8.6080000000000005,9.4060000000000006
|
||||
weight-for-height,male,length,65.099999999999994,-0.35210000000000002,7.2904999999999998,0.082220000000000001,5.7549999999999999,6.2130000000000001,6.7229999999999999,7.29,7.9249999999999998,8.6359999999999992,9.4369999999999994
|
||||
weight-for-height,male,height,65,-0.35210000000000002,7.4326999999999996,0.082170000000000007,5.8680000000000003,6.335,6.8540000000000001,7.4329999999999998,8.0790000000000006,8.8040000000000003,9.6189999999999998
|
||||
weight-for-height,male,height,65.099999999999994,-0.35210000000000002,7.4562999999999997,0.082159999999999997,5.8869999999999996,6.3550000000000004,6.8760000000000003,7.4560000000000004,8.1050000000000004,8.8309999999999995,9.6489999999999991
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Sample data to validate calculation made to convert measure into z-score and vice-versa.
|
||||
|
||||
This data is based on values extracted from WHO csv files.
|
||||
"""
|
||||
@spec sample :: [
|
||||
%{
|
||||
key: String.t(),
|
||||
zscore: number(),
|
||||
measure: number(),
|
||||
l: number(),
|
||||
m: number(),
|
||||
s: number()
|
||||
}
|
||||
]
|
||||
def sample do
|
||||
@sample_combined_csv
|
||||
|> Growth.DataCase.parse_string()
|
||||
|> Enum.flat_map(fn [
|
||||
source,
|
||||
gender,
|
||||
age_unit,
|
||||
age,
|
||||
l,
|
||||
m,
|
||||
s,
|
||||
sd3n,
|
||||
sd2n,
|
||||
sd1n,
|
||||
sd0,
|
||||
sd1,
|
||||
sd2,
|
||||
sd3
|
||||
] ->
|
||||
key = "#{source}:#{gender}:#{age_unit}:#{age}"
|
||||
|
||||
[l, m, s, sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3] =
|
||||
Enum.map(
|
||||
[l, m, s, sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3],
|
||||
&(&1 |> Float.parse() |> elem(0))
|
||||
)
|
||||
|
||||
[-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]
|
||||
|> Enum.zip([sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3])
|
||||
|> Enum.map(fn {zscore, measure} ->
|
||||
%{key: "#{key}:#{zscore}", zscore: zscore, measure: measure, l: l, m: m, s: s}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
@@ -11,6 +11,7 @@ defmodule WabanexWeb.Schema.Types.Custom.DateRangeTest do
|
||||
import_types Types.Custom.DateRange
|
||||
|
||||
query do
|
||||
field :everything, :string, resolve: fn _, _, _ -> "is awesome" end
|
||||
end
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user