diff --git a/app/Helpers/Publication.php b/app/Helpers/Publication.php new file mode 100644 index 00000000..8798b711 --- /dev/null +++ b/app/Helpers/Publication.php @@ -0,0 +1,213 @@ + ['last_name', 'comma', 'space', 'first_initial', 'space', 'middle_initial'], + //'MLA' => ['last_name', 'comma', 'space', 'first_name'], // Example ONLY, for testing purposes + //'Chicago' => ['first_name', 'space', 'last_name'], // Example ONLY, for testing purposes + ]; + + /** @var array */ + const REGEX_PATTERNS = [ + 'APA' => "/^[A-Za-z][\s\w\p{L}\p{M}áéíóúüññÑ'-]+,\s[A-Z][\.\s\b][\s]?[A-Z]?[\.\s\b]?$/", + 'last_name_initials' => "/^[A-Za-z][\s\w\p{L}\p{M}áéíóúüññÑ'-]+,?\s[A-Z]{1,2}\b$/", + 'first_name_last_name' => "/^[A-Za-z][\s\w\p{L}\p{M}áéíóúüññÑ'-]+\s[A-Za-z][\s\w\p{L}\p{M}áéíóúüññÑ'-]+$/", + // 'MLA' => "/^[\p{L}ñÑ'., -]+, [\p{Lu}ñÑ]\. ?[\p{Lu}ñÑ]?\.?$/", // Example ONLY, for testing purposes + // 'Chicago' => "/^[\p{L}ñÑ'., -]+, [\p{Lu}ñÑ]\. ?[\p{Lu}ñÑ]?\.?$/", // Example ONLY, for testing purposes + ]; + + /** @var array */ + const SEPARATORS = [ + 'comma' => ',', + 'ellipsis' => '...', + 'ampersand' => '&', + 'space' => ' ', + 'period' => '.', + ]; + + public static function citationFormats(): array + { + return static::CITATION_FORMAT_AUTHOR_NAMES; + } + + public static function citationFormatRegex(): array + { + return static::REGEX_PATTERNS; + } + + public static function matchesRegexPattern($citation_format, $formatted_author_name) : int + { + return preg_match(static::citationFormatRegex()[$citation_format], $formatted_author_name); + } + + public static function firstName(array $author_name_array, $pattern = null) : string + { + if (!is_null($pattern) && $pattern === 'last_name_initials') { + return $author_name_array[1]; + } + return $author_name_array[0]; + } + + public static function lastName(array $author_name_array, $pattern = null) : string + { + if (!is_null($pattern) && $pattern === 'last_name_initials') { + return $author_name_array[0]; + } + + if (count($author_name_array) == 3) { + return $author_name_array[2]; + } + return Arr::last($author_name_array); + } + + public static function middleName(array $author_name_array, $pattern = null) : string + { + if (!is_null($pattern) && $pattern === 'last_name_initials') { + return strlen($author_name_array[1]) == 2 ? $author_name_array[1][1] : ''; + } + + if (count($author_name_array) == 3) { + return $author_name_array[1]; + } + return ''; + } + + public static function initial(string $name) : string + { + return strlen($name) > 0 ? "{$name[0]}." : ''; + } + + /** + * Return a string with the authors names in APA format + * @param array $authors + * @return string + */ + public static function formatAuthorsApa(array $authors) : string + { + $authors = static::formatAuthorsNames($authors); + + $string_authors_names = ""; + $greater_than_20 = false; + $authors_count = count($authors); + + if ($authors_count > 1) { + $last = $authors[$authors_count - 1]; + + if ($authors_count >= 20) { + $greater_than_20 = true; + array_splice($authors, 20); + } + else { + array_splice($authors, $authors_count - 1); + } + + foreach ($authors as $key => $author) { + $string_authors_names = "{$string_authors_names} {$author['APA']}"; + + if ($key < count($authors) - 1) { + $string_authors_names = $string_authors_names . static::SEPARATORS['comma'] . static::SEPARATORS['space']; + } + else { + if ($greater_than_20) { + $string_authors_names = $string_authors_names . static::SEPARATORS['space'] . static::SEPARATORS['ellipsis'] . static::SEPARATORS['space']; + } + else { + $string_authors_names = $string_authors_names . static::SEPARATORS['space'] . static::SEPARATORS['ampersand'] . static::SEPARATORS['space']; + } + $string_authors_names = "{$string_authors_names} {$last['APA']}"; + } + } + } + else { + $string_authors_names = $authors[0]['APA']; + } + + return $string_authors_names; + } + + /** + * Return a string with the authors names in MLA format + * @param array $authors + */ + public static function formatAuthorsMla(array $authors) + { + + } + + /** + * Return a string with the authors names in Chicago format + * @param array $authors + */ + public static function formatAuthorsChicago(array $authors) + { + + } + + /** + * Receive an array of author names, assuming each name is either already formatted or in the form of First Name Middle initial. Last Name + * Return an array formatted author name for each citation format + * + * @param $author_names + * @return array + */ + public static function formatAuthorsNames(array $author_names): array + { + /** @var array */ + $formatted_author_names = []; + + foreach ($author_names as $author_name) { + $raw_author_name = trim($author_name); + + foreach (array_keys(static::citationFormats()) as $key => $citation_format) { + //If matches given citation format pattern + if (static::matchesRegexPattern($citation_format, $raw_author_name)) { + $formatted_author_name[$citation_format] = ucwords($raw_author_name); + } //If matches last name first initial middle initial pattern + elseif (static::matchesRegexPattern('last_name_initials', $raw_author_name)) { + $formatted_author_name[$citation_format] = static::formatAuthorName($citation_format, $author_name, 'last_name_initials'); + } + else { //If matches any other pattern, it will use the first name last name pattern by default to format the name + $formatted_author_name[$citation_format] = static::formatAuthorName($citation_format, $author_name); + + } + } + + $formatted_author_names[] = $formatted_author_name; + } + return $formatted_author_names; + } + + /** + * Format an author name according to a given citation format + * + * @param $citation_format + * @param $full_name + * @return string + */ + public static function formatAuthorName($citation_format, $full_name, $pattern = null) : string + { + $result = ''; + $format_components = static::citationFormats()[$citation_format]; + $full_name_array = explode(" ", $full_name); + $first_name = static::firstName($full_name_array, $pattern); + $middle_name = static::middleName($full_name_array, $pattern); + $last_name = static::lastName($full_name_array, $pattern); + $first_initial = static::initial($first_name); + $middle_initial = static::initial($middle_name); + $comma = static::SEPARATORS['comma']; + $space = static::SEPARATORS['space']; + + foreach ($format_components as $key => $value) { + $result = $result . array_values(compact($value))[0]; + } + + return trim($result); + } + +} diff --git a/app/Http/Controllers/ProfilesController.php b/app/Http/Controllers/ProfilesController.php index ba7d1b9f..fb7e44fa 100644 --- a/app/Http/Controllers/ProfilesController.php +++ b/app/Http/Controllers/ProfilesController.php @@ -225,12 +225,13 @@ public function create(Request $request, User $user, LdapHelperContract $ldap): */ public function edit(Profile $profile, string $section): View|ViewContract|RedirectResponse { - //dont manage auto-managed publications + //If auto-managed publications if ($section == 'publications' && $profile->hasOrcidManagedPublications()) { - $profile->updateORCID(); - return redirect() - ->route('profiles.show', $profile->slug) - ->with('flash_message', 'Publications updated via ORCID.'); + if ($profile->updateORCID()) { + return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Publications updated via ORCID.'); + } else { + return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Error updating your ORCID publications.'); + } } $data = $profile->data()->$section()->get(); @@ -251,11 +252,11 @@ public function edit(Profile $profile, string $section): View|ViewContract|Redir */ public function orcid(Profile $profile): RedirectResponse { - if ($profile->updateORCID()) { - return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Publications updated via ORCID.'); - } else { - return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Error updating your ORCID publications.'); - } + if ($profile->updateORCID()) { + return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Publications updated via ORCID.'); + } else { + return redirect()->route('profiles.show', $profile->slug)->with('flash_message', 'Error updating your ORCID publications.'); + } } /** diff --git a/app/Profile.php b/app/Profile.php index 04f34c7b..fcd88bec 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -4,6 +4,7 @@ use App\ProfileData; use App\ProfileStudent; +use App\Repositories\OrcidPublicationsRepository; use App\Student; use App\User; use Illuminate\Database\Eloquent\Model; @@ -18,6 +19,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; /** @@ -58,6 +60,8 @@ class Profile extends Model implements HasMedia, Auditable */ protected $casts = [ 'public' => 'boolean', + 'contributors' => 'array', + 'authors' => 'array', ]; /** @@ -173,64 +177,13 @@ public function hasOrcidManagedPublications() public function updateORCID() { - $orc_id = $this->information()->get(array('data'))->toArray()[0]['data']['orc_id']; - - if(is_null($orc_id)){ - //can't update if we don't know your ID - return false; - } - - $orc_url = "https://pub.orcid.org/v2.0/" . $orc_id . "/activities"; - - $client = new Client(); - - $res = $client->get($orc_url, [ - 'headers' => [ - 'Authorization' => 'Bearer ' . config('ORCID_TOKEN'), - 'Accept' => 'application/json' - ], - 'http_errors' => false, // don't throw exceptions for 4xx,5xx responses - ]); - - //an error of some sort - if($res->getStatusCode() != 200){ - return false; - } - - $datum = json_decode($res->getBody()->getContents(), true); - - foreach($datum['works']['group'] as $record){ - $url = NULL; - foreach($record['external-ids']['external-id'] as $ref){ - if($ref['external-id-type'] == "eid"){ - $url = "https://www.scopus.com/record/display.uri?origin=resultslist&eid=" . $ref['external-id-value']; - } - else if($ref['external-id-type'] == "doi"){ - $url = "http://doi.org/" . $ref['external-id-value']; - } - } - $record = ProfileData::firstOrCreate([ - 'profile_id' => $this->id, - 'type' => 'publications', - 'data->title' => $record['work-summary'][0]['title']['title']['value'], - 'sort_order' => $record['work-summary'][0]['publication-date']['year']['value'] ?? null, - ],[ - 'data' => [ - 'url' => $url, - 'title' => $record['work-summary'][0]['title']['title']['value'], - 'year' => $record['work-summary'][0]['publication-date']['year']['value'] ?? null, - 'type' => ucwords(strtolower(str_replace('_', ' ', $record['work-summary'][0]['type']))), - 'status' => 'Published' - ], - ]); - } - - Cache::tags(['profile_data'])->flush(); - - //ran through process successfully - return true; + $publications_manager = app()->make(OrcidPublicationsRepository::class); + $publications_manager->setProfile($this); + + return $publications_manager->syncPublications(); } + public function updateDatum($section, $request) { $sort_order = count($request->data ?? []) + 1; @@ -375,6 +328,21 @@ protected function registerImageThumbnails(Media $media = null, $name, $width, $ // Query Scopes // ////////////////// + /** + * Profiles with orcid sync on + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + * + */ + public function scopeWithOrcidSyncOn($query) : Builder { + return $query->whereHas('data', function ($data) { + $data + ->where('type', 'information') + ->where('data', 'like', '%"orc_id_managed": "1"%'); + }); + } + /** * Query scope for public Profiles * @@ -596,6 +564,16 @@ public function getApiUrlAttribute() return route('api.index', ['person' => $this->slug, 'with_data' => true]); } + /** + * Get the profile ORCID ID + */ + public function getOrcidAttribute() + { + $orc_id = $this->information()->get(array('data'))->toArray()[0]['data']['orc_id']; + + return $orc_id ?? null; + } + /////////////// // Relations // /////////////// diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 00000000..68206ae6 --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,34 @@ +app->bind(OrcidPublicationsRepository::class, function($app) { + return new OrcidPublicationsRepository($app[Profile::class]); + }); + $this->app->bind(PublicationsRepositoryContract::class, OrcidPublicationsRepository::class); + } +} diff --git a/app/Repositories/Contracts/PublicationsRepositoryContract.php b/app/Repositories/Contracts/PublicationsRepositoryContract.php new file mode 100644 index 00000000..c84d4545 --- /dev/null +++ b/app/Repositories/Contracts/PublicationsRepositoryContract.php @@ -0,0 +1,75 @@ +|false + */ + public function getPublications() : Collection|false|null; + + /** + * Cache publications for the current profile + * @return Collection + */ + public function getCachedPublications() : Collection; + + /** + * Sync a collection of ProfileData publications + * @return bool + */ + public function syncPublications() : bool; + + /** + * Get additional publications codes/identifiers from the API + * @return array + */ + public function getPublicationsCodes(string $url) : array; + + /** + * Get a publication external/additional references + * @return array + */ + public function getPublicationReferences(array $record) : array; + + /** + * Return the publication contributors names in an array + * @param array $record + * @return array + */ + public function getPublicationAuthors(array $record, string $default_author_name) : array; + + /** + * Make a get request to the API, returns false if the response returns an error + * @param string $url + * @return array|false + */ + public function sendRequest(string $url) : array|false; + + /** + * Return the service provider http client + */ + public function getHttpClient() : Client; + +} \ No newline at end of file diff --git a/app/Repositories/OrcidPublicationsRepository.php b/app/Repositories/OrcidPublicationsRepository.php new file mode 100644 index 00000000..9713a819 --- /dev/null +++ b/app/Repositories/OrcidPublicationsRepository.php @@ -0,0 +1,251 @@ +client = New Client(); + } + + /** + * Receive an attribute to get from the API the identifier necessary to retrieve the publications + * @param $faculty_id + * @return false + */ + public function getPersonId($faculty_id = null) : string|false + { + return false; + } + + /** + * Get the publications from the Orcid API to return a collection of ProfileData + * @return Collection|false|null + */ + public function getPublications() : Collection|false|null + { + /** @var Collection */ + $publications = collect(); + + $orc_id = $this->profile->orcid; + + if (is_null($orc_id)) { + return false; + } + + $putcodes = $this->getPublicationsCodes("https://pub.orcid.org/v2.0/" . $orc_id . "/works"); + + $split_putcodes = array_chunk($putcodes, 100); + + foreach ($split_putcodes as $putcodes_set) { + + $string_put_codes = implode(',', $putcodes_set); + + $putcodes_works_data = $this->sendRequest("https://pub.orcid.org/v2.0/$orc_id/works/$string_put_codes"); + + foreach ($putcodes_works_data['bulk'] as $record) { + + $authors = $this->getPublicationAuthors($record, $this->profile->full_name); + $references = $this->getPublicationReferences($record); + + $profile_data = new ProfileData([ + 'profile_id' => $this->profile->id, + 'type' => 'publications', + 'data->title' => $record['work']['title']['title']['value'], + 'sort_order' => $record['work']['publication-date']['year']['value'] ?? null, + 'data' => [ + 'put-code' => $record['work']['put-code'], + 'url' => $references['url'], + 'title' => $record['work']['title']['title']['value'], + 'year' => $record['work']['publication-date']['year']['value'] ?? null, + 'month' => $record['work']['publication-date']['month']['value'] ?? null, + 'day' => $record['work']['publication-date']['day']['value'] ?? null, + 'type' => ucwords(strtolower(str_replace('_', ' ', $record['work']['type']))), + 'journal_title' => $record['work']['journal-title']['value'] ?? null, + 'doi' => $references['doi'], + 'eid' => $references['eid'], + 'authors' => $authors, + 'authors_formatted' => [ + 'APA' => Publication::formatAuthorsApa($authors), + ], + 'status' => 'Published', + 'citation-type' => $record['work']['type'] ?? null, + 'citation-value' => $record['work']['value'] ?? null, + 'visibility' => $record['work']['visibility'], + ], + ]); + $publications->push($profile_data); + } + } + return $publications; + } + + /** + * Sync collection of ProfileData publications + * @return bool + */ + public function syncPublications() : bool + { + $publications = $this->getCachedPublications(); + + foreach ($publications as $publication) { + ProfileData::firstOrCreate([ + 'profile_id' => $publication->profile_id, + 'type' => 'publications', + 'data->title' => $publication->data['title'], + 'sort_order' => $publication->sort_order, + ], + [ + 'data' => [ + 'put-code' => $publication->data['put-code'], + 'url' => $publication->data['url'], + 'title' => $publication->data['title'], + 'year' => $publication->data['year'], + 'month' => $publication->data['month'], + 'day' => $publication->data['day'], + 'type' => $publication->data['type'], + 'journal_title' => $publication->data['journal_title'], + 'doi' => $publication->data['doi' ], + 'eid' => $publication->data['eid'], + 'authors' => $publication->data['authors'], + 'authors_formatted' => $publication->data['authors_formatted'], + 'status' => $publication->data['status'], + 'citation-type' => $publication->data['citation-type'], + 'citation-value' => $publication->data['citation-value'], + 'visibility' => $publication->data['visibility'], + ], + ]); + } + + Cache::tags(['profile_data'])->flush(); + + //ran through process successfully + return true; + } + + /** + * Cache publications for the current profile + * @return Collection + */ + public function getCachedPublications() : Collection + { + return Cache::remember( + "profile{$this->profile->id}-orcid-pubs", + 15 * 60, + fn() => $this->getPublications() + ); + } + + /** Make API request, return false if there's any error in the response + * @param string $url + * @return array|false + */ + public function sendRequest(string $url): array|false + { + $response = $this->client->get($url, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . config('ORCID_TOKEN'), + 'Accept' => 'application/json' + ], + 'http_errors' => false, // don't throw exceptions for 4xx,5xx responses + ]); + + if ($response->getStatusCode() != 200) { + return false; + } + + return json_decode($response->getBody()->getContents(), true); + } + + /** + * Return the service provider http client + */ + public function getHttpClient(): Client + { + return $this->client; + } + + /** + * Set profile property + */ + public function setProfile(Profile $profile) : void + { + $this->profile = $profile; + } + + /** + * Auxiliary method to obtain orcid publications putcodes + * + * @param string $url + * @return array + */ + public function getPublicationsCodes(string $url) : array + { + $all_works_data = $this->sendRequest($url); + + $grouped_works = collect($all_works_data['group'])->pluck('work-summary'); + + return $grouped_works->map(function ($item, $key) { + return collect($item)->sortByDesc('display-index')->value('put-code'); + })->toArray(); + } + + /** + * Return a orcid publication external references codes (doi and eid) + * and, if the pub url is not present, it takes it from either doi or eid + * + * @param array $record + * @return array + */ + public function getPublicationReferences(array $record) : array { + $url = $doi_url = $eid_url = null; + + foreach ($record['work']['external-ids']['external-id'] as $ref) { + if ($ref['external-id-type'] == "eid" && $ref['external-id-relationship'] === "SELF") { + $eid = $ref['external-id-value']; + $eid_url = "https://www.scopus.com/record/display.uri?origin=resultslist&eid=$eid"; + } + elseif ($ref['external-id-type'] == "doi" && $ref['external-id-relationship'] === "SELF") { + $doi = $ref['external-id-value']; + $doi_url = "http://doi.org/$doi"; + } + } + + $url = $record['work']['url']['value'] ?? ($doi_url ?? ($eid_url ?? null)); + + return [ + 'url' => $url, + 'doi' => $doi ?? null, + 'eid' => $eid ?? null, + ]; + } + + /** + * Return array with the publication contributors' names + * @param array $record + * @return array + */ + public function getPublicationAuthors(array $record, string $default_author_name) : array + { + $contributors = collect($record['work']['contributors']) + ->flatten(1) + ->map(fn($author) => $author['credit-name']['value']); + + /** @var array */ + $contributors_array = count($contributors) > 0 ? $contributors->toArray() : [$default_author_name]; + + return $contributors_array; + } +} \ No newline at end of file diff --git a/config/app.php b/config/app.php index fe3b8316..acd490f5 100644 --- a/config/app.php +++ b/config/app.php @@ -227,6 +227,7 @@ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\HelperServiceProvider::class, + App\Providers\RepositoryServiceProvider::class, App\Providers\RouteServiceProvider::class, ], diff --git a/database/factories/ProfileDataFactory.php b/database/factories/ProfileDataFactory.php index d2e83897..ea86a0c4 100644 --- a/database/factories/ProfileDataFactory.php +++ b/database/factories/ProfileDataFactory.php @@ -4,6 +4,7 @@ use App\Profile; use App\ProfileData; +use App\Helpers\Publication; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Arr; @@ -29,6 +30,33 @@ class ProfileDataFactory extends Factory 'Library', ]; + protected $existing_publications_data = + [ + 0 => [ + 'sort_order' => '2021', + 'title' => 'Existing Publication Title #1', + ], + 1 => [ + 'sort_order' => '2022', + 'title' => 'Existing Publication Title #2', + ], + 2 => [ + 'sort_order' => '2023', + 'title' => 'Existing Publication Title #3', + ], + ]; + + protected $authors_names_patterns = + [ + 'return ($this->faker->unique()->lastName() . ", " . strtoupper($this->faker->randomLetter()) . ". " . strtoupper($this->faker->randomLetter()) . ".");', + 'return ($this->faker->unique()->lastName() . ", " . strtoupper($this->faker->randomLetter()). ".");', + 'return ($this->faker->firstName() . " " . $this->faker->lastName());', + 'return ($this->faker->lastName() . " " . strtoupper($this->faker->randomLetter()) . strtoupper($this->faker->randomLetter()));', + 'return ($this->faker->lastName() . " " . strtoupper($this->faker->randomLetter()));', + ]; + + public int $authors_count = 5; + /** * Define the model's default state. * @@ -71,6 +99,7 @@ public function general() 'url' => $this->faker->url(), 'title' => $this->faker->sentence(), 'year' => $this->faker->year(), + 'authors_formatted' => ['APA' => $this->faker->paragraph()], ], ]; }); @@ -179,4 +208,75 @@ public function news() ]; }); } + + /** + * Data Type "publications" with pre-defined sort_order and title + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + public function existing_publication($type, $profile = null) + { + return $this + ->count(3) + ->state(['type' => 'publications']) + ->$type($profile) + ->sequence(function($sequence) { + return [ + 'sort_order' => $this->existing_publications_data[$sequence->index]['sort_order'], + 'data->title' => $this->existing_publications_data[$sequence->index]['title'], + ]; + }); + } + + /** + * Data Type "publications" sourced from the Orcid API. Formatted, and ready to sync with ProfileData + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + public function orcid_publication(Profile $profile = null) { + return $this->state(function (array $attributes) use ($profile) { + + $authors = $this->authorsNames(); + + return [ + 'profile_id' => $profile->id, + 'sort_order' => $this->faker->year(), + 'data' => [ + 'put-code' => $this->faker->numberBetween(99000,100000), + 'url' => $this->faker->url(), + 'title' => $this->faker->sentence(), + 'year' => $this->faker->year(), + 'type' => 'Journal', + 'month' => $this->faker->month(), + 'day' => $this->faker->dayOfMonth(), + 'journal_title' => $this->faker->sentence(), + 'doi' => $this->faker->regexify(config('app.DOI_REGEX')), + 'eid' => $this->faker->regexify(config('app.EID_REGEX')), + 'authors' => $authors, + 'authors_formatted' => [ + 'APA' => Publication::formatAuthorsApa($authors), + ], + 'status' => 'published', + 'visibility' => true, + 'citation-type' => $this->faker->optional(0.5)->word(), + 'citation-value' => $this->faker->optional(0.5)->word(), + ], + ]; + }); + } + + /** + * Return array of authors names formatted in any of the $this->$authors_names_patterns formats + */ + public function authorsNames() { + $names = []; + + for ($i = 0; $i < $this->authors_count; $i++) { + $elem = $this->faker->randomElement(array_keys($this->authors_names_patterns)); + $names[] = eval($this->authors_names_patterns[$elem]); + } + + return $names; + } + } diff --git a/resources/views/livewire/profile-data-cards/publications.blade.php b/resources/views/livewire/profile-data-cards/publications.blade.php index ee32c63b..7b3857cc 100644 --- a/resources/views/livewire/profile-data-cards/publications.blade.php +++ b/resources/views/livewire/profile-data-cards/publications.blade.php @@ -12,10 +12,14 @@ @foreach($data as $pub)
- {!! Purify::clean($pub->title) !!} {{$pub->year}} - {{$pub->type}} + @if($profile->hasOrcidManagedPublications() && !is_null($pub->authors_formatted)) + {{ $pub->authors_formatted['APA'] }} ({{ $pub->year }}) {!! Purify::clean($pub->title) !!}. + @else + {!! Purify::clean($pub->title) !!} {{$pub->year}} - {{$pub->type}} + @endif @if($pub->url) - + {{ $pub->url }} @endif
diff --git a/tests/Feature/PublicationsRepositoryTest.php b/tests/Feature/PublicationsRepositoryTest.php new file mode 100644 index 00000000..8bf2a373 --- /dev/null +++ b/tests/Feature/PublicationsRepositoryTest.php @@ -0,0 +1,121 @@ +profile = Profile::factory() + ->hasData([ + 'data->orc_id_managed' => 1, + 'data->orc_id' => $this->faker()->numerify(), + ]) + ->has( + ProfileData::factory() //count = 3 + ->existing_publication('general'), + 'data') + ->has( + ProfileData::factory() + ->count(2) + ->state([ + 'type' => 'publications', + 'data->sort_order' => $this->faker->year() + ]) + ->general(), 'data') + ->create(); + + $this->assertTrue($this->profile->hasOrcidManagedPublications()); + + $this->assertCount(5, $this->profile->publications); + $this->assertDatabaseCount('profile_data', 6); + + // $this->output("PROFILE PUBLICATIONS CREATED", $this->profile->publications, ['profile_id', 'sort_order', 'title']); + + $publications_edit_route = route('profiles.edit', [ + 'profile' => $this->profile, + 'section' => 'publications', + ]); + + $orcid_pubs_repo = $this->mockPublicationsRepository(); + + $this->instance(OrcidPublicationsRepository::class, $orcid_pubs_repo); + $this->loginAsAdmin(); + + $this->followingRedirects() + ->get($publications_edit_route) + ->assertStatus(200) + ->assertViewIs('profiles.show') + ->assertSee('Publications updated via ORCID.'); + + $this->profile->refresh(); + $this->assertCount(9, $this->profile->publications); + $this->assertDatabaseCount('profile_data', 10); + + foreach ($this->profile->publications as $orcid_pub) { + $this->assertDatabaseHas( + 'profile_data', + ['data' => $this->castToJson((array)$orcid_pub->data)] + ); + + if (isset($orcid_pub->data['authors'])) { + + $authors = Publication::formatAuthorsNames($orcid_pub->data['authors']); + + foreach ($authors as $author) { + $this->assertMatchesRegularExpression(Publication::REGEX_PATTERNS['APA'], $author['APA']); + + } + } + } + } + + /** + * Output a message to the console and log file + */ + public function output(string $message, $items = null, $attributes = null ): void + { + echo "\n $message \n"; + + if (!is_null($items)) { + + foreach ($items as $key => $item) { + $string = "$key "; + foreach ($attributes as $attr) { + $string .= $item->$attr . " "; + } + $string .= "\n"; + echo $string; + } + } + } +} \ No newline at end of file diff --git a/tests/Feature/Traits/MockPublicationsRepository.php b/tests/Feature/Traits/MockPublicationsRepository.php new file mode 100644 index 00000000..a35222b9 --- /dev/null +++ b/tests/Feature/Traits/MockPublicationsRepository.php @@ -0,0 +1,65 @@ +makePublications(); + + // $this->output("API PUBLICATIONS TO SYNC", $publications, ['profile_id', 'sort_order', 'title']); + + $pubs_mock = mock(OrcidPublicationsRepository::class)->makePartial(); + + $pubs_mock + ->shouldReceive('getCachedPublications') + ->andReturn($publications); + + return $pubs_mock; + } + + /** + * Returns a ProfileDataFactory collection of publications exisisting in the DB and new publications + * + * @return \Illuminate\Support\Collection + */ + public function makePublications() + { + $orcid_api_new_pubs = + ProfileData::factory() + ->count(4) + ->orcid_publication($this->profile) + ->make(); + + $orcid_api_existing_pubs = + ProfileData::factory() //count = 3 + ->existing_publication('orcid_publication', $this->profile) + ->make(); + + $orcid_api_new_pubs->map(fn($pub) => $orcid_api_existing_pubs->push($pub)); + + return $orcid_api_existing_pubs; + } + + /** + * Clean up the testing environment before the next test. + * + * @return void + */ + protected function tearDown(): void + { + // fix for the config() helper not resolving in tests using Mockery + $config = app('config'); + parent::tearDown(); + app()->instance('config', $config); + } +}