├── public ├── favicon.ico ├── robots.txt ├── vendor │ └── binarytorch │ │ └── larecipe │ │ └── assets │ │ └── fonts │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ ├── fa-solid-900.woff2 │ │ ├── nucleo-icons.eot │ │ ├── nucleo-icons.ttf │ │ ├── nucleo-icons.woff │ │ ├── nucleo-icons.woff2 │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.woff │ │ └── fa-regular-400.woff2 ├── .htaccess ├── web.config └── index.php ├── resources ├── sass │ └── app.scss ├── js │ ├── app.js │ └── bootstrap.js ├── views │ ├── vendor │ │ └── larecipe │ │ │ └── partials │ │ │ ├── sidebar.blade.php │ │ │ ├── nav.blade.php │ │ │ └── logo.blade.php │ └── welcome.blade.php ├── docs │ └── 1.0 │ │ ├── index.md │ │ ├── regions.md │ │ ├── overview.md │ │ ├── fatalities.md │ │ ├── cases.md │ │ ├── vaccine-age-groups.md │ │ ├── provinces.md │ │ └── subregions.md └── lang │ └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── database ├── .gitignore ├── seeders │ ├── csv │ │ ├── postal_districts.csv │ │ ├── provinces.csv │ │ └── reports.csv │ ├── DatabaseSeeder.php │ ├── RolesAndPermissionsSeeder.php │ ├── CaseSeeder.php │ ├── ReportSeeder.php │ ├── FatalitySeeder.php │ ├── ProvinceSeeder.php │ ├── SubRegionsSeeder.php │ ├── HealthRegionsSeeder.php │ ├── PostalDistrictSeeder.php │ └── NoteSeeder.php ├── migrations │ ├── 2020_04_08_021044_create_fatalities_table.php │ ├── 2020_04_19_062458_add_notes_to_reports_table.php │ ├── 2020_09_06_165608_add_hr_uid_to_cases_table.php │ ├── 2020_09_06_165616_add_hr_uid_to_fatalities_table.php │ ├── 2021_01_11_222818_add_vaccinated_to_reports_table.php │ ├── 2021_02_07_143924_add_vaccinated_to_hr_reports.php │ ├── 2021_10_19_220929_add_boosters_1_column_to_reports.php │ ├── 2022_01_15_233345_create_postal_districts_table.php │ ├── 2022_02_01_000356_add_boosters_2_column_to_reports.php │ ├── 2020_04_26_182059_create_options_table.php │ ├── 2020_07_23_202600_add_vaccinations_to_reports_table.php │ ├── 2020_05_05_231134_add_geographic_column_to_provinces.php │ ├── 2021_10_19_221217_add_boosters_1_column_to_hr_reports.php │ ├── 2022_02_01_000425_add_boosters_2_column_to_hr_reports.php │ ├── 2020_12_28_161242_add_vaccines_distributed_to_reports_table.php │ ├── 2021_05_10_234507_create_vaccine_groups_table.php │ ├── 2021_01_10_005345_create_process_queue_table.php │ ├── 2020_05_20_223602_add_data_status_column_to_provinces.php │ ├── 2020_04_08_021051_create_locations_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2021_08_31_091812_add_boosters1_to_vaccine_reports.php │ ├── 2021_05_01_121104_add_johnson_to_vaccine_distribution.php │ ├── 2021_11_11_112220_create_sub_regions_table.php │ ├── 2022_01_15_140953_create_rt_reports_table.php │ ├── 2021_01_11_222831_add_vaccinated_to_processed_reports_table.php │ ├── 2021_02_07_144009_add_vaccinated_to_processed_hr_reports.php │ ├── 2021_10_19_221127_add_boosters_1_column_to_processed_reports.php │ ├── 2022_02_01_000411_add_boosters_2_column_to_processed_reports.php │ ├── 2021_10_19_221231_add_boosters_1_column_to_processed_hr_reports.php │ ├── 2022_02_01_000440_add_boosters_2_column_to_processed_hr_reports.php │ ├── 2020_09_06_100527_add_vaccinations_to_processed_reports_table.php │ ├── 2020_04_08_021031_create_cases_table.php │ ├── 2020_11_15_213140_create_provinces_users_table.php │ ├── 2020_12_28_162331_add_vaccines_distributed_to_processed_reports_table.php │ ├── 2020_06_08_004516_change_cases_date_to_nullable.php │ ├── 2021_12_03_220411_add_pfizer_biontech_paediatric_to_vaccine_distribution_table.php │ ├── 2020_09_06_142336_create_notes_table.php │ ├── 2020_08_07_203709_create_health_regions_table.php │ ├── 2020_04_12_082548_create_reports_table.php │ ├── 2021_05_29_194015_create_vaccine_reports_table.php │ ├── 2020_07_23_201616_create_hr_reports_table.php │ ├── 2021_04_27_214341_create_vaccine_distribution_table.php │ ├── 2020_04_12_050802_create_provinces_table.php │ ├── 2021_06_13_175534_add_out_of_province_to_vaccine_reports.php │ ├── 2021_11_07_111954_create_sr_vaccine_reports_table.php │ ├── 2020_04_19_003533_create_processed_reports_table.php │ ├── 2021_09_06_194048_drop_adult_and_out_of_province_from_vaccine_reports.php │ └── 2020_09_06_092920_create_processed_hr_reports_table.php └── factories │ └── UserFactory.php ├── .gitattributes ├── app ├── Location.php ├── ProcessedReport.php ├── Fatality.php ├── PostalDistrict.php ├── VaccineAgeGroup.php ├── VaccineDistribution.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── CheckForMaintenanceMode.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ ├── Authenticate.php │ │ ├── RedirectIfAuthenticated.php │ │ └── Cors.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── HealthRegionController.php │ │ ├── NoteController.php │ │ ├── SubRegionController.php │ │ ├── ProvinceController.php │ │ ├── FatalityController.php │ │ ├── PartnerReportController.php │ │ ├── CaseController.php │ │ ├── AuthController.php │ │ └── UserController.php │ └── Kernel.php ├── Cases.php ├── HealthRegion.php ├── RapidTestReport.php ├── HrReport.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── EventServiceProvider.php │ ├── AuthServiceProvider.php │ └── RouteServiceProvider.php ├── Province.php ├── Note.php ├── Option.php ├── Report.php ├── SubRegion.php ├── VaccineReport.php ├── SrVaccineReport.php ├── User.php ├── Utilities │ └── ProxyRequest.php ├── Exceptions │ └── Handler.php ├── Console │ ├── Kernel.php │ └── Commands │ │ └── TransferCaseFatality.php ├── RapidTest.php └── ModularReport.php ├── tests ├── TestCase.php ├── Unit │ ├── ExampleTest.php │ └── RapidTestTest.php ├── CreatesApplication.php └── Feature │ ├── PartnerTest.php │ ├── VaccineAgeGroupsTest.php │ ├── RapidTestTest.php │ ├── LocationTest.php │ ├── RapidTestReportTest.php │ └── SrReportTest.php ├── .styleci.yml ├── .editorconfig ├── .gitignore ├── routes ├── web.php ├── channels.php └── console.php ├── webpack.mix.js ├── server.php ├── README-report.md ├── config ├── cors.php ├── view.php ├── services.php ├── hashing.php ├── broadcasting.php ├── filesystems.php ├── queue.php └── logging.php ├── package.json ├── phpunit.xml ├── LICENSE ├── .env.example ├── artisan └── composer.json /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap'); 2 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite-journal 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /resources/views/vendor/larecipe/partials/sidebar.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /app/Location.php: -------------------------------------------------------------------------------- 1 | pluck('province', 'letter'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/VaccineAgeGroup.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/VaccineDistribution.php: -------------------------------------------------------------------------------- 1 | hasMany('App\HrReport', 'hr_uid', 'hr_uid'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckForMaintenanceMode.php: -------------------------------------------------------------------------------- 1 | belongsTo('App\HealthRegion', 'hr_uid', 'hr_uid'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(UsersTableSeeder::class); 17 | 18 | // CSV seeders 19 | $this->call(ProvinceSeeder::class); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | area > 0 ) 22 | return $this->population / $this->area; 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/js/app.js', 'public/js') 15 | .sass('resources/sass/app.scss', 'public/css'); 16 | -------------------------------------------------------------------------------- /app/Note.php: -------------------------------------------------------------------------------- 1 | ='; 22 | return $query->where( 'expiry_date', $operand, date('Y-m-d') ) 23 | ->orWhereNull( 'expiry_date' ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/docs/1.0/index.md: -------------------------------------------------------------------------------- 1 | - ## API 2 | - [Overview](/{{route}}/{{version}}/overview) 3 | - [Summary](/{{route}}/{{version}}/summary) 4 | - [Reports](/{{route}}/{{version}}/reports) 5 | - [Vaccination Data](/{{route}}/{{version}}/vaccinations) 6 | - [Vaccine Age Groups](/{{route}}/{{version}}/vaccine-age-groups) 7 | - [Provinces](/{{route}}/{{version}}/provinces) 8 | - [Health Regions](/{{route}}/{{version}}/regions) 9 | - [Subregions](/{{route}}/{{version}}/subregions) 10 | - [*Cases*](/{{route}}/{{version}}/cases) 11 | - [*Fatalities*](/{{route}}/{{version}}/fatalities) 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->describe('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /app/Option.php: -------------------------------------------------------------------------------- 1 | first() ) { 18 | return $option->value; 19 | } 20 | return ''; 21 | } 22 | 23 | static function set( $attribute, $value ) { 24 | return self::updateOrInsert( 25 | ['attribute' => $attribute], 26 | ['value' => $value] 27 | ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 22 | return redirect(RouteServiceProvider::HOME); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/seeders/RolesAndPermissionsSeeder.php: -------------------------------------------------------------------------------- 1 | forgetCachedPermissions(); 22 | 23 | // create roles 24 | $role = Role::create(['name' => 'admin']); 25 | $role = Role::create(['name' => 'editor']); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Report.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->text('province'); 19 | $table->date('date'); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('fatalities'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_04_19_062458_add_notes_to_reports_table.php: -------------------------------------------------------------------------------- 1 | string('notes')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('notes'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_09_06_165608_add_hr_uid_to_cases_table.php: -------------------------------------------------------------------------------- 1 | foreignId('hr_uid')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('cases', function (Blueprint $table) { 29 | $table->dropColumn('hr_uid'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/seeders/CaseSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 21 | 22 | // use csv to seed database 23 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 24 | if( file_exists( $seed_csv ) ) { 25 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 26 | $seeder->table = $table; 27 | $seeder->filename = $seed_csv; 28 | $seeder->run(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /database/seeders/ReportSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 21 | 22 | // use csv to seed database 23 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 24 | if( file_exists( $seed_csv ) ) { 25 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 26 | $seeder->table = $table; 27 | $seeder->filename = $seed_csv; 28 | $seeder->run(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2020_09_06_165616_add_hr_uid_to_fatalities_table.php: -------------------------------------------------------------------------------- 1 | foreignId('hr_uid')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('fatalities', function (Blueprint $table) { 29 | $table->dropColumn('hr_uid'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_01_11_222818_add_vaccinated_to_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('vaccinated')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('vaccinated'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_02_07_143924_add_vaccinated_to_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('vaccinated')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('hr_reports', function (Blueprint $table) { 29 | $table->dropColumn('vaccinated'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_10_19_220929_add_boosters_1_column_to_reports.php: -------------------------------------------------------------------------------- 1 | integer('boosters_1')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('boosters_1'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_01_15_233345_create_postal_districts_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('letter', 1)->unique(); 19 | $table->string('province', 8); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('postal_districts'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_02_01_000356_add_boosters_2_column_to_reports.php: -------------------------------------------------------------------------------- 1 | integer('boosters_2')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('boosters_2'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/seeders/FatalitySeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 21 | 22 | // use csv to seed database 23 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 24 | if( file_exists( $seed_csv ) ) { 25 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 26 | $seeder->table = $table; 27 | $seeder->filename = $seed_csv; 28 | $seeder->run(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2020_04_26_182059_create_options_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('attribute')->unique(); 19 | $table->text('value')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('options'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2020_07_23_202600_add_vaccinations_to_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('vaccinations')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('vaccinations'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2020_05_05_231134_add_geographic_column_to_provinces.php: -------------------------------------------------------------------------------- 1 | boolean('geographic')->default(true); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('provinces', function (Blueprint $table) { 29 | $table->dropColumn('geographic'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2021_10_19_221217_add_boosters_1_column_to_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('boosters_1')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('hr_reports', function (Blueprint $table) { 29 | $table->dropColumn('boosters_1'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_02_01_000425_add_boosters_2_column_to_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('boosters_2')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('hr_reports', function (Blueprint $table) { 29 | $table->dropColumn('boosters_2'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/seeders/ProvinceSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 23 | 24 | // use csv to seed database 25 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 26 | if( file_exists( $seed_csv ) ) { 27 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 28 | $seeder->table = $table; 29 | $seeder->filename = $seed_csv; 30 | $seeder->run(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | parent::boot(); 31 | 32 | // 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/seeders/SubRegionsSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 23 | 24 | // use csv to seed database 25 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 26 | if( file_exists( $seed_csv ) ) { 27 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 28 | $seeder->table = $table; 29 | $seeder->filename = $seed_csv; 30 | $seeder->run(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/seeders/HealthRegionsSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 23 | 24 | // use csv to seed database 25 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 26 | if( file_exists( $seed_csv ) ) { 27 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 28 | $seeder->table = $table; 29 | $seeder->filename = $seed_csv; 30 | $seeder->run(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2020_12_28_161242_add_vaccines_distributed_to_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('vaccines_distributed')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('reports', function (Blueprint $table) { 29 | $table->dropColumn('vaccines_distributed'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/seeders/PostalDistrictSeeder.php: -------------------------------------------------------------------------------- 1 | truncate(); 23 | 24 | // use csv to seed database 25 | $seed_csv = base_path()."/database/seeders/csv/{$table}.csv"; 26 | if( file_exists( $seed_csv ) ) { 27 | $seeder = new \Flynsarmy\CsvSeeder\CsvSeeder; 28 | $seeder->table = $table; 29 | $seeder->filename = $seed_csv; 30 | $seeder->run(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2021_05_10_234507_create_vaccine_groups_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->date('date'); 19 | // province code 20 | $table->string('province', 8); 21 | $table->json('data'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('vaccine_groups'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Controllers/HealthRegionController.php: -------------------------------------------------------------------------------- 1 | where('hr_uid', $hr_uid) 18 | ->get(); 19 | 20 | return [ 21 | 'data' => $reports, 22 | ]; 23 | } 24 | 25 | public function regions( $hr_uid = null ) { 26 | $regions = []; 27 | if( $hr_uid ) { 28 | $regions = HealthRegion::find( $hr_uid ); 29 | } else { 30 | $regions = HealthRegion::all(); 31 | } 32 | return [ 33 | 'data' => $regions, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2021_01_10_005345_create_process_queue_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('province', 8); 19 | $table->date('date'); 20 | $table->boolean('processed')->default(false); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('process_queue'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_05_20_223602_add_data_status_column_to_provinces.php: -------------------------------------------------------------------------------- 1 | string('data_status', 32); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('provinces', function (Blueprint $table) { 30 | $table->dropColumn('data_status'); 31 | $table->dropTimestamps(); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README-report.md: -------------------------------------------------------------------------------- 1 | # V2 Report System 2 | 3 | Initial work to make reporting tables modular and not have to repeat processing scripts. 4 | 5 | ## Quick Setup 6 | 7 | ### Migration 8 | ``` 9 | php make:migration create_{new_report_name}_table 10 | ``` 11 | #### Required columns 12 | ``` 13 | $table->id(); 14 | $table->date('date'); 15 | $table->string('province', 8); 16 | ``` 17 | Columns that will be processed should be prefixed with `changed_` or `total_` 18 | 19 | ### Model 20 | ``` 21 | php make:model {NewReportName} 22 | ``` 23 | *or make a copy of VaccineReport.php as you will need to modify the attrs() and reportAttrs() accordingly. 24 | 25 | ### Console 26 | Add new entry to `$supported_tables` in `app/Console/Command/FillReports.php` 27 | 28 | ### Other 29 | Add new entry for `availableReports` in `app/Common.php` 30 | 31 | Make any adjustments to `getReports` and `saveReports` in `app/Http/Controllers/ManageController.php` for C19T-manage support. 32 | -------------------------------------------------------------------------------- /database/migrations/2020_04_08_021051_create_locations_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->text('place_id'); 19 | $table->text('display_name'); 20 | $table->text('full_name'); 21 | $table->text('coordinates'); 22 | $table->text('original_text'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('locations'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/SubRegion.php: -------------------------------------------------------------------------------- 1 | hasMany('App\SrVaccineReport', 'code', 'code'); 19 | } 20 | 21 | public function parent() 22 | { 23 | return $this->belongsTo('App\Province', 'province', 'code'); 24 | } 25 | 26 | public static function getCodes($province = null) 27 | { 28 | $sub_regions = null; 29 | if( $province ) { 30 | $sub_regions = self::where('province', $province); 31 | } else { 32 | $sub_regions = self::all(); 33 | } 34 | return $sub_regions->pluck('code')->toArray(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/NoteController.php: -------------------------------------------------------------------------------- 1 | expired ) $get_expired = true; 21 | 22 | return Note::expired( $get_expired ) 23 | ->when($tag, function( $query, $tag ) { 24 | return $query->where('tag', $tag); 25 | }) 26 | ->orderByRaw('ISNULL(priority), priority ASC, expiry_date ASC') 27 | ->get(); 28 | } 29 | 30 | /** 31 | * retrieve a specific note 32 | */ 33 | public function get( Request $request, $note_id ) { 34 | return Note::find( $note_id ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | 3 | /** 4 | * We'll load the axios HTTP library which allows us to easily issue requests 5 | * to our Laravel back-end. This library automatically handles sending the 6 | * CSRF token as a header based on the value of the "XSRF" token cookie. 7 | */ 8 | 9 | window.axios = require('axios'); 10 | 11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 12 | 13 | /** 14 | * Echo exposes an expressive API for subscribing to channels and listening 15 | * for events that are broadcast by Laravel. Echo and event broadcasting 16 | * allows your team to easily build robust real-time web applications. 17 | */ 18 | 19 | // import Echo from 'laravel-echo'; 20 | 21 | // window.Pusher = require('pusher-js'); 22 | 23 | // window.Echo = new Echo({ 24 | // broadcaster: 'pusher', 25 | // key: process.env.MIX_PUSHER_APP_KEY, 26 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 27 | // forceTLS: true 28 | // }); 29 | -------------------------------------------------------------------------------- /app/VaccineReport.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('users'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 21 | return [ 22 | 'name' => $faker->name, 23 | 'email' => $faker->unique()->safeEmail, 24 | 'email_verified_at' => now(), 25 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 26 | 'remember_token' => Str::random(10), 27 | ]; 28 | }); 29 | -------------------------------------------------------------------------------- /database/migrations/2021_08_31_091812_add_boosters1_to_vaccine_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_boosters_1')->nullable(); 18 | $table->integer('total_boosters_1')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('vaccine_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_boosters_1'); 31 | $table->dropColumn('total_boosters_1'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2021_05_01_121104_add_johnson_to_vaccine_distribution.php: -------------------------------------------------------------------------------- 1 | integer('johnson')->nullable(); 18 | $table->integer('johnson_administered')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('vaccine_distribution', function (Blueprint $table) { 30 | $table->dropColumn('johnson'); 31 | $table->dropColumn('johnson_administered'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2021_11_11_112220_create_sub_regions_table.php: -------------------------------------------------------------------------------- 1 | string('code', 8); 18 | $table->string('province', 2); 19 | $table->string('zone'); 20 | $table->string('region'); 21 | $table->integer('population')->nullable(); 22 | $table->timestamps(); 23 | 24 | $table->primary('code'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('sub_regions'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2022_01_15_140953_create_rt_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('province'); 19 | $table->date('date'); 20 | // reporting columns 21 | $table->integer('positive')->nullable(); 22 | $table->integer('negative')->nullable(); 23 | $table->integer('invalid')->nullable(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('rt_reports'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/Cors.php: -------------------------------------------------------------------------------- 1 | headers->get('origin'); 20 | 21 | if (in_array($request_origin, $allowed_origins)) { 22 | return $next($request) 23 | ->header('Access-Control-Allow-Origin', $request_origin) 24 | ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') 25 | ->header('Access-Control-Allow-Credentials', 'true') 26 | ->header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 27 | } 28 | 29 | return $next($request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2021_01_11_222831_add_vaccinated_to_processed_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('change_vaccinated')->nullable(); 18 | $table->integer('total_vaccinated')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_vaccinated'); 31 | $table->dropColumn('total_vaccinated'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2021_02_07_144009_add_vaccinated_to_processed_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_vaccinated')->nullable(); 18 | $table->integer('total_vaccinated')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_hr_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_vaccinated'); 31 | $table->dropColumn('total_vaccinated'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2021_10_19_221127_add_boosters_1_column_to_processed_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_boosters_1')->nullable(); 18 | $table->integer('total_boosters_1')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_boosters_1'); 31 | $table->dropColumn('total_boosters_1'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2022_02_01_000411_add_boosters_2_column_to_processed_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_boosters_2')->nullable(); 18 | $table->integer('total_boosters_2')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_boosters_2'); 31 | $table->dropColumn('total_boosters_2'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2021_10_19_221231_add_boosters_1_column_to_processed_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_boosters_1')->nullable(); 18 | $table->integer('total_boosters_1')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_hr_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_boosters_1'); 31 | $table->dropColumn('total_boosters_1'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2022_02_01_000440_add_boosters_2_column_to_processed_hr_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_boosters_2')->nullable(); 18 | $table->integer('total_boosters_2')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_hr_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_boosters_2'); 31 | $table->dropColumn('total_boosters_2'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_09_06_100527_add_vaccinations_to_processed_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('change_vaccinations')->nullable(); 18 | $table->integer('total_vaccinations')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_vaccinations'); 31 | $table->dropColumn('total_vaccinations'); 32 | // 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/SrVaccineReport.php: -------------------------------------------------------------------------------- 1 | belongsTo(SubRegion::class, 'code', 'code'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2020_04_08_021031_create_cases_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->text('province'); 19 | $table->string('city', 100); 20 | $table->string('age', 50); 21 | $table->text('travel_history'); 22 | $table->text('confirmed_presumptive'); 23 | $table->text('source'); 24 | $table->datetime('date', 0); 25 | // $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('cases'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 18 | ]; 19 | 20 | /** 21 | * Register any authentication / authorization services. 22 | * 23 | * @return void 24 | */ 25 | public function boot() 26 | { 27 | $this->registerPolicies(); 28 | 29 | Passport::routes(function ($router) { 30 | $router->forAccessTokens(); 31 | $router->forPersonalAccessTokens(); 32 | $router->forTransientTokens(); 33 | }); 34 | Passport::tokensExpireIn(now()->addMinutes(60)); 35 | Passport::refreshTokensExpireIn(now()->addDays(30)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_11_15_213140_create_provinces_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('user_id') 19 | ->references('id') 20 | ->on('users') 21 | ->onDelete('cascade'); 22 | $table->foreignId('province_id') 23 | ->references('id') 24 | ->on('provinces') 25 | ->onDelete('cascade'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('provinces_users'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2020_12_28_162331_add_vaccines_distributed_to_processed_reports_table.php: -------------------------------------------------------------------------------- 1 | integer('change_vaccines_distributed')->nullable(); 18 | $table->integer('total_vaccines_distributed')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('processed_reports', function (Blueprint $table) { 30 | $table->dropColumn('change_vaccines_distributed'); 31 | $table->dropColumn('total_vaccines_distributed'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": ">=0.21.1", 14 | "cross-env": "^7.0", 15 | "laravel-mix": "^5.0.1", 16 | "lodash": "^4.17.13", 17 | "resolve-url-loader": "^3.1.0", 18 | "sass": "^1.15.2", 19 | "sass-loader": "^8.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/migrations/2020_06_08_004516_change_cases_date_to_nullable.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./tests/Unit 9 | 10 | 11 | ./tests/Feature 12 | 13 | 14 | 15 | 16 | ./app 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Thong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/migrations/2021_12_03_220411_add_pfizer_biontech_paediatric_to_vaccine_distribution_table.php: -------------------------------------------------------------------------------- 1 | integer('pfizer_biontech_paediatric')->nullable(); 18 | $table->integer('pfizer_biontech_paediatric_administered')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('vaccine_distribution', function (Blueprint $table) { 30 | $table->dropColumn('pfizer_biontech_paediatric'); 31 | $table->dropColumn('pfizer_biontech_paediatric_administered'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2020_09_06_142336_create_notes_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->text('title')->nullable(); 19 | $table->text('description')->nullable(); 20 | // simple tag system 21 | $table->string('tag', 32)->nullable(); 22 | $table->enum('type', ['success', 'warning', 'danger', 'info']); 23 | $table->date('expiry_date')->nullable(); 24 | $table->unsignedTinyInteger('priority')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('notes'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2020_08_07_203709_create_health_regions_table.php: -------------------------------------------------------------------------------- 1 | integer('hr_uid')->unsigned(); 22 | $table->string('province', 2); 23 | $table->string('engname'); 24 | $table->string('frename'); 25 | $table->timestamps(); 26 | 27 | $table->primary('hr_uid'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('health_regions'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2020_04_12_082548_create_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('province'); 19 | $table->date('date'); 20 | // reporting columns 21 | $table->integer('tests')->nullable(); 22 | $table->integer('cases')->nullable(); 23 | $table->integer('hospitalizations')->nullable(); 24 | $table->integer('criticals')->nullable(); 25 | $table->integer('recoveries')->nullable(); 26 | $table->integer('fatalities')->nullable(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('reports'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/PartnerTest.php: -------------------------------------------------------------------------------- 1 | get('_p/'.env('PARTNER01', 'none').'/report-hr-vaccination'); 19 | $response->assertStatus(200); 20 | $response->assertJson(fn (AssertableJson $json) => 21 | $json->has('last_updated') 22 | ->has('data') 23 | ->has('data.0', fn ($json) => 24 | $json->has('date') 25 | ->has('hr_uid') 26 | ->has('total_vaccinations') 27 | ->has('total_vaccinated') 28 | ->has('total_boosters_1') 29 | ->has('total_boosters_2') 30 | ) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2021_05_29_194015_create_vaccine_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->date('date'); 19 | // province code 20 | $table->string('province', 8); 21 | // vaccine reports 22 | $table->integer('total_adults_vaccinations')->nullable(); 23 | $table->integer('total_adults_vaccinated')->nullable(); 24 | $table->integer('change_adults_vaccinations')->nullable(); 25 | $table->integer('change_adults_vaccinated')->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('vaccine_reports'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 41 | ]; 42 | 43 | public function provinces() 44 | { 45 | return $this->belongsToMany('App\Province', 'provinces_users', 'user_id', 'province_id'); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'password_client_id' => env('PASSWORD_CLIENT_ID'), 19 | 'password_client_secret' => env('PASSWORD_CLIENT_SECRET'), 20 | ], 21 | 22 | 'mailgun' => [ 23 | 'domain' => env('MAILGUN_DOMAIN'), 24 | 'secret' => env('MAILGUN_SECRET'), 25 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 26 | ], 27 | 28 | 'postmark' => [ 29 | 'token' => env('POSTMARK_TOKEN'), 30 | ], 31 | 32 | 'ses' => [ 33 | 'key' => env('AWS_ACCESS_KEY_ID'), 34 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 35 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /resources/docs/1.0/regions.md: -------------------------------------------------------------------------------- 1 | # Health Regions 2 | 3 | --- 4 | 5 | - [Basic Usage](#basic) 6 | - [Sample Response](#sample-response) 7 | - [Specific Region](#single) 8 | - [By Province](#by-province) 9 | 10 | 11 | ## Basic Usage 12 | 13 | Returns a list of regions, including the hr_uid, which becomes helpful when seeking data for specific health regions with other endpoints. 14 | 15 | | Method | URI | 16 | | :- | :- | 17 | | GET | `/regions` | 18 | 19 | 20 | ## Sample Response 21 | 22 | ```json 23 | { 24 | "data": [ 25 | { 26 | "hr_uid":471, 27 | "province":"SK", 28 | "engname":"Far North", 29 | "frename":"Far North" 30 | }, 31 | ... 32 | ] 33 | } 34 | ``` 35 | 36 | 37 | ## Get a specific Region 38 | 39 | Returns a single health region record by Health Region UID (hr_uid) 40 | 41 | | Method | URI | 42 | | :- | :- | 43 | | GET | `/regions/:hr_uid` | 44 | 45 | ### Example 46 | 47 | `/regions/3553` 48 | 49 | 50 | ## Regions by Province 51 | 52 | Returns all Health Regions assigned to a Province. 53 | 54 | | Method | URI | 55 | | :- | :- | 56 | | GET | `/province/:code/regions` | 57 | 58 | ### Example 59 | 60 | `/province/SK/regions` 61 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /database/migrations/2020_07_23_201616_create_hr_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('hr_uid'); 19 | $table->date('date'); 20 | // reporting columns 21 | $table->integer('tests')->nullable(); 22 | $table->integer('cases')->nullable(); 23 | $table->integer('hospitalizations')->nullable(); 24 | $table->integer('criticals')->nullable(); 25 | $table->integer('recoveries')->nullable(); 26 | $table->integer('fatalities')->nullable(); 27 | $table->integer('vaccinations')->nullable(); 28 | $table->string('notes')->nullable(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('hr_reports'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/migrations/2021_04_27_214341_create_vaccine_distribution_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->date('date'); 19 | // province code 20 | $table->string('province', 8); 21 | // vaccine types 22 | $table->integer('pfizer_biontech')->nullable(); 23 | $table->integer('pfizer_biontech_administered')->nullable(); 24 | $table->integer('moderna')->nullable(); 25 | $table->integer('moderna_administered')->nullable(); 26 | $table->integer('astrazeneca')->nullable(); 27 | $table->integer('astrazeneca_administered')->nullable(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('vaccine_distribution'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Utilities/ProxyRequest.php: -------------------------------------------------------------------------------- 1 | 'password', 11 | 'username' => $email, 12 | 'password' => $password, 13 | ]; 14 | 15 | return $this->makePostRequest($params); 16 | } 17 | 18 | public function refreshAccessToken() 19 | { 20 | $refreshToken = request('refresh_token'); 21 | 22 | abort_unless($refreshToken, 403, 'Missing request token.'); 23 | 24 | $params = [ 25 | 'grant_type' => 'refresh_token', 26 | 'refresh_token' => $refreshToken, 27 | ]; 28 | 29 | return $this->makePostRequest($params); 30 | } 31 | 32 | protected function makePostRequest(array $params) 33 | { 34 | $params = array_merge([ 35 | 'client_id' => config('services.passport.password_client_id'), 36 | 'client_secret' => config('services.passport.password_client_secret'), 37 | 'scope' => '*', 38 | ], $params); 39 | 40 | $proxy = \Request::create('oauth/token', 'post', $params); 41 | $resp = json_decode(app()->handle($proxy)->getContent()); 42 | 43 | return $resp; 44 | } 45 | } -------------------------------------------------------------------------------- /database/migrations/2020_04_12_050802_create_provinces_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('code', 8)->unique(); 19 | $table->string('name'); 20 | // reference where tracking information is retrieved from 21 | $table->string('data_source')->nullable(); 22 | // additional attributes that may be useful 23 | $table->integer('population')->nullable(); 24 | // leaving the unit of measurement out to allow for some flexibility 25 | // want to use sq miles with total area (land and water), and billions for gdp, go for it 26 | $table->float('area', 9, 2)->nullable(); 27 | $table->integer('gdp')->nullable(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('provinces'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | call(function () { 32 | ProcessQueue::process(); 33 | }) 34 | ->everyMinute() 35 | ->runInBackground(); 36 | $schedule->call(function () { 37 | Artisan::call('report:processrt'); 38 | }) 39 | ->hourly() 40 | ->runInBackground(); 41 | } 42 | 43 | /** 44 | * Register the commands for the application. 45 | * 46 | * @return void 47 | */ 48 | protected function commands() 49 | { 50 | $this->load(__DIR__.'/Commands'); 51 | 52 | require base_path('routes/console.php'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /database/migrations/2021_06_13_175534_add_out_of_province_to_vaccine_reports.php: -------------------------------------------------------------------------------- 1 | integer('change_vaccinations_out_of_province')->nullable(); 18 | $table->integer('change_vaccinated_out_of_province')->nullable(); 19 | $table->integer('total_vaccinations_out_of_province')->nullable(); 20 | $table->integer('total_vaccinated_out_of_province')->nullable(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::table('vaccine_reports', function (Blueprint $table) { 32 | $table->dropColumn('change_vaccinations_out_of_province'); 33 | $table->dropColumn('change_vaccinated_out_of_province'); 34 | $table->dropColumn('total_vaccinations_out_of_province'); 35 | $table->dropColumn('total_vaccinated_out_of_province'); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/RapidTestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 17 | ['positive', 'negative', 'invalid result'], 18 | RapidTest::getTestResultsTypes() 19 | ); 20 | } 21 | 22 | /** 23 | * today should be considered valid 24 | */ 25 | public function test_today_is_valid() 26 | { 27 | $this->assertFalse( RapidTest::isTestDateInvalid( date('Y-m-d') ) ); 28 | } 29 | 30 | /** 31 | * today should be considered valid 32 | */ 33 | public function test_day_before_rapid_test_start_date_is_invalid() 34 | { 35 | // earliest test date supported 36 | $min_u = strtotime( env('RAPID_TESTS_START_DATE', '2021-12-01') ); 37 | $day_before_min_u = $min_u - (24 * 60 * 60); 38 | $res = RapidTest::isTestDateInvalid( date('Y-m-d', $day_before_min_u) ); 39 | $this->assertStringContainsString( 'before', $res ); 40 | } 41 | 42 | /** 43 | * status key is fillable 44 | */ 45 | public function test_status_key_is_fillable() 46 | { 47 | $rapid_test = new RapidTest(); 48 | $this->assertContains( RapidTest::reportStatusKey(), $rapid_test->getFillable() ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/SubRegionController.php: -------------------------------------------------------------------------------- 1 | getRequestUri(); 18 | $value = Cache::rememberForever( $cache_key, function() use ($request, $code) { 19 | $regions = []; 20 | if( $code ) { 21 | $regions = SubRegion::find( $code ); 22 | } else { 23 | $regions = SubRegion::all(); 24 | } 25 | return [ 26 | 'data' => $regions, 27 | ]; 28 | });//cache closure 29 | return $value; 30 | } 31 | 32 | /* 33 | return list of all provinces that have sub-regions 34 | */ 35 | public function provinces( Request $request ) { 36 | // $cache_key = $request->getRequestUri(); 37 | // $value = Cache::rememberForever( $cache_key, function() use ($request) { 38 | $provinces = SubRegion::with('parent')->get()->pluck('parent')->unique()->flatten(); 39 | return $provinces; 40 | // });//cache closure 41 | // return $value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/seeders/csv/reports.csv: -------------------------------------------------------------------------------- 1 | province,date,tests,cases,hospitalizations,criticals,recoveries,fatalities 2 | aa,2020-03-01,3816,84,12,2,42,1 3 | aa,2020-03-02,2062,49,5,1,25,1 4 | aa,2020-03-03,2800,25,2,0,13,0 5 | aa,2020-03-04,4353,50,6,1,25,1 6 | aa,2020-03-05,4182,54,5,0,27,1 7 | aa,2020-03-06,1647,89,8,2,45,0 8 | aa,2020-03-07,3768,52,6,1,26,0 9 | aa,2020-03-08,4396,87,11,2,44,2 10 | aa,2020-03-09,1380,49,8,1,25,2 11 | aa,2020-03-10,4389,22,3,1,11,0 12 | aa,2020-03-11,2307,83,7,0,42,1 13 | aa,2020-03-12,2153,72,11,1,36,0 14 | aa,2020-03-13,646,54,6,1,27,0 15 | aa,2020-03-14,1953,35,4,0,18,1 16 | aa,2020-03-15,2930,59,5,1,30,0 17 | aa,2020-03-16,4003,33,3,1,17,1 18 | aa,2020-03-17,4998,42,6,1,21,0 19 | aa,2020-03-18,4376,99,16,1,50,4 20 | aa,2020-03-19,1757,45,6,1,23,1 21 | aa,2020-03-20,2154,77,9,1,39,1 22 | bb,2020-03-01,7734,76,7,2,38,1 23 | bb,2020-03-02,4260,82,14,2,41,3 24 | bb,2020-03-03,2994,130,20,2,65,2 25 | bb,2020-03-04,10587,102,15,3,51,1 26 | bb,2020-03-05,1941,122,17,1,61,2 27 | bb,2020-03-06,9695,191,20,1,96,4 28 | bb,2020-03-07,2702,195,30,4,98,2 29 | bb,2020-03-08,6303,37,5,1,19,1 30 | bb,2020-03-09,4623,36,5,1,18,1 31 | bb,2020-03-10,7050,199,34,5,100,1 32 | bb,2020-03-11,4923,37,4,1,19,0 33 | bb,2020-03-12,8163,138,19,4,69,4 34 | bb,2020-03-13,6202,144,15,4,72,1 35 | bb,2020-03-14,6267,198,30,6,99,5 36 | bb,2020-03-15,4088,168,25,5,84,3 37 | bb,2020-03-16,8310,118,19,1,59,2 38 | bb,2020-03-17,6227,162,21,3,81,3 39 | bb,2020-03-18,7040,68,7,1,34,1 40 | bb,2020-03-19,1220,117,17,2,59,3 41 | bb,2020-03-20,3688,181,24,4,91,3 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | 9 | DB_CONNECTION=mysql 10 | DB_HOST=127.0.0.1 11 | DB_PORT=3306 12 | DB_DATABASE=laravel 13 | DB_USERNAME=root 14 | DB_PASSWORD= 15 | 16 | MONGODB_HOST=127.0.0.1 17 | MONGODB_PORT=27017 18 | MONGODB_DATABASE=laravel 19 | MONGODB_USERNAME=root 20 | MONGODB_PASSWORD= 21 | 22 | BROADCAST_DRIVER=log 23 | CACHE_DRIVER=file 24 | QUEUE_CONNECTION=sync 25 | SESSION_DRIVER=file 26 | SESSION_LIFETIME=120 27 | 28 | REDIS_HOST=127.0.0.1 29 | REDIS_PASSWORD=null 30 | REDIS_PORT=6379 31 | 32 | MAIL_MAILER=smtp 33 | MAIL_HOST=smtp.mailtrap.io 34 | MAIL_PORT=2525 35 | MAIL_USERNAME=null 36 | MAIL_PASSWORD=null 37 | MAIL_ENCRYPTION=null 38 | MAIL_FROM_ADDRESS=null 39 | MAIL_FROM_NAME="${APP_NAME}" 40 | 41 | AWS_ACCESS_KEY_ID= 42 | AWS_SECRET_ACCESS_KEY= 43 | AWS_DEFAULT_REGION=us-east-1 44 | AWS_BUCKET= 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_APP_CLUSTER=mt1 50 | 51 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 52 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 53 | 54 | # user authentication (C19T Manager) 55 | PERSONAL_CLIENT_ID=1 56 | PERSONAL_CLIENT_SECRET=111 57 | 58 | PASSWORD_CLIENT_ID=2 59 | PASSWORD_CLIENT_SECRET=222 60 | 61 | # C19T Manager URL for CORS whitelist 62 | MANAGE_URL=http://localhost:3000 63 | 64 | # number of days to go back for reports returning recent data 65 | RECENT_REPORT_DAYS=10 66 | 67 | # used for rapid test submission validation, leave blank to disable 68 | RECAPTCHA_SECRET_KEY= -------------------------------------------------------------------------------- /database/migrations/2021_11_07_111954_create_sr_vaccine_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('code', 8); 19 | $table->date('date'); 20 | // stats 21 | $table->integer('total_dose_1')->nullable(); 22 | $table->decimal('percent_dose_1', $precision = 6, $scale = 5)->nullable(); 23 | $table->enum('source_dose_1', ['total', 'percent'])->nullable(); 24 | $table->integer('total_dose_2')->nullable(); 25 | $table->decimal('percent_dose_2', $precision = 6, $scale = 5)->nullable(); 26 | $table->enum('source_dose_2', ['total', 'percent'])->nullable(); 27 | $table->integer('total_dose_3')->nullable(); 28 | $table->decimal('percent_dose_3', $precision = 6, $scale = 5)->nullable(); 29 | $table->enum('source_dose_3', ['total', 'percent'])->nullable(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('sr_vaccine_reports'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProvinceController.php: -------------------------------------------------------------------------------- 1 | getRequestUri(); 18 | $value = Cache::rememberForever( $cache_key, function() use ($request) { 19 | $provinces = Province::query(); 20 | if( request('geo_only') ) 21 | $provinces->where( 'geographic', 1 ); 22 | return $provinces->get(); 23 | });//cache closure 24 | return $value; 25 | } 26 | 27 | public function get( Request $request, $province = null ) { 28 | $cache_key = $request->getRequestUri(); 29 | $value = Cache::rememberForever( $cache_key, function() use ($request, $province) { 30 | return Province::where( 'code', $province )->get(); 31 | }); 32 | return $value; 33 | } 34 | 35 | /* 36 | return health regions in a given province 37 | */ 38 | public function healthRegions( Request $request, $province = null ) { 39 | $cache_key = $request->getRequestUri(); 40 | $value = Cache::rememberForever( $cache_key, function() use ($request, $province) { 41 | return HealthRegion::where( 'province', $province )->get(); 42 | }); 43 | return $value; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /resources/docs/1.0/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | --- 4 | 5 | - [Welcome](#welcome) 6 | - [Get Started](#get-started) 7 | - [Features](#features) 8 | - [License](#license) 9 | - [Credits](#credits) 10 | 11 | 12 | ## Welcome 13 | 14 | This is the API service for the [COVID-19 Tracker Canada](https://covid19tracker.ca) project. The goal is to provide immediate access to the dataset maintained by the COVID-19 Tracker Canada team. 15 | 16 | 17 | ## Get Started 18 | 19 | `GET` https://api.covid19tracker.ca/summary 20 | 21 | Returns the current date's national-level data including cumulative and new cases, fatalities, tests, recoveries, vaccinations and more. 22 | 23 | See additional routes in the menu. 24 | 25 | 26 | ## Issues / Troubleshooting 27 | 28 | [https://github.com/andrewthong/covid19tracker-api/issues](https://github.com/andrewthong/covid19tracker-api/issues) 29 | 30 | 31 | ## License 32 | 33 | The [COVID19Tracker.ca](https://covid19tracker.ca) Dataset is licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). 34 | 35 | API source is licensed under the [MIT License](https://github.com/andrewthong/covid19tracker-api/blob/master/LICENSE). 36 | 37 | 38 | ## Credits 39 | 40 | Team [acknowledgements](https://covid19tracker.ca/acknowledgements.html). 41 | 42 | ### Made with 43 | 44 | - Framework: [Laravel](https://laravel.com/) 45 | - Documentation: [LaRecipe](https://larecipe.binarytorch.com.my/) 46 | - Database Management: [Adminer](https://www.adminer.org/) 47 | -------------------------------------------------------------------------------- /database/seeders/NoteSeeder.php: -------------------------------------------------------------------------------- 1 | insert([ 32 | 'title' => $note[0], 33 | 'description' => $note[1], 34 | 'expiry_date' => $note[2], 35 | 'tag' => $note[3], 36 | 'priority' => $note[4], 37 | 'type' => $note[5], 38 | ]); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/FatalityController.php: -------------------------------------------------------------------------------- 1 | province ) { 19 | $province = $request->province; 20 | } 21 | 22 | // hr_uid support 23 | $hr_uid = null; 24 | if( $request->hr_uid ) { 25 | $hr_uid = $request->hr_uid; 26 | } 27 | 28 | // pagination 29 | $per_page = 100; 30 | if( $request->per_page ) { 31 | $num = (int) $request->per_page; 32 | $per_page = max( min( $request->per_page, 1000 ), 1); 33 | } 34 | 35 | $order = 'DESC'; 36 | $order_by = 'id'; 37 | if( $request->order === 'ASC' ) { 38 | $order = $request->order; 39 | } 40 | 41 | $cases = DB::table('fatalities') 42 | ->when( $province, function($query) use ($province) { 43 | // if a province is provided; otherwise all 44 | return $query->where('province', $province); 45 | }) 46 | ->when( $hr_uid, function($query) use ($hr_uid) { 47 | return $query->where('hr_uid', $hr_uid); 48 | }) 49 | ->orderBy('id', $order) 50 | ->paginate($per_page); 51 | 52 | return $cases; 53 | } 54 | 55 | /* 56 | get specific fatality 57 | */ 58 | public function get($id) { 59 | return Fatality::find($id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resources/docs/1.0/fatalities.md: -------------------------------------------------------------------------------- 1 | # Fatalities 2 | 3 | *This feature is depreciated, and individual-level fatality data is no longer updated; it remains available to allow access to historical data.* 4 | 5 | --- 6 | 7 | - [Basic Usage](#basic) 8 | - [Sample Response](#sample-response) 9 | - [Parameters](#parameters) 10 | - [Single Record](#single) 11 | 12 | 13 | ## Basic Usage 14 | 15 | Returns the latest 100 fatalities. 16 | 17 | | Method | URI | 18 | | :- | :- | 19 | | GET | `/fatalities` | 20 | 21 | 22 | ## Sample Response 23 | 24 | ```json 25 | { 26 | "current_page": 1, 27 | "data": [ 28 | { 29 | "id": 380, 30 | "province": "ON", 31 | "date": "2020-04-07" 32 | }, 33 | ... 34 | ], 35 | "first_page_url": ".../fatalities?page=1", 36 | "from": 1, 37 | "last_page": 4, 38 | "last_page_url": ".../fatalities?page=4", 39 | "next_page_url": ".../fatalities?page=2", 40 | "path": ".../fatalities", 41 | "per_page": "100", 42 | "prev_page_url": null, 43 | "to": 100, 44 | "total": 380 45 | } 46 | ``` 47 | 48 | 49 | 50 | ## Parameters 51 | 52 | | Parameter | Type | Description | 53 | | :- | :- | :- | 54 | | province | String | Filter fatalities to a specific province e.g. `QC` | 55 | | per_page | Integer | Number of fatalities per page (max 1000) | 56 | | order | `DESC`\|`ASC` | If `ASC`, oldest fatalities are shown first. Defaults to `DESC` | 57 | 58 | ### Example 59 | `/fatalities?province=ON&per_page=50` 60 | 61 | 62 | 63 | ## Single Record 64 | 65 | Returns a single fatality record by ID. 66 | 67 | | Method | URI | 68 | | :- | :- | 69 | | GET | `/fatality/:id` | 70 | 71 | ### Example 72 | 73 | `/fatality/123` 74 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /database/migrations/2020_04_19_003533_create_processed_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('province'); 19 | $table->date('date'); 20 | // change (difference between last period) 21 | $table->integer('change_tests')->nullable(); 22 | $table->integer('change_cases')->nullable(); 23 | $table->integer('change_hospitalizations')->nullable(); 24 | $table->integer('change_criticals')->nullable(); 25 | $table->integer('change_recoveries')->nullable(); 26 | $table->integer('change_fatalities')->nullable(); 27 | // total (rolling cumulative count) 28 | $table->integer('total_tests')->nullable(); 29 | $table->integer('total_cases')->nullable(); 30 | $table->integer('total_hospitalizations')->nullable(); 31 | $table->integer('total_criticals')->nullable(); 32 | $table->integer('total_recoveries')->nullable(); 33 | $table->integer('total_fatalities')->nullable(); 34 | // additional notes 35 | $table->string('notes')->nullable(); 36 | $table->timestamps(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | Schema::dropIfExists('processed_reports'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'useTLS' => true, 41 | ], 42 | ], 43 | 44 | 'redis' => [ 45 | 'driver' => 'redis', 46 | 'connection' => 'default', 47 | ], 48 | 49 | 'log' => [ 50 | 'driver' => 'log', 51 | ], 52 | 53 | 'null' => [ 54 | 'driver' => 'null', 55 | ], 56 | 57 | ], 58 | 59 | ]; 60 | -------------------------------------------------------------------------------- /resources/docs/1.0/cases.md: -------------------------------------------------------------------------------- 1 | # Cases 2 | 3 | *This feature is depreciated, and individual-level case data is no longer updated; it remains available to allow access to historical data.* 4 | 5 | --- 6 | 7 | - [Basic Usage](#basic) 8 | - [Sample Response](#sample-response) 9 | - [Parameters](#parameters) 10 | - [Single Record](#single) 11 | 12 | 13 | ## Basic Usage 14 | 15 | Returns the latest 100 cases. 16 | 17 | | Method | URI | 18 | | :- | :- | 19 | | GET | `/cases` | 20 | 21 | 22 | ## Sample Response 23 | 24 | ```json 25 | { 26 | "current_page": 1, 27 | "data": [ 28 | { 29 | "id": 10000, 30 | "province": "ON", 31 | "city": "Example City", 32 | "age": "60", 33 | "travel_history": "Pending", 34 | "confirmed_presumptive": "CONFIRMED", 35 | "source": "https://www.example.net", 36 | "date": "2020-03-30 19:45:00" 37 | }, 38 | ... 39 | ], 40 | "first_page_url": ".../cases", 41 | "from": 1, 42 | "last_page": 100, 43 | "last_page_url": ".../cases?page=100", 44 | "next_page_url": ".../cases?page=2", 45 | "path": ".../cases", 46 | "per_page": "100", 47 | "prev_page_url": null, 48 | "to": 100, 49 | "total": 10000 50 | } 51 | ``` 52 | 53 | 54 | ## Parameters 55 | 56 | | Parameter | Type | Description | 57 | | :- | :- | :- | 58 | | province | String | Filter cases to a specific province e.g. `QC` | 59 | | per_page | Integer | Number of cases per page (max 1000) | 60 | | order | `DESC`\|`ASC` | If `ASC`, oldest cases are shown first. Defaults to `DESC` | 61 | 62 | ### Example 63 | `/cases?province=ON&per_page=50` 64 | 65 | 66 | 67 | ## Single Record 68 | 69 | Returns a single case record by ID. 70 | 71 | | Method | URI | 72 | | :- | :- | 73 | | GET | `/case/:id` | 74 | 75 | ### Example 76 | 77 | `/case/123` 78 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /database/migrations/2021_09_06_194048_drop_adult_and_out_of_province_from_vaccine_reports.php: -------------------------------------------------------------------------------- 1 | dropColumn('total_adults_vaccinations'); 18 | $table->dropColumn('total_adults_vaccinated'); 19 | $table->dropColumn('change_adults_vaccinations'); 20 | $table->dropColumn('change_adults_vaccinated'); 21 | $table->dropColumn('change_vaccinations_out_of_province'); 22 | $table->dropColumn('change_vaccinated_out_of_province'); 23 | $table->dropColumn('total_vaccinations_out_of_province'); 24 | $table->dropColumn('total_vaccinated_out_of_province'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::table('vaccine_reports', function (Blueprint $table) { 36 | $table->integer('total_adults_vaccinations')->nullable(); 37 | $table->integer('total_adults_vaccinated')->nullable(); 38 | $table->integer('change_adults_vaccinations')->nullable(); 39 | $table->integer('change_adults_vaccinated')->nullable(); 40 | $table->integer('change_vaccinations_out_of_province')->nullable(); 41 | $table->integer('change_vaccinated_out_of_province')->nullable(); 42 | $table->integer('total_vaccinations_out_of_province')->nullable(); 43 | $table->integer('total_vaccinated_out_of_province')->nullable(); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /database/migrations/2020_09_06_092920_create_processed_hr_reports_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('hr_uid'); 19 | $table->date('date'); 20 | // change (difference between last period) 21 | $table->integer('change_tests')->nullable(); 22 | $table->integer('change_cases')->nullable(); 23 | $table->integer('change_hospitalizations')->nullable(); 24 | $table->integer('change_criticals')->nullable(); 25 | $table->integer('change_recoveries')->nullable(); 26 | $table->integer('change_fatalities')->nullable(); 27 | $table->integer('change_vaccinations')->nullable(); 28 | // total (rolling cumulative count) 29 | $table->integer('total_tests')->nullable(); 30 | $table->integer('total_cases')->nullable(); 31 | $table->integer('total_hospitalizations')->nullable(); 32 | $table->integer('total_criticals')->nullable(); 33 | $table->integer('total_recoveries')->nullable(); 34 | $table->integer('total_fatalities')->nullable(); 35 | $table->integer('total_vaccinations')->nullable(); 36 | // additional notes 37 | $table->string('notes')->nullable(); 38 | $table->timestamps(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | * 45 | * @return void 46 | */ 47 | public function down() 48 | { 49 | Schema::dropIfExists('processed_hr_reports'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapApiRoutes(); 46 | 47 | $this->mapWebRoutes(); 48 | 49 | // 50 | } 51 | 52 | /** 53 | * Define the "web" routes for the application. 54 | * 55 | * These routes all receive session state, CSRF protection, etc. 56 | * 57 | * @return void 58 | */ 59 | protected function mapWebRoutes() 60 | { 61 | Route::middleware('web') 62 | ->namespace($this->namespace) 63 | ->group(base_path('routes/web.php')); 64 | } 65 | 66 | /** 67 | * Define the "api" routes for the application. 68 | * 69 | * These routes are typically stateless. 70 | * 71 | * @return void 72 | */ 73 | protected function mapApiRoutes() 74 | { 75 | Route::middleware('api') 76 | ->namespace($this->namespace) 77 | ->group(base_path('routes/api.php')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/RapidTest.php: -------------------------------------------------------------------------------- 1 | timestamp; 61 | $max_date = date('Y-m-d', $max_u); 62 | 63 | $the_date = strtotime($test_date); 64 | 65 | if( $the_date < $min_u ) { 66 | return "Test date cannot be before {$min_date}"; 67 | } 68 | if( $the_date > $max_u ) { 69 | return "Test date cannot be after {$max_date}"; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /tests/Feature/VaccineAgeGroupsTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/vaccines/age-groups'); 19 | $response->assertJson(fn (AssertableJson $json) => 20 | $json->has('province') 21 | ->has('data') 22 | ->has('data.0', fn ($json) => 23 | $json->has('date') 24 | ->has('data') 25 | ) 26 | ); 27 | } 28 | 29 | /** 30 | * split vaccine age groups 31 | */ 32 | public function test_vaccine_age_groups_split() 33 | { 34 | $response = $this->json('GET', '/vaccines/age-groups/split'); 35 | $response->assertJson(fn (AssertableJson $json) => 36 | $json->where('province', 'All') 37 | ->has('data') 38 | ->has('data.0', fn ($json) => 39 | $json->has('date') 40 | ->has('data') 41 | ->has('province') 42 | ) 43 | ); 44 | } 45 | 46 | /** 47 | * vaccine age groups by province 48 | */ 49 | public function test_vaccine_age_groups_by_province() 50 | { 51 | $response = $this->json('GET', '/vaccines/age-groups/province/ab'); 52 | $response->assertJson(fn (AssertableJson $json) => 53 | $json->where('province', 'ab') 54 | ->has('data') 55 | ->has('data.0', fn ($json) => 56 | $json->has('date') 57 | ->has('data') 58 | ) 59 | ); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | define('LARAVEL_START', microtime(true)); 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Register The Auto Loader 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Composer provides a convenient, automatically generated class loader for 18 | | our application. We just need to utilize it! We'll simply require it 19 | | into the script here so that we don't have to worry about manual 20 | | loading any of our classes later on. It feels great to relax. 21 | | 22 | */ 23 | 24 | require __DIR__.'/../vendor/autoload.php'; 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Turn On The Lights 29 | |-------------------------------------------------------------------------- 30 | | 31 | | We need to illuminate PHP development, so let us turn on the lights. 32 | | This bootstraps the framework and gets it ready for use, then it 33 | | will load up this application so that we can run it and send 34 | | the responses back to the browser and delight our users. 35 | | 36 | */ 37 | 38 | $app = require_once __DIR__.'/../bootstrap/app.php'; 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Run The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once we have the application, we can handle the incoming request 46 | | through the kernel, and send the associated response back to 47 | | the client's browser allowing them to enjoy the creative 48 | | and wonderful application we have prepared for them. 49 | | 50 | */ 51 | 52 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 53 | 54 | $response = $kernel->handle( 55 | $request = Illuminate\Http\Request::capture() 56 | ); 57 | 58 | $response->send(); 59 | 60 | $kernel->terminate($request, $response); 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": [ 6 | "framework", 7 | "laravel" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^7.3", 12 | "binarytorch/larecipe": "^2.4", 13 | "fideloper/proxy": "^4.2", 14 | "flynsarmy/csv-seeder": "2.*", 15 | "fruitcake/laravel-cors": "^1.0", 16 | "guzzlehttp/guzzle": "^7.0.1", 17 | "jenssegers/mongodb": "^3.8", 18 | "laravel/framework": "^8.0", 19 | "laravel/passport": "^10.0", 20 | "laravel/tinker": "^2.0", 21 | "mongodb/mongodb": "^1.6", 22 | "spatie/laravel-permission": "^3.17" 23 | }, 24 | "require-dev": { 25 | "facade/ignition": "^2.5.14", 26 | "fzaninotto/faker": "^1.9.1", 27 | "mockery/mockery": "^1.3.1", 28 | "nunomaduro/collision": "^5.0", 29 | "phpunit/phpunit": "^9.0", 30 | "illuminate/support": "^8.6" 31 | }, 32 | "config": { 33 | "optimize-autoloader": true, 34 | "preferred-install": "dist", 35 | "sort-packages": true 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "dont-discover": [] 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "App\\": "app/" 45 | }, 46 | "classmap": [ 47 | "database/seeders", 48 | "database/factories" 49 | ] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Tests\\": "tests/" 54 | } 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true, 58 | "scripts": { 59 | "post-autoload-dump": [ 60 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 61 | "@php artisan package:discover --ansi" 62 | ], 63 | "post-root-package-install": [ 64 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 65 | ], 66 | "post-create-project-cmd": [ 67 | "@php artisan key:generate --ansi" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Feature/RapidTestTest.php: -------------------------------------------------------------------------------- 1 | first()->letter; 32 | if(!$p1) $p1 = 'A'; 33 | $payload = [ 34 | 'test_date' => date('Y-m-d', strtotime('-1 day')), 35 | 'test_result' => $rt[array_rand($rt)], 36 | 'postal_code' => $p1.rand(0, 9).$p1, 37 | 'age' => '12345', 38 | ]; 39 | 40 | $response = $this->json('POST', '/collect/rapid-test/', $payload); 41 | $response->assertJson(fn (AssertableJson $obj) => 42 | $obj->where('created', true) 43 | ); 44 | 45 | return $payload; 46 | } 47 | 48 | /** 49 | * @depends test_submission 50 | */ 51 | public function test_has_submission($payload) 52 | { 53 | // ISO date for MongoDB 54 | $payload['test_date'] = new DateTime($payload['test_date']); 55 | $submission = RapidTest::where($payload)->first(); 56 | // set status key so it's not processed 57 | $submission[RapidTest::reportStatusKey()] = 'ignored-test'; 58 | $submission->save(); 59 | // test 60 | $arr = $submission->toArray(); 61 | $this->assertArrayHasKey('created_at', $arr); 62 | $this->assertArrayHasKey('ip', $arr); 63 | $this->assertEquals($payload['test_result'], $arr['test_result']); 64 | $this->assertEquals($payload['postal_code'], $arr['postal_code']); 65 | $this->assertEquals($payload['age'], $arr['age']); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/docs/1.0/vaccine-age-groups.md: -------------------------------------------------------------------------------- 1 | # Vaccine Age Groups 2 | 3 | --- 4 | 5 | - [Basic Usage](#basic) 6 | - [Sample Response](#sample-response) 7 | - [Parameters](#parameters) 8 | - [Split](#split) 9 | - [By Province](#by-province) 10 | 11 | 12 | 13 | ## Basic Usage 14 | 15 | By default, this request returns a week-by-week report of vaccine stats by various age groups. 16 | 17 | While national-level data and data for most provinces is updated weekly from PHAC, some provinces including Ontario, Quebec and Saskatchewan include data updated daily from provincial sources. 18 | 19 | | Method | URI | 20 | | :- | :- | 21 | | GET | `/vaccines/age-groups` | 22 | 23 | 24 | 25 | ## Sample Response 26 | 27 | ```json 28 | { 29 | "province": "All", 30 | "data": [ 31 | { 32 | "date": "2021-05-01", 33 | "data": "{\"80+\": {\"full\": 292848..." 34 | }, 35 | ... 36 | ] 37 | } 38 | ``` 39 | 40 | ### data 41 | An array of vaccination reports. 42 | - **date** — in `Y-m-d` format 43 | - **data** — JSON string containing stats split into age group. 44 | 45 | *due to reporting standard shifts overtime, the JSON string data may not be consistent across weeks. Minimal effort is taken to normalize some of this data. 46 | 47 | 48 | 49 | ## Parameters 50 | 51 | | Parameter | Type | Description | 52 | | :- | :- | :- | 53 | | after | Date | Returns reports on or after the specified date. | 54 | | before | Date | Returns reports on or before the specified date. | 55 | | group | String | Returns reports for a specific group. Must be URL encoded | 56 | 57 | ### Example 58 | 59 | `/vaccines/age-groups?after=2021-03-01&group=80%2B` 60 | 61 | 62 | ## Split 63 | 64 | This will return all provinces (except ALL/Canada). 65 | 66 | | Method | URI | 67 | | :- | :- | 68 | | GET | `/vaccines/age-groups/split` | 69 | 70 | ### Data 71 | An array of vaccination reports but with an addition when split. 72 | - **province** — province code 73 | 74 | 75 | 76 | ## By Province 77 | 78 | Filter the data to the province level by providing a province code e.g. `SK`. All parameters are supported. 79 | 80 | | Method | URI | 81 | | :- | :- | 82 | | GET | `/vaccines/age-groups/province/:code` | 83 | 84 | ### Example 85 | 86 | `/vaccines/age-groups/province/ab` 87 | -------------------------------------------------------------------------------- /resources/docs/1.0/provinces.md: -------------------------------------------------------------------------------- 1 | # Provinces 2 | 3 | --- 4 | 5 | - [Basic Usage](#basic) 6 | - [Sample Response](#sample-response) 7 | 8 | 9 | ## Basic Usage 10 | 11 | Returns a list of provinces, including the status of each province. 12 | 13 | | Method | URI | 14 | | :- | :- | 15 | | GET | `/provinces` | 16 | 17 | 18 | ## Sample Response 19 | 20 | ```json 21 | [ 22 | { 23 | "id": 1, 24 | "code": "ON", 25 | "name": "Ontario", 26 | "data_source": null, 27 | "population": 14711827, 28 | "area": 917741, 29 | "gdp": 857384, 30 | "geographic": 1, 31 | "data_status": "Reported", 32 | "created_at": null, 33 | "updated_at": "2021-01-06T22:38:43.000000Z", 34 | "density": 16.030478097851137 35 | }, 36 | ... 37 | ] 38 | ``` 39 | 40 | ### Attributes 41 | 42 | Additional context for the attributes returned. 43 | 44 | | Attribute | Description | 45 | | :- | :- | 46 | | Code | Code or initial of the province. This is used as the primary identifier when aggregating data | 47 | | Geographic | A boolean used to indicate whether the grouping is geographical or not. In the dataset, this allows for groupings like "Repatriated" | 48 | | Data Status | This is set by the Manage utility to note the current status of reporting. | 49 | | Updated At | Timestamp when reports for the province (or health region in the province) was updated | 50 | 51 | 52 | ## Parameters 53 | 54 | | Parameter | Type | Description | 55 | | :- | :- | :- | 56 | | geo_only | Boolean | If true, only provinces marked as geographic will be returned. This is useful for excluding data from non-geographic entities such as Repatriated Canadians or the Federal Allocation for vaccinations. | 57 | 58 | 59 | ## Data Status 60 | Additional context for the Data Status attribute. 61 | 62 | | Status | Description | 63 | | :- | :- | 64 | | Waiting for report | This status indicated that an update is expected to happen in the current day, but has not yet occured. | 65 | | In progress | This status indicates that an update is in-progress and will be completed soon. Note that when this status is indicated, some or all data may not be updated yet. | 66 | | Reported | When this status is indicated, the province has been updated with final data for the day, and the update is complete. | 67 | | No report expected today | When this status is indicated, the province is not expected to provide an update on the current day, and one should not be expected. | 68 | | Custom | Custom statuses are used to communicate certain issues with a province's update including delays or partial updates. | 69 | -------------------------------------------------------------------------------- /resources/docs/1.0/subregions.md: -------------------------------------------------------------------------------- 1 | # Subregions 2 | 3 | --- 4 | 5 | - [Overview](#overview) 6 | - [Basic Usage](#basic) 7 | - [Sample Response](#sample-response) 8 | - [Specific Region](#single) 9 | - [Population Values](#pop) 10 | - [Accessing The Data](#data) 11 | 12 | 13 | ## Overview 14 | Subregions were introduced in late 2021 to take advantage of the availability of sub-health region level vaccination data in some provinces and territories. 15 | 16 | Subregion data only includes data on vaccinations, and may return data as a raw number or as a percentage, depending on the source data. Regardless, the type of source data is indicated in each response. 17 | 18 | The current number of subregions by province can be found below: 19 | 20 | | Province | Number of Subregions | 21 | | :- | :- | 22 | | AB | 132 | 23 | | NL | 38 | 24 | | NT | 30 | 25 | | SK | 13 | 26 | | ON | 514 | 27 | | MB | 79 | 28 | 29 | 30 | ## Basic Usage 31 | Returns a list of regions, including the subregion code, which becomes helpful when seeking data for specific subregions with other endpoints. 32 | 33 | | Method | URI | 34 | | :- | :- | 35 | | GET | `/sub-regions` | 36 | 37 | 38 | ## Sample Response 39 | 40 | ```json 41 | { 42 | "data": [ 43 | { 44 | "code":"AB001", 45 | "province":"AB", 46 | "zone":"SOUTH", 47 | "region":"CROWSNEST PASS", 48 | "population":6280 49 | }, 50 | ... 51 | ] 52 | } 53 | ``` 54 | 55 | 56 | ## Get a Specific Subregion 57 | 58 | Returns a single health subregion record by subregion code 59 | 60 | | Method | URI | 61 | | :- | :- | 62 | | GET | `/sub-regions/:CODE` | 63 | 64 | ### Example 65 | 66 | `/sub-regions/AB001` 67 | 68 | 69 | ## Population Values 70 | 71 | Population values are returned with each subregion, and are used in calculations to interconvert vaccination totals and percentages. However, it is important to note that the population values for some regions only include the ***eligible*** population and exclude the portion of the population which is not yet able to receive a COVID-19 vaccine. 72 | 73 | This is reflected in both the population value and percentages returned. Please familiarize yourself with the table below, and check back frequently as this is subject to change. 74 | 75 | | Province | Subregion Percentage Type| 76 | | :- | :- | 77 | |AB| Total Population| 78 | |SK| Total Population| 79 | |NT| Total Population| 80 | |ON| Total Population| 81 | |MB| **Eligible Population Only**| 82 | |NL| **Eligible Population Only**| 83 | 84 | 85 | ## Accessing the Data 86 | 87 | See the [Vaccination Data](/{{route}}/{{version}}/vaccinations) page for more information on accessing subregion-level vaccination data. 88 | -------------------------------------------------------------------------------- /tests/Feature/LocationTest.php: -------------------------------------------------------------------------------- 1 | get('/provinces/'); 18 | $response->assertStatus(200); 19 | } 20 | 21 | /** 22 | * specific health region 23 | */ 24 | public function test_single_province() 25 | { 26 | $response = $this->get('/province/ab'); 27 | $response->assertStatus(200); 28 | $response->assertJson(fn (AssertableJson $json) => 29 | $json->has('0.id') 30 | ->where('0.code', 'AB') 31 | ->has('0.name') 32 | ->has('0.data_source') 33 | ->has('0.population') 34 | ->has('0.area') 35 | ->has('0.gdp') 36 | ->has('0.geographic') 37 | ->has('0.data_status') 38 | ->has('0.created_at') 39 | ->has('0.updated_at') 40 | ->has('0.density') 41 | ); 42 | } 43 | 44 | /** 45 | * basic health regions 46 | */ 47 | public function test_health_regions() 48 | { 49 | $response = $this->get('/regions/'); 50 | $response->assertStatus(200); 51 | } 52 | 53 | /** 54 | * specific health region 55 | */ 56 | public function test_single_health_region() 57 | { 58 | $response = $this->get('/regions/1201'); 59 | $response->assertStatus(200); 60 | $response->assertJson(fn (AssertableJson $json) => 61 | $json->has('data') 62 | ->where('data.hr_uid', 1201) 63 | ->has('data.province') 64 | ->has('data.engname') 65 | ->has('data.frename') 66 | ); 67 | } 68 | 69 | /** 70 | * basic sub regions 71 | */ 72 | public function test_sub_regions() 73 | { 74 | $response = $this->get('/sub-regions/'); 75 | $response->assertStatus(200); 76 | } 77 | 78 | /** 79 | * specific sub region 80 | */ 81 | public function test_single_sub_region() 82 | { 83 | $response = $this->get('/sub-regions/ab012'); 84 | $response->assertStatus(200); 85 | $response->assertJson(fn (AssertableJson $json) => 86 | $json->has('data') 87 | ->where('data.code', 'AB012') 88 | ->has('data.province') 89 | ->has('data.zone') 90 | ->has('data.region') 91 | ->has('data.population') 92 | ); 93 | } 94 | } -------------------------------------------------------------------------------- /app/Http/Controllers/PartnerReportController.php: -------------------------------------------------------------------------------- 1 | json(null); 29 | } 30 | 31 | // using a specifc key for now; until option support is added 32 | $cache_key = 'partner_01_hr_vaccination_report'; 33 | 34 | $value = Cache::rememberForever( $cache_key, function() use ($request) { 35 | 36 | // specific attributes to select 37 | $select_core = [ 38 | 'date', 39 | 'hr_uid', 40 | 'total_vaccinations', 41 | 'total_vaccinated', 42 | 'total_boosters_1', 43 | 'total_boosters_2', 44 | ]; 45 | 46 | $table = 'processed_hr_reports'; 47 | 48 | // get earliest date 49 | $result = DB::table( $table ) 50 | ->select('date') 51 | ->whereNotNull('total_vaccinations') 52 | ->orWhereNotNull('total_vaccinated') 53 | ->first(); 54 | $earliest_date = $result->date; 55 | 56 | // safeguard if no data 57 | if( !$earliest_date ) { 58 | return \response()->json(null); 59 | } 60 | 61 | // prepare SELECT 62 | $select_stmt = implode(",", $select_core); 63 | 64 | // prepare WHERE 65 | $where_stmt = "WHERE `date` >= '{$earliest_date}'"; 66 | 67 | // DB query 68 | $data = DB::select(" 69 | SELECT 70 | {$select_stmt} 71 | FROM 72 | {$table} 73 | {$where_stmt} 74 | ORDER BY 75 | `date`, `hr_uid` 76 | "); 77 | 78 | $last_run = Common::getLastUpdated( 'healthregion' ); 79 | 80 | $response = [ 81 | 'last_updated' => $last_run, 82 | 'data' => $data, 83 | ]; 84 | 85 | return response()->json($response)->setEncodingOptions(JSON_NUMERIC_CHECK); 86 | 87 | });//cache closure 88 | 89 | return $value; 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/Http/Controllers/CaseController.php: -------------------------------------------------------------------------------- 1 | Cases::count(), 23 | 'total_fatalities' => Fatality::count(), 24 | ]; 25 | return $result; 26 | } 27 | 28 | /* 29 | return count of cases by province 30 | */ 31 | public function summaryProvince() { 32 | $fatalities = DB::table('fatalities') 33 | ->selectRAW('fatalities.province, count(fatalities.province) as total_fatalities') 34 | ->groupBy('fatalities.province'); 35 | 36 | $result = DB::table('cases') 37 | ->selectRaw('cases.province, count(cases.province) as total_cases, ifnull(total_fatalities, 0) as total_fatalities') 38 | ->leftJoinSub($fatalities, 'fatalities', function ($join) { 39 | $join->on('cases.province', '=', 'fatalities.province'); 40 | }) 41 | ->groupBy('cases.province') 42 | ->get(); 43 | return $result; 44 | } 45 | 46 | /* 47 | cases 48 | */ 49 | public function list(Request $request, $province = null) { 50 | 51 | // province support 52 | if( $request->province ) { 53 | $province = $request->province; 54 | } 55 | 56 | // hr_uid support 57 | $hr_uid = null; 58 | if( $request->hr_uid ) { 59 | $hr_uid = $request->hr_uid; 60 | } 61 | 62 | // pagination 63 | $per_page = 100; 64 | if( $request->per_page ) { 65 | $num = (int) $request->per_page; 66 | $per_page = max( min( $request->per_page, 1000 ), 1); 67 | } 68 | 69 | $order = 'DESC'; 70 | $order_by = 'id'; 71 | if( $request->order === 'ASC' ) { 72 | $order = $request->order; 73 | } 74 | 75 | $cases = DB::table('cases') 76 | ->when( $province, function($query) use ($province) { 77 | // if a province is provided; otherwise all 78 | return $query->where('province', $province); 79 | }) 80 | ->when( $hr_uid, function($query) use ($hr_uid) { 81 | return $query->where('hr_uid', $hr_uid); 82 | }) 83 | ->orderBy('id', $order) 84 | ->paginate($per_page); 85 | 86 | return $cases; 87 | } 88 | 89 | /* 90 | get specific case 91 | */ 92 | public function get($id) { 93 | return Cases::find($id); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 34 | \App\Http\Middleware\EncryptCookies::class, 35 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 36 | \Illuminate\Session\Middleware\StartSession::class, 37 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 38 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 39 | \App\Http\Middleware\VerifyCsrfToken::class, 40 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 41 | ], 42 | 43 | 'api' => [ 44 | 'throttle:60,1', 45 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 46 | ], 47 | ]; 48 | 49 | /** 50 | * The application's route middleware. 51 | * 52 | * These middleware may be assigned to groups or used individually. 53 | * 54 | * @var array 55 | */ 56 | protected $routeMiddleware = [ 57 | 'auth' => \App\Http\Middleware\Authenticate::class, 58 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 59 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 60 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 61 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 62 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 63 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 64 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 65 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 66 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 67 | 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, 68 | ]; 69 | } 70 | -------------------------------------------------------------------------------- /app/Console/Commands/TransferCaseFatality.php: -------------------------------------------------------------------------------- 1 | output->createProgressBar( count($processed_reports) ); 57 | $bar->start(); 58 | 59 | // loop through each processed report 60 | foreach( $processed_reports as $pr ) { 61 | if( $pr->province && $pr->date ) { 62 | $affected = DB::table('reports') 63 | ->where([ 64 | ['province', '=', $pr->province], 65 | ['date', '=', $pr->date] 66 | ]) 67 | ->update([ 68 | 'cases' => $pr->total_cases, 69 | 'fatalities' => $pr->total_fatalities, 70 | ]); 71 | if( $affected === 0 ) { 72 | // update won't count if values are the same 73 | // only trigger warning when there is no corresponding row 74 | $check = DB::table('reports') 75 | ->where([ 76 | ['province', '=', $pr->province], 77 | ['date', '=', $pr->date] 78 | ]) 79 | ->get(); 80 | if( $check->isEmpty() ) { 81 | $this->line(" # No matching record in ID {$pr->id}"); 82 | } 83 | } 84 | } else { 85 | $this->line(" # Invalid province/date in ID {$pr->id}"); 86 | } 87 | $bar->advance(); 88 | } 89 | 90 | return 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "sftp", "s3" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'url' => env('APP_URL').'/storage', 55 | 'visibility' => 'public', 56 | ], 57 | 58 | 's3' => [ 59 | 'driver' => 's3', 60 | 'key' => env('AWS_ACCESS_KEY_ID'), 61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 62 | 'region' => env('AWS_DEFAULT_REGION'), 63 | 'bucket' => env('AWS_BUCKET'), 64 | 'endpoint' => env('AWS_URL'), 65 | ], 66 | 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Symbolic Links 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Here you may configure the symbolic links that will be created when the 75 | | `storage:link` Artisan command is executed. The array keys should be 76 | | the locations of the links and the values should be their targets. 77 | | 78 | */ 79 | 80 | 'links' => [ 81 | public_path('storage') => storage_path('app/public'), 82 | ], 83 | 84 | ]; 85 | -------------------------------------------------------------------------------- /tests/Feature/RapidTestReportTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/rapid-tests/'); 19 | $response->assertJson(fn (AssertableJson $obj) => 20 | $obj->has('last_updated') 21 | ->has('data', 1) 22 | ->has('data.0', fn ($obj) => 23 | $obj->has('latest_date') 24 | ->has('earliest_date') 25 | ->has('total_positive') 26 | ->has('total_negative') 27 | ->has('total_invalid') 28 | ) 29 | ->etc() 30 | ); 31 | } 32 | 33 | /** 34 | * split summary 35 | */ 36 | public function test_summary_split() 37 | { 38 | $response = $this->json('GET', '/rapid-tests/split'); 39 | $response->assertJson(fn (AssertableJson $obj) => 40 | $obj->has('last_updated') 41 | ->has('data') 42 | ->has('data.0', fn ($obj) => 43 | $obj->has('province') 44 | ->has('latest_date') 45 | ->has('earliest_date') 46 | ->has('total_positive') 47 | ->has('total_negative') 48 | ->has('total_invalid') 49 | ) 50 | ->etc() 51 | ); 52 | } 53 | 54 | /** 55 | * reports 56 | */ 57 | public function test_reports() 58 | { 59 | $response = $this->json('GET', '/rapid-tests/report'); 60 | $response->assertJson(fn (AssertableJson $obj) => 61 | $obj->has('province') 62 | ->has('last_updated') 63 | ->has('data') 64 | ->has('data.0', fn ($obj) => 65 | $obj->has('date') 66 | ->has('positive') 67 | ->has('negative') 68 | ->has('invalid') 69 | ) 70 | ); 71 | } 72 | 73 | /** 74 | * reports by province 75 | */ 76 | public function test_reports_by_province() 77 | { 78 | $province = RapidTestReport::inRandomOrder()->first()->province; 79 | $response = $this->json('GET', '/rapid-tests/report/province/'.$province); 80 | $response->assertJson(fn (AssertableJson $obj) => 81 | $obj->where('province', $province) 82 | ->has('last_updated') 83 | ->has('data') 84 | ->has('data.0', fn ($obj) => 85 | $obj->has('date') 86 | ->has('positive') 87 | ->has('negative') 88 | ->has('invalid') 89 | ) 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | 'block_for' => 0, 50 | ], 51 | 52 | 'sqs' => [ 53 | 'driver' => 'sqs', 54 | 'key' => env('AWS_ACCESS_KEY_ID'), 55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 58 | 'suffix' => env('SQS_SUFFIX'), 59 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 60 | ], 61 | 62 | 'redis' => [ 63 | 'driver' => 'redis', 64 | 'connection' => 'default', 65 | 'queue' => env('REDIS_QUEUE', 'default'), 66 | 'retry_after' => 90, 67 | 'block_for' => null, 68 | ], 69 | 70 | ], 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Failed Queue Jobs 75 | |-------------------------------------------------------------------------- 76 | | 77 | | These options configure the behavior of failed queue job logging so you 78 | | can control which database and table are used to store the jobs that 79 | | have failed. You may change them to any database / table you wish. 80 | | 81 | */ 82 | 83 | 'failed' => [ 84 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 85 | 'database' => env('DB_CONNECTION', 'mysql'), 86 | 'table' => 'failed_jobs', 87 | ], 88 | 89 | ]; 90 | -------------------------------------------------------------------------------- /app/ModularReport.php: -------------------------------------------------------------------------------- 1 | getTable(); 64 | } 65 | 66 | /** 67 | * helper to pull latest records for all unique provinces 68 | */ 69 | public static function latest() { 70 | $table = static::getTableName(); 71 | 72 | $select_core = array_merge( 73 | ['t1.province', 'date'], 74 | static::allAttrs() 75 | ); 76 | 77 | $select_stmt = implode( ",", $select_core ); 78 | 79 | $query = "SELECT {$select_stmt} FROM {$table} t1 JOIN (SELECT province, MAX(`date`) as latest_date FROM {$table} group by `province`) t2 ON t1.province = t2.province AND t1.date = t2.latest_date"; 80 | 81 | $report = DB::select($query); 82 | 83 | $response = [ 84 | 'data' => $report, 85 | ]; 86 | 87 | return $response; 88 | } 89 | 90 | /** 91 | * helper to pull the latest record for the specified province 92 | */ 93 | public static function latestByProvince( $province ) { 94 | $report = static::select( 95 | array_merge(['province', 'date'], static::allAttrs()) 96 | )->where('province', $province) 97 | ->latest('date') 98 | ->first(); 99 | 100 | $response = [ 101 | 'data' => $report, 102 | ]; 103 | 104 | return $response; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /resources/views/vendor/larecipe/partials/nav.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 71 |
-------------------------------------------------------------------------------- /tests/Feature/SrReportTest.php: -------------------------------------------------------------------------------- 1 | json('GET', '/reports/sub-regions/summary'); 19 | $response->assertJson(fn (AssertableJson $json) => 20 | $json->has('last_updated') 21 | ->has('data') 22 | ->has('data.0', fn ($json) => 23 | $json->has('code') 24 | ->has('date') 25 | ->has('total_dose_1') 26 | ->has('percent_dose_1') 27 | ->has('source_dose_1') 28 | ->has('total_dose_2') 29 | ->has('percent_dose_2') 30 | ->has('source_dose_2') 31 | ->has('total_dose_3') 32 | ->has('percent_dose_3') 33 | ->has('source_dose_3') 34 | ) 35 | ->etc() 36 | ); 37 | } 38 | 39 | /** 40 | * base recent sr vaccine reports 41 | */ 42 | public function test_recent_reports() 43 | { 44 | $response = $this->json('GET', '/reports/sub-regions/recent'); 45 | $response->assertJson(fn (AssertableJson $json) => 46 | $json->has('last_updated') 47 | ->has('recent_from') 48 | ->has('data') 49 | ->has('data.'.rand(1,10), fn ($json) => 50 | $json->has('code') 51 | ->has('date') 52 | ->has('total_dose_1') 53 | ->has('percent_dose_1') 54 | ->has('source_dose_1') 55 | ->has('total_dose_2') 56 | ->has('percent_dose_2') 57 | ->has('source_dose_2') 58 | ->has('total_dose_3') 59 | ->has('percent_dose_3') 60 | ->has('source_dose_3') 61 | ) 62 | ->etc() 63 | ); 64 | } 65 | 66 | /** 67 | * base sr vaccine report for a specific sub region 68 | */ 69 | public function test_specific_sub_region_report() 70 | { 71 | $response = $this->json('GET', '/reports/sub-regions/ab123'); 72 | $response->assertJson(fn (AssertableJson $json) => 73 | $json->has('sub_region') 74 | ->has('data') 75 | ->has('data.0', fn ($json) => 76 | $json->has('date') 77 | ->has('total_dose_1') 78 | ->has('percent_dose_1') 79 | ->has('source_dose_1') 80 | ->has('total_dose_2') 81 | ->has('percent_dose_2') 82 | ->has('source_dose_2') 83 | ->has('total_dose_3') 84 | ->has('percent_dose_3') 85 | ->has('source_dose_3') 86 | ) 87 | ->etc() 88 | ); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Log Channels 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may configure the log channels for your application. Out of 28 | | the box, Laravel uses the Monolog PHP logging library. This gives 29 | | you a variety of powerful log handlers / formatters to utilize. 30 | | 31 | | Available Drivers: "single", "daily", "slack", "syslog", 32 | | "errorlog", "monolog", 33 | | "custom", "stack" 34 | | 35 | */ 36 | 37 | 'channels' => [ 38 | 'stack' => [ 39 | 'driver' => 'stack', 40 | 'channels' => ['single'], 41 | 'ignore_exceptions' => false, 42 | ], 43 | 44 | 'single' => [ 45 | 'driver' => 'single', 46 | 'path' => storage_path('logs/laravel.log'), 47 | 'level' => 'debug', 48 | ], 49 | 50 | 'daily' => [ 51 | 'driver' => 'daily', 52 | 'path' => storage_path('logs/laravel.log'), 53 | 'level' => 'debug', 54 | 'days' => 14, 55 | ], 56 | 57 | 'slack' => [ 58 | 'driver' => 'slack', 59 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 60 | 'username' => 'Laravel Log', 61 | 'emoji' => ':boom:', 62 | 'level' => 'critical', 63 | ], 64 | 65 | 'papertrail' => [ 66 | 'driver' => 'monolog', 67 | 'level' => 'debug', 68 | 'handler' => SyslogUdpHandler::class, 69 | 'handler_with' => [ 70 | 'host' => env('PAPERTRAIL_URL'), 71 | 'port' => env('PAPERTRAIL_PORT'), 72 | ], 73 | ], 74 | 75 | 'stderr' => [ 76 | 'driver' => 'monolog', 77 | 'handler' => StreamHandler::class, 78 | 'formatter' => env('LOG_STDERR_FORMATTER'), 79 | 'with' => [ 80 | 'stream' => 'php://stderr', 81 | ], 82 | ], 83 | 84 | 'syslog' => [ 85 | 'driver' => 'syslog', 86 | 'level' => 'debug', 87 | ], 88 | 89 | 'errorlog' => [ 90 | 'driver' => 'errorlog', 91 | 'level' => 'debug', 92 | ], 93 | 94 | 'null' => [ 95 | 'driver' => 'monolog', 96 | 'handler' => NullHandler::class, 97 | ], 98 | 99 | 'emergency' => [ 100 | 'path' => storage_path('logs/laravel.log'), 101 | ], 102 | ], 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /app/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | proxy = $proxy; 17 | } 18 | 19 | public function register() 20 | { 21 | $this->validate(request(), [ 22 | 'name' => 'required', 23 | 'email' => 'required|email', 24 | 'password' => 'required', 25 | ]); 26 | 27 | $user = User::create([ 28 | 'name' => request('name'), 29 | 'email' => request('email'), 30 | 'password' => bcrypt(request('password')), 31 | ]); 32 | 33 | $resp = $this->proxy->grantPasswordToken( 34 | $user->email, 35 | request('password') 36 | ); 37 | 38 | return response([ 39 | 'token' => $resp->access_token, 40 | 'expiresIn' => $resp->expires_in, 41 | 'message' => 'Your account has been created', 42 | ], 201); 43 | } 44 | 45 | public function login() 46 | { 47 | $user = User::where('email', request('email'))->with(['roles', 'provinces'])->first(); 48 | 49 | abort_unless($user, 404, 'This combination does not exists.'); 50 | abort_unless( 51 | \Hash::check(request('password'), $user->password), 52 | 403, 53 | 'This combination does not exists.' 54 | ); 55 | 56 | $resp = $this->proxy 57 | ->grantPasswordToken(request('email'), request('password')); 58 | 59 | // single-role mode 60 | $user->role = $user->roles->pluck('name')[0]; 61 | 62 | return response([ 63 | 'token' => $resp->access_token, 64 | 'refresh_token' => $resp->refresh_token, 65 | 'expiresIn' => $resp->expires_in, 66 | 'message' => 'You have been logged in', 67 | 'user' => $user, 68 | ], 200); 69 | } 70 | 71 | public function user() 72 | { 73 | $user = auth()->guard('api')->user(); 74 | $user->load(['roles', 'provinces']); 75 | // single-role mode 76 | $user->role = $user->roles->pluck('name')[0]; 77 | return response([ 78 | 'user' => $user, 79 | ], 200); 80 | } 81 | 82 | public function refreshToken() 83 | { 84 | $resp = $this->proxy->refreshAccessToken(); 85 | 86 | if( isset($resp->error) ) { 87 | abort( 403, $resp->error_description ); 88 | } 89 | 90 | return response([ 91 | 'token' => $resp->access_token, 92 | 'refresh_token' => $resp->refresh_token, 93 | 'expiresIn' => $resp->expires_in, 94 | 'message' => 'Token has been refreshed.', 95 | ], 200); 96 | } 97 | 98 | public function logout() 99 | { 100 | $token = request()->user()->token(); 101 | $token->delete(); 102 | 103 | // remove the httponly cookie 104 | cookie()->queue(cookie()->forget('refresh_token')); 105 | 106 | return response([ 107 | 'message' => 'You have been successfully logged out', 108 | ], 200); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel 8 | 9 | 10 | 11 | 12 | 13 | 65 | 66 | 67 |
68 | @if (Route::has('login')) 69 | 80 | @endif 81 | 82 |
83 |
84 | Laravel 85 |
86 | 87 | 97 |
98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /resources/views/vendor/larecipe/partials/logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | get(); 16 | } 17 | 18 | static function getUser( $id ) 19 | { 20 | $user = User::with('roles', 'provinces')->find($id); 21 | // 22 | if( !$user ) { 23 | return abort(400, "User does not exist"); 24 | } 25 | // block admin editing 26 | if( $user->hasRole('admin') ) { 27 | return abort(400, "Admins cannot be modified here"); 28 | } 29 | return $user; 30 | } 31 | 32 | /** 33 | * function to create editors 34 | */ 35 | static function createUser( Request $request ) 36 | { 37 | // default role 38 | $role = 'editor'; 39 | 40 | // 41 | if( !request('name') || !request('email') ) { 42 | abort(400, "Missing name / email"); 43 | } 44 | 45 | // 46 | if( strlen( request('password') ) < 12 ) { 47 | abort(400, "Password should be at least 12 characters"); 48 | } 49 | 50 | // quick validate email 51 | $user = User::where('email', '=', request('email'))->first(); 52 | if ($user !== null) { 53 | abort(400, "This email address is already registered"); 54 | } 55 | 56 | // base user object 57 | $user = User::create([ 58 | 'name' => request('name'), 59 | 'email' => request('email'), 60 | 'password' => Hash::make( request('password') ), 61 | ]); 62 | 63 | $user->save(); 64 | 65 | // sync province assignments (permissions) 66 | $user->provinces()->sync( request('provinces') ); 67 | 68 | // role 69 | $user->assignRole($role); 70 | 71 | return response([ 72 | 'message' => "User created", 73 | 'user' => $user, 74 | ], 200); 75 | } 76 | 77 | /** 78 | * function to create editors 79 | */ 80 | static function updateUser( $id, Request $request ) 81 | { 82 | $user = User::find($id); 83 | 84 | // check if user 85 | if( !$user ) { 86 | abort(400, "Invalid user"); 87 | } 88 | 89 | // check details 90 | if( !request('name') || !request('email') ) { 91 | abort(400, "Missing name / email"); 92 | } 93 | 94 | // 95 | if( request('password') && strlen( request('password') ) < 12 ) { 96 | abort(400, "Password should be at least 12 characters"); 97 | } 98 | 99 | // update user details 100 | $user->name = request('name'); 101 | $user->email = request('email'); 102 | $password_updated = false; 103 | if( request('password') ) { 104 | $user->password = Hash::make( request('password') ); 105 | $password_updated = true; 106 | } 107 | // save 108 | $user->save(); 109 | 110 | // sync province assignments (permissions) 111 | $user->provinces()->sync( request('provinces') ); 112 | 113 | return response([ 114 | 'message' => "User ${id} updated", 115 | 'passwordUpdated' => $password_updated, 116 | ], 200); 117 | 118 | } 119 | } 120 | --------------------------------------------------------------------------------