diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..2442695 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,43 @@ +name: CI +on: + pull_request: + push: +jobs: + build-tag-push-deploy: + runs-on: ubuntu-latest + if: > + github.ref == 'refs/heads/main' || + github.ref == 'refs/heads/dev' + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: cmu-delphi-deploy-machine + password: ${{ secrets.CMU_DELPHI_DEPLOY_MACHINE_PAT }} + - name: Build, tag, and push image to GitHub Container Registry + id: image + run: | + baseRef="${GITHUB_REF#*/}" + baseRef="${baseRef#*/}" + case "${baseRef}" in + main) + image_tag="latest" + ;; + *) + image_tag="${baseRef//\//_}" # replace `/` with `_` in branch name + ;; + esac + cd ${{ github.workspace }} + echo "using tag: --${image-tag}--" + docker build -t ghcr.io/${{ github.repository }}:${image_tag} --file ./devops/Dockerfile . + docker push ghcr.io/${{ github.repository }}:${image_tag} + echo "IMAGE_TAG=${image_tag}" >> $GITHUB_OUTPUT + - name: Trigger smee.io webhook to deploy new container image + run: | + curl -H "Authorization: Bearer ${{ secrets.DELPHI_DEPLOY_WEBHOOK_TOKEN }}" \ + -X POST ${{ secrets.DELPHI_DEPLOY_WEBHOOK_URL }} \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "repository=ghcr.io/${{ github.repository }}&tag=${{ steps.image.outputs.IMAGE_TAG }}" diff --git a/README.md b/README.md index 06bb328..d50340a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,39 @@ -# Status - -[![Deploy Status](https://delphi.midas.cs.cmu.edu/~automation/public/github_deploy_repo/badge.php?repo=cmu-delphi/www-crowdcast)](#) # About -The Crowdcast website for collecting flu forecasts. +The Crowdcast website for collecting flu forecasts. (Previously known as +Epicast.) + +The site is live at . + +# Branches + +The website is deployed to two separate environments: `staging` and `production`. The code for those environments is kept in the +[`dev`](https://github.com/cmu-delphi/www-epicast/tree/dev) and +[`main`](https://github.com/cmu-delphi/www-epicast/tree/main) branches, +respectively. + +## `dev` branch + +The `dev` branch is deployed to a Delphi-internal development environment where we can iterate quickly without worry of breaking the production site. + +## `main` branch + +The `main` branch is deployed to a public-facing production environment. It +should contain only tested and reliable code. + +## Process + +**`main` should not be updated while a forecasting round is active** (i.e. +Friday through Monday), except in case of a critical bugfix. + +Basic develop changes -> deploy/review in staging -> release to production workflow: + +- Start by creating a **[bug|fix|feature|etc]** branch based on `dev`. +- Make a PR and tag a reviewer with your changes against `dev`. Once apporved and merged this will trigger CI to deploy the application at https://staging.delphi.cmu.edu/crowdcast. +- Once staging is reviewed and deemed acceptable, make a PR against `main` and tag a reviewer. Once this is approved and merged the production version of the application will be available at https://delphi.cmu.edu/crowdcast -The site is live at: -- https://delphi.cmu.edu/crowdcast +# Development -A live demo is available at http://demo.epicast.net/ (User ID is `00000000`) +For developing the website, see the +[epicast development guide](docs/epicast_development.md). diff --git a/deploy.json b/deploy.json index ffdf95a..a730619 100644 --- a/deploy.json +++ b/deploy.json @@ -7,42 +7,35 @@ { "type": "move", "src": "site/.htaccess", - "dst": "/var/www/html/crowdcast/.htaccess", + "dst": "/var/www/html/cc-test/.htaccess", "add-header-comment": false }, - "// service worker", - { - "type": "move", - "src": "site/sw.js", - "dst": "/var/www/html/crowdcast/sw.js", - "add-header-comment": true - }, "// web sources", { "type": "move", "src": "site/", - "dst": "/var/www/html/crowdcast/", + "dst": "/var/www/html/cc-test/", "match": "^.*\\.php$", "add-header-comment": true }, { "type": "move", "src": "site/common/", - "dst": "/var/www/html/crowdcast/common/", + "dst": "/var/www/html/cc-test/common/", "match": "^.*\\.php$", "add-header-comment": true }, { "type": "move", "src": "site/css/", - "dst": "/var/www/html/crowdcast/css/", - "match": "^.*\\.(css|php)$", + "dst": "/var/www/html/cc-test/css/", + "match": "^.*\\.(php)$", "add-header-comment": true }, { "type": "move", "src": "site/js/", - "dst": "/var/www/html/crowdcast/js/", + "dst": "/var/www/html/cc-test/js/", "match": "^.*\\.js$", "add-header-comment": true }, @@ -51,21 +44,21 @@ { "type": "move", "src": "site/images/flags/", - "dst": "/var/www/html/crowdcast/images/flags/", + "dst": "/var/www/html/cc-test/images/flags/", "match": "^.*\\.png$" }, "// tutorials", { "type": "move", "src": "site/images/", - "dst": "/var/www/html/crowdcast/images/", + "dst": "/var/www/html/cc-test/images/", "match": "^.*\\.(gif|mp4)$" }, "// benchmark data", { "type":"move", "src":"site/data/", - "dst":"/var/www/html/crowdcast/data/", + "dst":"/var/www/html/cc-test/data/", "match":"^.*\\.csv$" } ] diff --git a/dev/docker/database/epicast/Dockerfile b/dev/docker/database/epicast/Dockerfile new file mode 100644 index 0000000..18681fa --- /dev/null +++ b/dev/docker/database/epicast/Dockerfile @@ -0,0 +1,19 @@ +# start with the `delphi_database` image +FROM delphi_database + +# create the `epicast2` database +ENV MYSQL_DATABASE epicast2 + +# create the `epi` user account with a development-only password +ENV MYSQL_USER user +ENV MYSQL_PASSWORD pass + +# provide DDL which will create empty tables at container startup +# note that files are executed in order of filename alphabetically, so here +# destination files are named such that table definitions are executed prior to +# data insertions +COPY repos/delphi/www-epicast/src/ddl/epicast2.sql /docker-entrypoint-initdb.d/0_epicast2.sql +COPY repos/delphi/www-epicast/src/ddl/development_data.sql /docker-entrypoint-initdb.d/1_development_data.sql + +# ensure files are readable at runtime +RUN chmod o+r /docker-entrypoint-initdb.d/* diff --git a/dev/docker/database/epicast/README.md b/dev/docker/database/epicast/README.md new file mode 100644 index 0000000..6b767cd --- /dev/null +++ b/dev/docker/database/epicast/README.md @@ -0,0 +1,27 @@ +# `delphi_database_epicast` + +This image extends Delphi's database by: + +- adding the `epi` user account +- adding the `epicast2` database +- creating and minimally populating tables in `epicast2` + +To start a container from this image, run: + +```bash +docker run --rm -p 13306:3306 \ + --network delphi-net --name delphi_database_epicast \ + delphi_database_epicast +``` + +For debugging purposes, you can interactively connect to the database inside +the container using a `mysql` client (either installed locally or supplied via +a docker image) like this: + +```bash +mysql --user=user --password=pass --port 13306 --host 127.0.0.1 epicast2 +``` + +Note that using host `localhost` may fail on some platforms as mysql will +attempt, and fail, to use a Unix socket. Using `127.0.0.1`, which implies +TCP/IP, works instead. diff --git a/dev/docker/web/epicast/Dockerfile b/dev/docker/web/epicast/Dockerfile new file mode 100644 index 0000000..3b5ca83 --- /dev/null +++ b/dev/docker/web/epicast/Dockerfile @@ -0,0 +1,30 @@ +# start with the `delphi_web` image +FROM delphi_web + +# enable mod_rewrite, aka RewriteEngine +RUN a2enmod rewrite + +# enable mod_headers +RUN a2enmod headers + +# create an empty htpasswd file +RUN touch /var/www/passwords + +# deploy the Epicast website (see `www-epicast/deploy.json`) +# deploy files in an order that tries to take advantage of build caching + +COPY repos/delphi/www-epicast/site/images/* /var/www/html/images/ +COPY repos/delphi/www-epicast/site/images/flags/* /var/www/html/images/flags/ +COPY repos/delphi/www-epicast/site/data/* /var/www/html/data/ + +COPY repos/delphi/www-epicast/site/css/* /var/www/html/css/ +COPY repos/delphi/www-epicast/site/js/*.js /var/www/html/js/ +COPY repos/delphi/www-epicast/site/js/us-map /var/www/html/js/us-map +COPY repos/delphi/www-epicast/site/common/* /var/www/html/common/ +COPY repos/delphi/www-epicast/site/*.php /var/www/html/ + +# point to the local development database (overwrites `common/settings.php`) +COPY repos/delphi/www-epicast/dev/docker/web/epicast/assets/settings.php /var/www/html/common/ + +# ensure files are readable at runtime +RUN chmod o+r -R /var/www/html diff --git a/dev/docker/web/epicast/README.md b/dev/docker/web/epicast/README.md new file mode 100644 index 0000000..b7818ba --- /dev/null +++ b/dev/docker/web/epicast/README.md @@ -0,0 +1,24 @@ +# `delphi_web_epicast` + +This image starts with Delphi's web server and adds the sources necessary for +hosting the Epicast website. It further extends Delphi's web server by: + +- enabling the `mod_rewrite` extension +- enabling the `mod_headers` extension +- creating an empty `htpasswd` file + +This image includes the file +[`settings.php`](assets/settings.php), which points to a local +container running the +[`delphi_database_epicast` image](../../database/epicast/README.md). + +To start a container from this image, run: + +```bash +docker run --rm -p 10080:80 \ + --network delphi-net --name delphi_web_epicast \ + delphi_web_epicast +``` + +You should be able to login and interact with the website (e.g. submitting +predictions) by visiting `http://localhost:10080/` in a web browser. diff --git a/dev/docker/web/epicast/assets/settings.php b/dev/docker/web/epicast/assets/settings.php new file mode 100644 index 0000000..7307cf8 --- /dev/null +++ b/dev/docker/web/epicast/assets/settings.php @@ -0,0 +1,15 @@ + 'Delphi Developer', + 'email' => 'fake_email_address', +); +?> diff --git a/devops/Dockerfile b/devops/Dockerfile new file mode 100644 index 0000000..9d3d2cd --- /dev/null +++ b/devops/Dockerfile @@ -0,0 +1,28 @@ +# start with a standard php7+apache image +# based on https://github.com/cmu-delphi/operations +FROM php:7-apache +LABEL org.opencontainers.image.source = "https://github.com/cmu-delphi/www-epicast" + + +# use PHP's recommended configuration +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +# install and enable PHP's `mysqli` extension +RUN docker-php-ext-install mysqli && \ + docker-php-ext-enable mysqli + +# use delphi's timezome +RUN ln -s -f /usr/share/zoneinfo/America/New_York /etc/localtime && \ + sed -i $PHP_INI_DIR/php.ini -e 's/^;date.timezone =$/date.timezone = "America\/New_York"/' +# enable mod_rewrite, aka RewriteEngine +RUN a2enmod rewrite && a2enmod headers + +ENV EPICAST_DB_HOST 'localhost' +ENV EPICAST_DB_PORT '3306' +ENV EPICAST_DB_USER 'user' +ENV EPICAST_DB_PASSWORD 'pass' +ENV EPICAST_DB_NAME 'epicast2' + +COPY ./site/ /var/www/html/ +# ensure files are readable at runtime +RUN chmod o+r -R /var/www/html diff --git a/site/.htaccess b/site/.htaccess index 61f4952..7d6fdd3 100644 --- a/site/.htaccess +++ b/site/.htaccess @@ -1,5 +1,14 @@ RewriteEngine On +## Restrict access to known users or docker development ## +AuthType Basic +AuthName "Restricted Site: Crowdcast" +AuthBasicProvider file +AuthUserFile /var/www/passwords +Require user cctest +Require ip 172.16.0.0/12 +## Restrict access to known users or docker development ## + ## Compress Files ## diff --git a/site/FAQ.php b/site/FAQ.php index 5892e5d..1a206b4 100644 --- a/site/FAQ.php +++ b/site/FAQ.php @@ -78,6 +78,52 @@ another project (not crowdsourced) we are forecasting future demand on hospitals.

+ +
+ +

+ + 5. “Can you only see your score if you’re one of the top 10? / Can you see more than just last weeks forecast? I’m having trouble figuring out if my prior forecasts were any good. " + +

+

+ Answer: Unfortunately we can only show the top 10 scores.  However, your own score each week is always sent you you in the email you receive the following week. +

+ +
+ +

+ + 6. “I only have an iPhone. I do not have a computer to work on. Should I withdraw from the study or is there a way I can use my iPhone? " + +

+

+ Answer: Unfortunately we do not yet have a way to support smooth participation by phone. +

+ +
+ +

+ + 7. “On forecasting methodologies: Would you like us to forecast the following week, two weeks out, or the rest of the dates on the graph? Do you want us to use a method we can communicate to others? Do you want us to use a statistical method or just look at the data in the context of what you know from public information?" + +

+

+ Answer: Please forecast the rest of the time period shown on the Crowdcast display.  You may use any method you wish, including qualitative reasoning and personal judgement, using whatever data you have and anything else that you believe is relevant.  Your goal is to be as accurate as possible, regardless of the method you use. +

+ +
+ +

+ + 8. “On forecasting assumptions: Now that there is a pandemic, people are being asked to self-isolate if they have symptoms, instead of seeing a doctor, because of shortage of testing. Would you have to take that into consideration when making your forecasts? (i.e. it would be lower then you may predict because of lack of doctor’s visits, and lack of testing). Is it based on what I see around my area? People sheltering or using social distancing base not doing those things?" + +

+

+ Answer: You should take into account all considerations that you believe are relevant to what will eventually be measured and reported in the location for which you are forecasting. +

+ + ', $field); - printf(' ', $field); -} -function sortBool(&$a, &$b) { - global $sortDir, $sortKey; - $value = 0; - if($a[$sortKey] && !$b[$sortKey]) { - $value = 1; - } - if(!$a[$sortKey] && $b[$sortKey]) { - $value = -1; - } - return $value * ($sortDir === 'a' ? 1 : -1); -} -function sortInt(&$a, &$b) { - global $sortDir, $sortKey; - $value = $a[$sortKey] - $b[$sortKey]; - return $value * ($sortDir === 'a' ? 1 : -1); -} -if(isAdmin($output)) { -?> -
- 0) { - $numForecastusers++; - } - } - } - foreach($u['submissions_hosp'] as &$s) { - if($s[0] === $output['epiweek']['round_epiweek']) { - $numForecasts_hosp += $s[1]; - if($s[1] > 0) { - $numForecastusers_hosp++; - } - } - } - foreach($emailTypes as &$type) { - if(getPreference($u, 'email_' . $type, 'int') === 1) { - $u['emails']++; - } - } - foreach($u['submissions'] as &$s) { - $u['total_submissions'] += $s[1]; - } - foreach($u['submissions_hosp'] as &$s) { - $u['total_submissions_hosp'] += $s[1]; - } - } - //PHP sort - if($sortFunc !== null) { - usort($output['userbase'], $sortFunc); - } - ?> -
-
- User Stats -
-
-
-
-
Total Registered Users
-
-
-
-
New Users Last 7 Days
-
-
-
-
Users Active Last 7 Days
-
-
-
-
Users Online Now
-
-
-
- -
-
- Regions and States -
-
-
-
-
Most Recent Report
-
-
-
-
Forecasts Received This Round
-
-
-
-
Users Participated This Round
-
-
-
- - -
-
- Userbase -
- Hopefully some day this will be too long to display on a single page. -
-
-
- - - - - - - - - - - - - - - -
Name EmailFirst Seen Last Seen PreferencesPresence Communication Submissions
', explode(' ', $u['first_seen'])) ?>', explode(' ', $u['last_seen'])) ?>', $k, $u['user_preferences'][$k]); - } else { - printf('%s: %s
', $k, $output['default_preferences'][$k]); - } - } - foreach(array_keys($u['user_preferences']) as $k) { - if(!isset($output['default_preferences'][$k])) { - printf('%s: %s
', $k, $u['user_preferences'][$k]); - } - } - ?>
- Online Now - - Offline - - Missing - - All Emails - 0) { ?> - Some Emails - - Do Not Contact - '); - foreach($u['submissions'] as &$s) { - printf('%s: %d
', formatEpiweek($s[0]), $s[1]); - } - printf('Total: %d
', $u['total_submissions']); - ?>
-
-
-
-
- Control Interface -
- Update user and system settings. -
-
-

The control interface is here.

-
-
- +', $field); + printf(' ', $field); +} +function sortBool(&$a, &$b) { + global $sortDir, $sortKey; + $value = 0; + if($a[$sortKey] && !$b[$sortKey]) { + $value = 1; + } + if(!$a[$sortKey] && $b[$sortKey]) { + $value = -1; + } + return $value * ($sortDir === 'a' ? 1 : -1); +} +function sortInt(&$a, &$b) { + global $sortDir, $sortKey; + $value = $a[$sortKey] - $b[$sortKey]; + return $value * ($sortDir === 'a' ? 1 : -1); +} +if(isAdmin($output)) { +?> +
+ 0) { + $numForecastusers++; + } + } + } + foreach($u['submissions_hosp'] as &$s) { + if($s[0] === $output['epiweek']['round_epiweek']) { + $numForecasts_hosp += $s[1]; + if($s[1] > 0) { + $numForecastusers_hosp++; + } + } + } + foreach($emailTypes as &$type) { + if(getPreference($u, 'email_' . $type, 'int') === 1) { + $u['emails']++; + } + } + foreach($u['submissions'] as &$s) { + $u['total_submissions'] += $s[1]; + } + foreach($u['submissions_hosp'] as &$s) { + $u['total_submissions_hosp'] += $s[1]; + } + } + //PHP sort + if($sortFunc !== null) { + usort($output['userbase'], $sortFunc); + } + ?> +
+
+ User Stats +
+
+
+
+
Total Registered Users
+
+
+
+
New Users Last 7 Days
+
+
+
+
Users Active Last 7 Days
+
+
+
+
Users Online Now
+
+
+
+ +
+
+ Regions and States +
+
+
+
+ +
Most Recent Report
+
+
+
+
Forecasts Received This Round
+
+
+
+
Users Participated This Round
+
+
+
+ + +
+
+ Userbase +
+ Hopefully some day this will be too long to display on a single page. +
+
+
+ + + + + + + + + + + + + + + +
Name EmailFirst Seen Last Seen PreferencesPresence Communication Submissions
', explode(' ', $u['first_seen'])) ?>', explode(' ', $u['last_seen'])) ?>', $k, $u['user_preferences'][$k]); + } else { + printf('%s: %s
', $k, $output['default_preferences'][$k]); + } + } + foreach(array_keys($u['user_preferences']) as $k) { + if(!isset($output['default_preferences'][$k])) { + printf('%s: %s
', $k, $u['user_preferences'][$k]); + } + } + ?>
+ Online Now + + Offline + + Missing + + All Emails + 0) { ?> + Some Emails + + Do Not Contact + '); + foreach($u['submissions'] as &$s) { + printf('%s: %d
', formatEpiweek($s[0]), $s[1]); + } + printf('Total: %d
', $u['total_submissions']); + ?>
+
+
+
+
+ Control Interface +
+ Update user and system settings. +
+
+

The control interface is here.

+
+
+ diff --git a/site/common/database.php b/site/common/database.php index fa054a0..a4a7a81 100644 --- a/site/common/database.php +++ b/site/common/database.php @@ -102,7 +102,7 @@ function getUserByHash($dbh, &$output, $hash) { function getUserByEmail($dbh, &$output, $email) { $result = $dbh->query("SELECT `hash` FROM ec_fluv_users WHERE `email` = '{$email}'"); if($row = $result->fetch_assoc()) { - return getUserByHash($output, $row['hash']); + return getUserByHash($dbh, $output, $row['hash']); } else { setResult($output, 2); return getResult($output); @@ -110,22 +110,21 @@ function getUserByEmail($dbh, &$output, $email) { } function getUserIDByMturkID($dbh, $mturkID) { - $result = $dbh->query("SELECT `id` FROM ec_fluv_users_mturk_2019 WHERE `name` = '{$mturkID}'"); - if($row = $result->fetch_assoc()) { - return $row['id']; - } else { - return -1; - } - + $result = $dbh->query("SELECT `id` FROM ec_fluv_users_mturk_2019 WHERE `name` = '{$mturkID}'"); + if($row = $result->fetch_assoc()) { + return $row['id']; + } else { + return -1; + } } function userAlreadyExist($dbh, $mturkID) { - $result = $dbh->query("SELECT `name` FROM ec_fluv_users_mturk_2019 WHERE `name` = '{$mturkID}'"); - if($row = $result->fetch_assoc()) { - return 1; - } else { - return 0; - } + $result = $dbh->query("SELECT `name` FROM ec_fluv_users_mturk_2019 WHERE `name` = '{$mturkID}'"); + if($row = $result->fetch_assoc()) { + return 1; + } else { + return 0; + } } @@ -191,9 +190,7 @@ function getUserStats_hosp($dbh, &$output, $userID, $epiweek) { $output['result'] will contain the following values: 1 - Success 2 - Failure (table ec_fluv_round) - 3 - Failure (table epidata.fluview) - $output['epiweek']['round_epiweek'] - This round's identifier (can be ahead of data_epiweek) - $output['epiweek']['data_epiweek'] - The most recently published issue (from epidata.fluview) + $output['epiweek']['round_epiweek'] - This round's identifier (can be ahead of latest issue) $output['epiweek']['deadline'] - Deadline timestamp as a string (YYYY-MM-DD HH:MM:SS) $output['epiweek']['deadline_timestamp'] - Unix timestamp of deadline $output['epiweek']['remaining'] - An array containing days/hours/minutes/seconds remaining @@ -246,14 +243,6 @@ function getEpiweekInfo($dbh, &$output) { setResult($output, 2); return getResult($output); } - $result = $dbh->query('SELECT max(`issue`) AS `data_epiweek` FROM epidata.`fluview`'); - if($row = $result->fetch_assoc()) { - $output['epiweek']['data_epiweek'] = intval($row['data_epiweek']); - setResult($output, 1); - } else { - setResult($output, 3); - return getResult($output); - } return getResult($output); } @@ -357,7 +346,7 @@ function getRegions($dbh, &$output, $userID) { $regions[$region['id']] = $region; } $output['regions'] = &$regions; - + setResult($output, count($regions) == NUM_REGIONS ? 1 : 2); return getResult($output); } @@ -443,7 +432,7 @@ function getRegionsExtended($dbh, &$output, $userID) { if(getRegions($dbh, $output, $userID) !== 1) { return getResult($output); } - + //History and forecast for every region foreach($output['regions'] as &$r) { if(getPreference($output, 'advanced_prior', 'int') === 1) { @@ -451,16 +440,16 @@ function getRegionsExtended($dbh, &$output, $userID) { } else { $firstWeek = 200430; } - + if(getHistory($dbh, $output, $r['id'], $firstWeek) !== 1) { return getResult($output); } - + $r['history'] = $output['history']; if(loadForecast($dbh, $output, $userID, $r['id']) !== 1) { return getResult($output); } - + $r['forecast'] = $output['forecast']; } setResult($output, 1); @@ -596,7 +585,7 @@ function getAgeGroupsExtended($dbh, &$output, $userID) { 2 - Failure $output['history'] - Arrays of epiweeks and historical incidence (wILI) for the region */ -function getHistory($dbh, &$output, $regionID, $firstWeek) { +function getHistory($dbh, &$output, $regionID, $firstWeek) { $result = $dbh->query("SELECT fv.`epiweek`, fv.`wili` FROM epidata.`fluview` AS fv JOIN ( SELECT `epiweek`, max(`issue`) AS `latest` FROM epidata.`fluview` AS fv JOIN ec_fluv_regions AS reg ON reg.`fluview_name` = fv.`region` WHERE reg.`id` = {$regionID} AND fv.`epiweek` >= {$firstWeek} GROUP BY fv.`epiweek` ) AS issues ON fv.`epiweek` = issues.`epiweek` AND fv.`issue` = issues.`latest` JOIN ec_fluv_regions AS reg ON reg.`fluview_name` = fv.`region` WHERE reg.`id` = {$regionID} AND fv.`epiweek` >= {$firstWeek} ORDER BY fv.`epiweek` ASC"); $date = array(); $wili = array(); @@ -739,7 +728,7 @@ function listAgeGroups($dbh) { 2 - Failure */ function saveForecast($dbh, &$output, $userID, $regionID, $forecast, $commit) { - + $temp = array(); if(getEpiweekInfo($dbh, $temp) !== 1) { return getResult($temp); @@ -752,18 +741,18 @@ function saveForecast($dbh, &$output, $userID, $regionID, $forecast, $commit) { if($commit) { $dbh->query("INSERT INTO ec_fluv_submissions (`user_id`, `region_id`, `epiweek_now`, `date`) VALUES ({$userID}, {$regionID}, {$temp['epiweek']['round_epiweek']}, now())"); } - + $debug = false; if ($debug) { echo "-------saveForecast----\n"; echo $commit; } - - + + setResult($output, 1); - + getRegions($dbh, $output, $userID); - + return getResult($output); } @@ -840,7 +829,7 @@ function loadForecast($dbh, &$output, $userID, $regionID, $submitted=false) { echo "-----inside loadForecast------\n"; echo "user ID, region ID, submitted: "; echo $userID; - echo $regionID; + echo $regionID; echo $submitted; } @@ -849,7 +838,7 @@ function loadForecast($dbh, &$output, $userID, $regionID, $submitted=false) { if(getEpiweekInfo($dbh, $temp) !== 1) { return getResult($temp); } - + $result = $dbh->query("SELECT coalesce(max(`epiweek_now`), 0) `epiweek` FROM ec_fluv_submissions WHERE `user_id` = {$userID} AND `region_id` = {$regionID} AND `epiweek_now` < {$temp['epiweek']['round_epiweek']}"); } else { $result = $dbh->query("SELECT coalesce(max(`epiweek_now`), 0) `epiweek` FROM ec_fluv_forecast WHERE `user_id` = {$userID} AND `region_id` = {$regionID}"); @@ -863,17 +852,17 @@ function loadForecast($dbh, &$output, $userID, $regionID, $submitted=false) { $date = array(); $wili = array(); $result = $dbh->query("SELECT `epiweek_now`, `epiweek`, `wili` FROM ec_fluv_forecast f WHERE `user_id` = {$userID} AND `region_id` = {$regionID} AND `epiweek_now` = {$epiweek} ORDER BY f.`epiweek` ASC"); - + if ($debug and ($regionID == 1 or $regionID == 8)) { echo "epiweek: "; echo $epiweek; echo "\n"; } - + while($row = $result->fetch_assoc()) { array_push($date, intval($row['epiweek'])); array_push($wili, floatval($row['wili'])); - + if ($debug and ($regionID == 1 or $regionID == 8)) { echo intval($row['epiweek']); echo ", "; @@ -996,20 +985,20 @@ function loadForecast_hosp($dbh, &$output, $userID, $group_id, $submitted=false) */ function registerUser($dbh, &$output, $name, $email, $adminEmail) { //Find, or create, the user - if(getUserByEmail($output, $email) === 1) { + if(getUserByEmail($dbh, $output, $email) === 1) { $output['user_new'] = false; } else { $dbh->query("INSERT INTO ec_fluv_users (`hash`, `name`, `email`, `first_seen`, `last_seen`) VALUES (md5(rand()), '{$name}', '{$email}', now(), now())"); $output['user_new'] = true; - if(getUserByEmail($output, $email) !== 1) { + if(getUserByEmail($dbh, $output, $email) !== 1) { return getResult($output); } } //Send an email to the user $hash = strtoupper(substr($output['user_hash'], 0, 8)); - $subject = mysqli_real_escape_string('Welcome to Crowdcast!'); - $body = mysqli_real_escape_string(sprintf("Hi %s,\r\n\r\nWelcome to Crowdcast (formerly known as Epicast)! Here's your User ID: %s\r\nYou can login and begin forecasting here: https://delphi.cmu.edu/epicast/launch.php?user=%s\r\n\r\nThank you,\r\nThe Delphi Team\r\n\r\n[This is an automated message. Please direct all replies to: %s. Unsubscribe: https://delphi.cmu.edu/epicast/preferences.php?user=%s]", $name, $hash, $hash, $adminEmail, $hash)); - $dbh->query("INSERT INTO automation.email_queue (`from`, `to`, `subject`, `body`) VALUES ('delphi@epicast.net', '{$email}', '{$subject}', '{$body}')"); + $subject = mysqli_real_escape_string($dbh, 'Welcome to Crowdcast!'); + $body = mysqli_real_escape_string($dbh, sprintf("Hi %s,\r\n\r\nWelcome to Crowdcast (formerly known as Epicast)! Here's your User ID: %s\r\nYou can login and begin forecasting here: https://delphi.cmu.edu/epicast/launch.php?user=%s\r\n\r\nThank you,\r\nThe Delphi Team\r\n\r\n[This is an automated message. Please direct all replies to: %s. Unsubscribe: https://delphi.cmu.edu/epicast/preferences.php?user=%s]", $name, $hash, $hash, $adminEmail, $hash)); + $dbh->query("INSERT INTO automation.email_queue (`from`, `to`, `subject`, `body`, `priority`, `timestamp`) VALUES ('delphi@epicast.net', '{$email}', '{$subject}', '{$body}', 0.9, UNIX_TIMESTAMP(NOW()))"); $dbh->query("CALL automation.RunStep(2)"); setResult($output, 1); return getResult($output); @@ -1017,12 +1006,12 @@ function registerUser($dbh, &$output, $name, $email, $adminEmail) { function registerUser_mturk($dbh, $mturkID) { //Find, or create, the user - if (userAlreadyExist($mturkID) === 1) { + if (userAlreadyExist($dbh, $mturkID) === 1) { return; } else { $email = md5(rand()); $hash = md5(rand()); - $escapedInput = mysqli_real_escape_string($mturkID); + $escapedInput = mysqli_real_escape_string($dbh, $mturkID); $query = "INSERT INTO ec_fluv_users_mturk (`hash`, `name`, `email`, `first_seen`, `last_seen`) VALUES ('{$hash}', '{$escapedInput}', '{$email}', now(), now())"; $result = $dbh->query($query); @@ -1032,16 +1021,16 @@ function registerUser_mturk($dbh, $mturkID) { function registerUser_mturk_2019($dbh, $mturkID, $taskID) { //Find, or create, the user - if (userAlreadyExist($mturkID) === 1) { + if (userAlreadyExist($dbh, $mturkID) === 1) { return; } else { $email = md5(rand()); $hash = md5(rand()); - $escapedInput = mysqli_real_escape_string($mturkID); + $escapedInput = mysqli_real_escape_string($dbh, $mturkID); $query = "INSERT INTO ec_fluv_users_mturk_2019 (`hash`, `name`, `email`, `first_seen`, `last_seen`, `taskID`) VALUES ('{$hash}', '{$escapedInput}', '{$email}', now(), now(), {$taskID})"; $dbh->query($query); - + $temp = array(); if(getEpiweekInfo_mturk($temp) !== 1) { return getResult($temp); @@ -1076,7 +1065,7 @@ function getAvailableTaskSets($dbh) { function getNextLocation($dbh, $mturkID, $regionID) { - if ($regionID === -1 && !userAlreadyExist($mturkID)) { + if ($regionID === -1 && !userAlreadyExist($dbh, $mturkID)) { // return the state with the smallest region ID in this task group $availableTasks = getAvailableTaskSets(); $task = $availableTasks[array_rand($availableTasks)]; @@ -1090,7 +1079,7 @@ function getNextLocation($dbh, $mturkID, $regionID) { } else { // return an array of unfinished states - $escapedInput = mysqli_real_escape_string($mturkID); + $escapedInput = mysqli_real_escape_string($dbh, $mturkID); $query = "select taskID from ec_fluv_users_mturk_2019 where name = '{$escapedInput}'"; $result = $dbh->query($query); // $taskID = intval($result->fetch_assoc()); @@ -1577,7 +1566,7 @@ function getECDCILI($dbh, &$output, $regionID, $firstWeek) { WHERE ed.`region` = \"{$country}\" AND ed.`epiweek` >= {$firstWeek} ORDER BY ed.`epiweek` ASC"; $result = $dbh->query($query); - + $date = array(); $wili = array(); while($row=$result->fetch_assoc()) { diff --git a/site/common/footer.php b/site/common/footer.php index a34496d..e17224e 100644 --- a/site/common/footer.php +++ b/site/common/footer.php @@ -1,7 +1,35 @@ - - + - - - + + + + diff --git a/site/common/header.php b/site/common/header.php index 27d1d96..e91b33a 100644 --- a/site/common/header.php +++ b/site/common/header.php @@ -20,6 +20,7 @@ +