From cacd4414ca35c44024ba7e59bc34c2e2f64efbc6 Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Tue, 12 Dec 2023 08:52:44 -0600 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=EF=B8=8FAdds=20OrcidPublicationsR?= =?UTF-8?q?epository=20and=20PublicationsHelper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved the Orcid import process from the Profile model to a new OrcidPublicationsRepository class. This class implements an interface and utilizes the new Publications helper to handle publication formatting methods to return author names in the APA citation format but scalable, to allow the addition of new formatting options. --- .phpactor.json | 4 + app/Helpers/Publication.php | 213 +++++++++++++++ app/Http/Controllers/ProfilesController.php | 21 +- app/Profile.php | 87 +++--- app/Providers/RepositoryServiceProvider.php | 30 +++ .../PublicationsRepositoryContract.php | 75 ++++++ .../OrcidPublicationsRepository.php | 251 ++++++++++++++++++ config/app.php | 1 + database/factories/ProfileDataFactory.php | 1 + .../profile-data-cards/publications.blade.php | 8 +- 10 files changed, 624 insertions(+), 67 deletions(-) create mode 100644 .phpactor.json create mode 100644 app/Helpers/Publication.php create mode 100644 app/Providers/RepositoryServiceProvider.php create mode 100644 app/Repositories/Contracts/PublicationsRepositoryContract.php create mode 100644 app/Repositories/OrcidPublicationsRepository.php diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 00000000..6c66e2e7 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,4 @@ +{ + "$schema": "/Applications/Tinkerwell.app/Contents/Resources/phpactor/phpactor.schema.json", + "language_server_psalm.enabled": false +} \ No newline at end of file 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..4a7a1bbe 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,12 @@ public function hasOrcidManagedPublications() public function updateORCID() { - $orc_id = $this->information()->get(array('data'))->toArray()[0]['data']['orc_id']; + $publicationsManager = new OrcidPublicationsRepository($this); - 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; + return $publicationsManager->syncPublications(); } + public function updateDatum($section, $request) { $sort_order = count($request->data ?? []) + 1; @@ -375,6 +327,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 +563,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..64dccf5c --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,30 @@ +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..8b3b0d36 100644 --- a/database/factories/ProfileDataFactory.php +++ b/database/factories/ProfileDataFactory.php @@ -71,6 +71,7 @@ public function general() 'url' => $this->faker->url(), 'title' => $this->faker->sentence(), 'year' => $this->faker->year(), + 'authors_formatted' => ['APA' => $this->faker->paragraph()], ], ]; }); 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
From 628400d166b37f454c4d0f74cd1dcd7bff0ee169 Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:17:43 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20Adds=20Orcid=20data=20import=20?= =?UTF-8?q?optimization=20feature=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Includes ProfileDataFactory for publications and authors, publications mock and feature test. --- app/Profile.php | 7 +- app/Providers/RepositoryServiceProvider.php | 4 + database/factories/ProfileDataFactory.php | 99 ++++++++++++++ tests/Feature/PublicationsRepositoryTest.php | 121 ++++++++++++++++++ .../Traits/MockPublicationsRepository.php | 65 ++++++++++ 5 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/PublicationsRepositoryTest.php create mode 100644 tests/Feature/Traits/MockPublicationsRepository.php diff --git a/app/Profile.php b/app/Profile.php index 4a7a1bbe..fcd88bec 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -177,9 +177,10 @@ public function hasOrcidManagedPublications() public function updateORCID() { - $publicationsManager = new OrcidPublicationsRepository($this); - - return $publicationsManager->syncPublications(); + $publications_manager = app()->make(OrcidPublicationsRepository::class); + $publications_manager->setProfile($this); + + return $publications_manager->syncPublications(); } diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 64dccf5c..68206ae6 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Profile; use App\Repositories\Contracts\PublicationsRepositoryContract; use App\Repositories\OrcidPublicationsRepository; use Illuminate\Support\ServiceProvider; @@ -25,6 +26,9 @@ public function boot() */ public function register() { + $this->app->bind(OrcidPublicationsRepository::class, function($app) { + return new OrcidPublicationsRepository($app[Profile::class]); + }); $this->app->bind(PublicationsRepositoryContract::class, OrcidPublicationsRepository::class); } } diff --git a/database/factories/ProfileDataFactory.php b/database/factories/ProfileDataFactory.php index 8b3b0d36..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. * @@ -180,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/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); + } +} From 24a33d203c50ceb8171472f850eb5c64db0e403c Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Wed, 1 May 2024 11:19:32 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=99=88=20Removes=20.phpactor.json=20f?= =?UTF-8?q?ile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phpactor.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .phpactor.json diff --git a/.phpactor.json b/.phpactor.json deleted file mode 100644 index 6c66e2e7..00000000 --- a/.phpactor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "/Applications/Tinkerwell.app/Contents/Resources/phpactor/phpactor.schema.json", - "language_server_psalm.enabled": false -} \ No newline at end of file