Skip to content

Commit

Permalink
Feature author expertise (#422)
Browse files Browse the repository at this point in the history
Creation of `expertise` model and `expertisable` relationship - expertises can now be added to the author profiles. Expertise used are from the sciences.gc.ca and RESE lists. Keeping only unique values and discarding the taxonomy of the science profiles which introduced too much redundancy, contradictions, as well as a few typos.
  • Loading branch information
vincentauger authored Oct 30, 2023
1 parent 6e7cd2e commit 00790e8
Show file tree
Hide file tree
Showing 62 changed files with 14,477 additions and 874 deletions.
66 changes: 44 additions & 22 deletions .phpstorm.meta.php

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Additional guidelines will be added here as we progress.
## Built With
This repo has both the front end and backend code. The front end is a SPA that consumes the API backend.
### Front-end Stack (Single Page Application)
- [TypeScript](https://www.typescriptlang.org/) (main framework)
- [TypeScript](https://www.typescriptlang.org/)
- [Vue.js](https://vuejs.org/) (TS with Composition API `script setup`)
- [Vue-i18n](https://vue-i18n.intlify.dev/) (app supports en-CA and fr-CA, with vite-plugin-vue-i18n, `globalInjection: true`)
- [Vue-Router](https://router.vuejs.org/) (SPA routing)
Expand Down
385 changes: 282 additions & 103 deletions _ide_helper.php

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions app/Actions/Expertise/SyncExpertiseWithScience.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

namespace App\Actions\Expertise;

use App\Models\Expertise;
use Illuminate\Support\Facades\Http;

class SyncExpertiseWithScience
{
public static function handle(): bool
{
$url = 'https://profils-profiles.science.gc.ca/api/views/admin_taxonomy_term?display_id=services_1';

$response = Http::get($url);
if (! $response->ok()) {
return false;
}

$expertises = collect($response->json());

// lower case array of keywords to remove in english
$termsToRemove = [
'bio-informatics',
'biological',
];

// Taken from RESE guidance - retreived on: 2023-10-27
$reseExperties = [
['Aquaculture', "L'aquaculture"],
['Finfish', 'Poisson à nageoires'],
['Pathogens', 'Les agents pathogènes'],
['Shellfish', 'Mollusques et crustacés'],
['Wild / Cultured interactions', 'Interactions entre les espèces sauvages et les espèces cultivées'],
['Biology and Ecology', 'Biologie et écologie'],
['Benthic Ecology', 'Écologie benthique'],
['Fish Biology', 'Biologie des poissons'],
['Invertebrate Biology', 'Biologie des invertébrés'],
['Harmful Algal Blooms', 'Efflorescences algales nuisibles'],
['Aquatic Invasive Species', 'Espèces aquatiques envahissantes'],
['Marine Mammal Biology', 'Biologie des mammifères marins'],
['Freshwater Ecology', 'Écologie des eaux douces'],
['Ecosystems Features', 'Caractéristiques des écosystèmes'],
['Marine Protected Areas', 'Zones marines protégées'],
['Cold-water corals and sponges', "Coraux et éponges d'eau froide"],
['Hydrothermal vents', 'Foyers hydrothermaux'],
['Seamounts', 'Monts sous-marins'],
['Eelgrass', 'La zostère'],
['Fisheries and Stock Groups', 'Pêcheries et groupes de stocks'],
['Management Strategies', 'Stratégies de gestion'],
['Stock Assessments', 'Évaluation des stocks'],
['Crustaceans', 'Crustacés'],
['Groundfish', 'Poissons de fond'],
['Large pelagics ', 'Grands pélagiques '],
['Other Stock Groups', "Autres groupes d'actions"],
['Salmonids', 'Salmonidés'],
['Small pelagics', 'Petits pélagiques'],
['Survey design', "Conception de l'enquête"],
['Marine Mammals', 'Mammifères marins'],
['Mollusks', 'Mollusques'],
['Genetics', 'Génétique'],
['Designatable Units', 'Unités désignables'],
['Genomics', 'Génomique'],
['Introgression', 'Introgression'],
['Stock Structure', 'Structure du stock'],
['Habitat', 'Habitat'],
['Estuarine', 'Estuaire'],
['Freshwater', 'Eau douce'],
['Marine', 'Marine'],
['Modeling, Statistics and Bioinformatics', 'Modélisation, statistiques et bioinformatique'],
['Bioinformatics', 'Bioinformatique'],
['Current modeling', 'Modélisation des courants'],
['Fisheries modeling', 'Modélisation de la pêche'],
['Oceans modeling', 'Modélisation des océans'],
['Quantitative modeling', 'Modélisation quantitative'],
['Spatial modeling', 'Modélisation spatiale'],
['Spatiotemporal statistics', 'Statistiques spatio-temporelles'],
['Oceanography', 'Océanographie'],
['Biological oceanography', 'Océanographie biologique'],
['Ocean chemistry', 'Chimie des océans'],
['Ocean monitoring', 'Surveillance des océans'],
['Physical chemistry', 'Chimie physique'],
];

// add rese experties to the Science expertise list
foreach ($reseExperties as $expertise) {
$expertises->push([
'name_en' => $expertise[0],
'name_fr' => $expertise[1],
]);
}

$unique = $expertises
->unique(function ($expertise) {
return strtolower($expertise['name_en']);
})
->unique(function ($expertise) {
return strtolower($expertise['name_fr']);
})
->filter(function ($expertise) use ($termsToRemove) {
$a = strtolower($expertise['name_en']);

return ! in_array($a, $termsToRemove);
});

foreach ($unique as $expertise) {
Expertise::updateOrCreate(
[
'name_en' => html_entity_decode($expertise['name_en']),
],
[
'name_fr' => html_entity_decode($expertise['name_fr']),
]
);
}

return true;
}
}
49 changes: 29 additions & 20 deletions app/Actions/ROR/DownloadLatestRORData.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
namespace App\Actions\ROR;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Process;

class DownloadLatestRORData
{
public static function handle(callable $progressCallable = null): array | null
public static function handle(callable $progressCallable = null): ?array
{
$url = 'https://zenodo.org/api/records';

Expand All @@ -24,42 +24,51 @@ public static function handle(callable $progressCallable = null): array | null
$version = $data['hits']['hits'][0]['metadata']['version'] ?? null;

// Download files to temporary storage
if(!$url) return null;
if (! $url) {
return null;
}

// create a path to store the file
$base_path = Storage::path('ror_data');

// make sure the path exists
if(!file_exists($base_path)) mkdir($base_path);
if (! file_exists($base_path)) {
mkdir($base_path);
}

$fileName = Str::afterLast($url, '/');
$zipFile = $base_path . '/' . $fileName;
$zipFile = $base_path.'/'.$fileName;

// does the file already exist?
if(!file_exists($zipFile)){
if (! file_exists($zipFile)) {
// use curl to download file
$command = "curl -o $zipFile $url";

$update = is_callable($progressCallable);

if($update) $progressCallable("Staring ROR download: ". $command . "...".PHP_EOL);
$process = Process::timeout(120)->start($command, function($type, $buffer) use ($progressCallable) {
if($type === 'stderr') {
$progressCallable("ERROR: " . $buffer);
if ($update) {
$progressCallable('Staring ROR download: '.$command.'...'.PHP_EOL);
}
$process = Process::timeout(120)->start($command, function ($type, $buffer) use ($progressCallable) {
if ($type === 'stderr') {
$progressCallable('ERROR: '.$buffer);
} else {
$progressCallable($buffer);
}
});


if(!$process->wait()->successful()) return null;
if($update) $progressCallable("Finished download: ". $zipFile.PHP_EOL);
if (! $process->wait()->successful()) {
return null;
}
if ($update) {
$progressCallable('Finished download: '.$zipFile.PHP_EOL);
}
}

// is Json file already extracted?
$jsonFile = $base_path . '/' . Str::beforeLast($fileName, '.') .'.json';
$jsonFile = $base_path.'/'.Str::beforeLast($fileName, '.').'.json';

if(file_exists($jsonFile)){
if (file_exists($jsonFile)) {
return [
'jsonFile' => $jsonFile,
'version' => $version,
Expand All @@ -68,15 +77,15 @@ public static function handle(callable $progressCallable = null): array | null

$command = "unzip -o $fileName";

$result = Process::path($base_path)->start($command, function($type, $buffer) use ($progressCallable) {
if($type === 'stderr') {
$progressCallable("ERROR: " . $buffer);
$result = Process::path($base_path)->start($command, function ($type, $buffer) use ($progressCallable) {
if ($type === 'stderr') {
$progressCallable('ERROR: '.$buffer);
} else {
$progressCallable($buffer);
}
});

if($result->wait()->successful()){
if ($result->wait()->successful()) {
return [
'jsonFile' => $jsonFile,
'version' => $version,
Expand All @@ -85,4 +94,4 @@ public static function handle(callable $progressCallable = null): array | null

return null;
}
}
}
40 changes: 25 additions & 15 deletions app/Actions/ROR/SyncRORData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@

class SyncRORData
{

public static function handle(string $jsonFilePath, string $version, callable $progressCallback = null): void
{
if(!file_exists($jsonFilePath)) {
throw new \Exception('File not found: ' . $jsonFilePath);
if (! file_exists($jsonFilePath)) {
throw new \Exception('File not found: '.$jsonFilePath);
}

$json = JsonParser::parse($jsonFilePath);
Expand All @@ -27,17 +26,23 @@ public static function handle(string $jsonFilePath, string $version, callable $p

$update = is_callable($progressCallback);

foreach($json as $key => $record){
foreach ($json as $key => $record) {

// we only want to import "active" records
if($record['status'] !== 'active') continue;
if ($record['status'] !== 'active') {
continue;
}
// only import records with a country code we want
if(!$countryCodesToImport->contains($record['country']['country_code'])) continue;
if (! $countryCodesToImport->contains($record['country']['country_code'])) {
continue;
}

// update or create the organization
self::updateOrCreateOrganization($record, $version);

if($update) $progressCallback($json->progress()->percentage());
if ($update) {
$progressCallback($json->progress()->percentage());
}

}
}
Expand All @@ -48,7 +53,6 @@ public static function handle(string $jsonFilePath, string $version, callable $p
*
* We will update this routine when ROR updates their schema to 2.0 in late 2023.
*
* @param array $record
* @return void
*/
private static function updateOrCreateOrganization(array $record, string $version)
Expand All @@ -59,20 +63,26 @@ private static function updateOrCreateOrganization(array $record, string $versio
$name_fr = null;
$name_en = null;

collect($record['labels'])->each(function($label) use (&$name_fr, &$name_en){
collect($record['labels'])->each(function ($label) use (&$name_fr, &$name_en) {
$iso639 = $label['iso639'] ?? null;
if(!$iso639) return;
if (! $iso639) {
return;
}

if($iso639 === 'fr') {
if ($iso639 === 'fr') {
$name_fr = $label['label'];
}
if($iso639 === 'en') {
if ($iso639 === 'en') {
$name_en = $label['label'];
}
});

if(!$name_en) $name_en = $record['name'];
if(!$name_fr) $name_fr = $record['name'];
if (! $name_en) {
$name_en = $record['name'];
}
if (! $name_fr) {
$name_fr = $record['name'];
}

// acronyms - no i18n available at this time. Assume EN first.
$abbr_en = $record['acronyms'][0] ?? null;
Expand Down Expand Up @@ -103,4 +113,4 @@ private static function updateOrCreateOrganization(array $record, string $versio
);

}
}
}
40 changes: 40 additions & 0 deletions app/Console/Commands/SyncExpertises.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SyncExpertises extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'osp:sync-expertises';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync the local database with the expertise taxonomy from the profiles registry at profils-profiles.science.gc.ca';

/**
* Execute the console command.
*/
public function handle()
{
$this->info('Syncing expertises...');

$result = \App\Actions\Expertise\SyncExpertiseWithScience::handle();

if (! $result) {
$this->error('Failed to sync expertises!');

return;
}

$this->info('Expertises synced!');
}
}
11 changes: 6 additions & 5 deletions app/Console/Commands/UpdateROROrganizations.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ class UpdateROROrganizations extends Command
*/
public function handle()
{
$this->info("Downloading or looking for ROR data dump...");
$this->info('Downloading or looking for ROR data dump...');
$rorPath = DownloadLatestRORData::handle(function ($message) {
$this->output->write($message);
});
if(!$rorPath){
$this->error("Unable to download ROR data dump");
if (! $rorPath) {
$this->error('Unable to download ROR data dump');

return;
}
$this->info("Found and uncompressed here: " . $rorPath['jsonFile']);
$this->info("Synchronizing ROR data with Organizations...");
$this->info('Found and uncompressed here: '.$rorPath['jsonFile']);
$this->info('Synchronizing ROR data with Organizations...');
$progressBar = $this->output->createProgressBar(100);
SyncRORData::handle(
$rorPath['jsonFile'],
Expand Down
Loading

0 comments on commit 00790e8

Please sign in to comment.