Aller au contenu

omnibase/scholar

Scholar for omnibase: a researcher's site - publications read from OpenAlex, HAL, Crossref and the other omnisense/omnischolar sources, validated before they show, with manual overrides that survive each sync; the CV (positions, degrees, awards, grants, courses, supervisions), research themes and projects; publication, book, CV and teaching pages, BibTeX / RIS exports, schema.org JSON-LD.

Étage
Structure préparée · Omnibase
Version
main
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibase/scholar:dev-main

omnibase/scholar is a researcher's site on omnibase: the publications read from the open sources and validated by the researcher, the books, the CV, the teaching, the research themes and projects, with their structured data and citation exports. Its namespace is Base\Scholar\; it reads the sources through omnisense/omnischolar (OpenAlex, HAL, Crossref, ORCID, Open Library).

What it adds

  • Scholar, the person and the profiles that name them in each source; Publication, a work as the site shows it (the sources' record and the site's own fields); Theme, a research theme or project; six CV entries (Position, Degree, Award, Grant, Course, Supervision).
  • scholar:sync, which reads every profile's works, the counts and the CV, and lays them over the site's publications: a new work waits for validation.
  • Seven pages: the publications (filtered and paged), a publication, the books, the CV, the teaching, the research, a theme; and two export routes (the list or one publication as BibTeX, RIS or CSL-JSON).
  • schema.org Person, ScholarlyArticle, Book and the others; the line to copy and the exports.
  • Twig functions for a host's home page.
  • Nine back office screens open to the site's administrator, a validation tile.
  • English, French and German messages (domain scholar).

Pages

Proposer une modification docs/index.md

Installation and configuration

composer require omnibase/scholar:dev-main omnisense/omnischolar omnischolar/openalex omnischolar/hal omnischolar/crossref omnischolar/orcid
// config/bundles.php
Omnischolar\Bridge\Symfony\OmnischolarBundle::class => ['all' => true],
Base\Scholar\ScholarBundle::class => ['all' => true],
# config/routes.yaml
scholar_controller:
    resource: "@ScholarBundle/src/Controller/Client"
    type: attribute
    prefix: /

The sources (omnisense/omnischolar)

The names a scholar's profiles use are the configured sources:

# config/packages/omnischolar.yaml
omnischolar:
    sources:
        openalex: { factory: openalex, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
        hal: { factory: hal }
        crossref: { factory: crossref, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
        orcid: { factory: orcid }
        openlibrary: { factory: openlibrary, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
    merger:
        preferences: { abstract: [hal, openalex], pdfUrl: [hal, openalex] }

The bundle (every key optional)

# config/packages/scholar.yaml
scholar:
    max_per_source: 2000          # works read from one source for one scholar
    metrics_source: openalex      # whose profile gives the counts (h-index...); ~ for none
    cv_source: orcid              # where the CV lines are read; ~: typed only
    per_page: 100                 # publications per page
    jsonld: true                  # Person, ScholarlyArticle, Book on the pages
    abstracts_in_exports: false

Then the tables (scholar_scholar, scholar_publication, scholar_publication_theme, scholar_theme, scholar_position, scholar_degree, scholar_award, scholar_grant, scholar_course, scholar_supervision):

bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate
bin/console assets:install          # public/css/scholar.css

The scholar

In the back office (Identity and sources), or in the fixtures:

$scholar = (new Scholar('Keitaro Nakatani'))
    ->setOrcid('0009-0005-1387-7295')
    ->setProfiles([
        'openalex' => ['A5108007452'],
        'hal' => ['Keitaro Nakatani'],      // by the name he signs: by ORCID HAL finds only the deposits that carry it
        'crossref' => ['Keitaro Nakatani'],
    ]);

A profile names a configured source and one or several authors there: an OpenAlex id, an ORCID, an idHAL, a name - what that source's works() reads (omnisense/omnischolar's docs/sources.md).

The cron container

30 2 * * 1  php /srv/app/bin/console scholar:sync >> /srv/app/var/log/cron.scholar-sync.log 2>&1

No Symfony Scheduler: the sites' cron container runs it.

Requirements

  • PHP 8.2 or later;
  • doctrine/orm 2.15 or 3;
  • omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin;
  • omnisense/omnischolar 1.x, and the sources the site reads (suggested: omnischolar/openalex, omnischolar/hal, omnischolar/crossref, omnischolar/orcid, omnischolar/openlibrary);
  • Symfony 6.4, 7 or 8 (config, console, dependency-injection, http-kernel, string).

Suggested as well: omnibase/agenda, for a scholar's talks and lectures as agenda events.

Proposer une modification docs/installation.md

Concepts

The scholar

Base\Scholar\Entity\Scholar (scholar_scholar) is the person the site is about. The pages and the Twig functions read the first one.

Field Meaning
name (required), givenName, familyName, honorific getFullName() is the honorific and the name: "Dr. Monica Neagoy"
jobTitle, affiliation, affiliationUrl for the CV and schema.org
orcid 0000-0000-0000-000X
profiles source name => the authors there (an OpenAlex id, an ORCID, an idHAL, a name); typed in the back office one source: author a line (getProfilesText(), setProfilesText())
sameAs other pages about the person, one a line
keywords what they work on (schema.org knowsAbout)
portrait, biography an image (16 MB); getBiographyParagraphs()
autoApprove the works read go online without validation
fromYear the sources are asked for the works from that year on
metrics the counts of the metrics source (works, citations, h-index, i10-index)
lastSyncAt, lastSync what the last run did (rememberSync())

getIdentityUrls() gathers the sameAs pages, the ORCID page, the OpenAlex author pages (ids like A5108007452) and the HAL CVs (an idHAL, not a name).

A publication

Base\Scholar\Entity\Publication (scholar_publication) is a work as the site shows it. It holds two kinds of fields:

  • the sources' record: work (omnisense/omnischolar's Work, as JSON: the abstract, the sources, the identifiers, the editions, the PDF and the page the sources give), with its columns title, type, year, venue, authors, doi and citations, refreshed by every run (refresh($work)), and what the merge recognises it by (matchKeys, fingerprint, fetchedAt, lastSeenAt);
  • the site's own: status, manual (typed by hand: its columns are the site's), hidden, pinned (first on the list), selected (the selection), pdf (an upload, 64 MB), pdfUrl, buyUrl, publisherUrl, cover (16 MB), note, themes.

Its type is one of omnisense/omnischolar's WorkType values: article, book, chapter, conference, preprint, thesis, report, dataset, software, review, editorial, other (an unknown value becomes other).

Method Returns
isShown() validated and not hidden: on the site
approve(), reject(), isPending(), isPublished() the status (PublicationStatus: pending, published, rejected)
getCoverUrl() the cover uploaded, else the sources' (Open Library's)
getFullTextUrl() the PDF uploaded, the address given, else the sources'
getLink() the publisher's page given, else the DOI's address, else the sources' page
isOpenAccess() the sources say so, or there is a full text to offer
getSlug() the title's slug, 80 characters at most, publication for none
getAbstract(), getIdentifiers(), getHalId(), getArxivId(), getIsbns(), getEditions(), getSources() read from work
toWork() the publication as a Work, with the site's own fields over the sources' (what the exports write)

Themes and projects

Base\Scholar\Entity\Theme (scholar_theme) is a research theme or a project (ThemeKind: theme, project; isProject()): title, unique slug, summary, description (getParagraphs()), image, start and end years (getYears()), funder, address, position, visible. Publications are linked to themes (scholar_publication_theme).

The CV

Six entities share Base\Scholar\Entity\Cv\CvEntry: the scholar, title (required), organization, city, country (two letters), start and end (YYYY, YYYY-MM or YYYY-MM-DD), description, url, position, visible, origin (site, or the source it was read from) and externalKey (the source's key, so a line is read once).

Entity Table Its own fields
Position scholar_position department
Degree scholar_degree thesis, advisor
Award scholar_award
Grant scholar_grant reference, role, amount
Course scholar_course level, volume
Supervision scholar_supervision level (SupervisionLevel: phd, master, postdoc, intern, other), subject, coSupervisors, outcome; the title is the student (getStudent())

getPeriod() writes "2019–2023", "2019–" for a current one, "2019" for one year; isCurrent() is started and not ended.

Base\Scholar\Service\Curriculum::sections($names) returns the visible entries of each section that has some (positions, degrees, awards, grants, courses, supervisions), by position, the current ones first, then the latest; a section whose table is not migrated yet is left out. countHidden() counts the hidden lines (a line read from the CV source waits hidden).

The three enums

PublicationStatus, ThemeKind and SupervisionLevel are PHP enums mapped with Doctrine's enumType on string columns; each case names itself in the scholar domain.

Proposer une modification docs/concepts.md

Synchronisation and validation

bin/console scholar:sync (--scholar=<id> for one) reads, for every scholar, every profile's works - every page, through omnischolar's Collector, merged by its Merger (same DOI, ISBN, arXiv id, HAL id, then title and year) - and lays them over the publications the site has:

The work What happens
unknown to the site a new Publication, pending: nothing shows before it is validated (unless the scholar has Publish without validation)
known (any identifier ever seen for it, else its title and year) its sources' record and columns refreshed; nothing the site added moves
rejected once refreshed, still rejected: a homonym's work never comes back
typed by hand (manual) found, its identifiers remembered; its columns are the site's and stay
no longer given by the sources left as it is

A source that is down or rate limited is named in the summary (incomplete) and the others still answer; nothing is taken for "gone".

The same run reads the counts of the metrics source's profile (scholar.metrics_source: works, citations, h-index, i10-index, as OpenAlex computes them) and the CV source's lines (scholar.cv_source: ORCID's employments, education, distinctions - each once, by a key of its own; a new line waits hidden).

What the run did is kept on the scholar (lastSyncAt, lastSync) and shown in the widget.

Calling it yourself

$summary = $synchronizer->sync($scholar);           // read the sources, then merge, flush
$summary = $synchronizer->merge($scholar, $works);  // lay Works over the site's (persisted, not flushed)
$summary->created; $summary->updated; $summary->incomplete;

merge() is what a test or an import of a file calls.

Validating

The dashboard's scholar_pending widget lists the newest pending works with two buttons; the list Publications filters on the status and has the same actions on each row and record. Publication::approve(), reject().

Proposer une modification docs/sync.md

Pages, Twig functions and the host's look

Route Path
scholar_publications /publications pinned first, then by year; ?type=article&year=2024&theme=<slug>&q=words, ?page=
scholar_publications_export /publications.{bib,ris,json} what the page lists, as a file
scholar_publication /publications/{id}-{slug} the record, its abstract, its editions, the reference to copy, its BibTeX
scholar_publication_export /publications/{id}.{bib,ris,json}
scholar_books /books the covers (the one uploaded, else Open Library's, else the title set as a cover)
scholar_cv /cv the person, the biography, the CV's sections, the selection
scholar_teaching /teaching courses and supervisions
scholar_research /research themes and projects
scholar_theme /research/{slug} a theme's text and its publications

Only validated, not hidden publications are shown. The publication and theme pages are in /sitemap.xml (EventListener\SitemapListener).

What is not given yet

A portrait, a biography, a CV, research themes that nobody gave are never filled with an invented text: @Scholar/client/_draft.html.twig marks the place as a draft (scholar-draft, its tag Draft).

{% include '@Scholar/client/_draft.html.twig' with {kind: 'biography', message: 'The biography is being written.'} %}

Twig, for the host's own pages

scholar() the site's scholar (the first one)
scholar_selected(n) the selection, else the latest
scholar_latest(n, types) the latest shown
scholar_books(n), scholar_count(types), scholar_themes(kind)
scholar_cv(sections) {positions: [...], degrees: [...]}
scholar_reference(publication) "Chocron, L., Nakatani, K. (2024). …"
scholar_person_jsonld(url, image) the Person, for a home page
type|scholar_type_label(count) "Article", "Articles" in the reader's language

{% include '@Scholar/client/_publication.html.twig' %} renders a publication as the list does; _theme_card.html.twig a theme.

The look

The templates extend layout1.html.twig and fill title, description, stylesheets and content. public/css/scholar.css follows the host's custom properties: --scholar-accent, --scholar-on-accent, --scholar-ink, --scholar-soft, --scholar-line, --scholar-surface, --scholar-font-display, --scholar-measure. Templates may be overridden in templates/bundles/ScholarBundle/.

Proposer une modification docs/pages.md

Structured data and exports

JSON-LD (Service\JsonLd, scholar.jsonld)

Page schema.org
/cv, and a host's home page through scholar_person_jsonld() Person: name, given and family names, honorific, jobTitle, affiliation, ORCID as identifier, sameAs (ORCID, OpenAlex, HAL's CV, the pages typed), knowsAbout
a publication ScholarlyArticle (a journal's Periodical, a conference's Event, a book's as isPartOf), Book (ISBNs, publisher, edition, an Offer to buy it), Chapter, Thesis, Report, Dataset, SoftwareSourceCode; the authors, the scholar's with his ORCID
/publications, /books an ItemList of them

Citing

Service\Citations::reference() writes the line to copy (authors with initials, year, title, journal, volume(issue), pages, DOI - or ISBN). export('bibtex'|'ris'|'csl-json', $publications) writes the files through omnisense/omnischolar's Export, from what the site shows: a PDF or a publisher's page given on the site goes into the file.

An example

use Base\Scholar\Entity\Publication;
use Base\Scholar\Entity\Scholar;
use Base\Scholar\Service\Citations;

$scholar = (new Scholar('Keitaro Nakatani'))->setOrcid('0009-0005-1387-7295');
$publication = (new Publication($scholar))
    ->setTitle('Acid-sensitive photoswitches')
    ->setType('article')
    ->setYear(2024)
    ->setVenue('Chemical Science')
    ->setAuthors('Lucas Chocron, Keitaro Nakatani')
    ->setDoi('10.1039/d4sc04973j');

$citations = new Citations();
echo $citations->reference($publication), "\n\n";
echo $citations->export('bibtex', $publication), "\n";
echo $citations->export('ris', $publication), "\n";

Its output, run in omnisense/omnibase-harness with omnisense/omnischolar on the autoloader:

Chocron, L., Nakatani, K. (2024). Acid-sensitive photoswitches. Chemical Science. https://doi.org/10.1039/d4sc04973j

@article{chocron2024acid,
  author = {Chocron, Lucas and Nakatani, Keitaro},
  title = {{Acid-sensitive photoswitches}},
  journal = {Chemical Science},
  year = {2024},
  doi = {10.1039/d4sc04973j}
}

TY  - JOUR
ID  - chocron2024acid
AU  - Chocron, Lucas
AU  - Nakatani, Keitaro
TI  - Acid-sensitive photoswitches
T2  - Chemical Science
PY  - 2024
DO  - 10.1039/d4sc04973j
UR  - https://doi.org/10.1039/d4sc04973j
ER  -

scholar.abstracts_in_exports: true writes the abstracts into the BibTeX and RIS files as well.

Proposer une modification docs/structured-data.md

The back office (omnibase/admin)

CRUD screens, under /admin/scholars/...:

Screen Controller
Identity and sources Crud\Scholar\ScholarCrudController the person; Sync now on each
Publications Crud\Scholar\PublicationCrudController Validate / Reject; the sources' fields read only, the site's editable; New types one by hand
Themes and projects Crud\Scholar\ThemeCrudController
CV PositionCrudController, DegreeCrudController, AwardCrudController, GrantCrudController, CourseCrudController, SupervisionCrudController a line read from ORCID waits hidden

Who writes

These screens are the site's administrator's (ROLE_ADMIN: the researcher whose site it is), not the super-admin's only - each controller carries omnibase/admin's #[OpenToAdmins]: creating, editing and deleting, and the screens' own buttons (approve, reject on a publication, sync on the scholar). An application's CRUD extending one of them is opened too, and may close an action again in its configureActions() (->setPermission(Action::DELETE, 'ROLE_SUPERADMIN')). The attribute is omnibase/admin's from 7474f85.

// DashboardController
yield MenuItem::block('scholar_pending', 'Publications à valider', 'fa-solid fa-book-open')->setSize(4);
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Publication::class, 'Publications', 'fa-solid fa-book-open');
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Scholar::class, 'Identité et sources', 'fa-solid fa-id-card');
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Cv\Position::class, 'CV · Postes', 'fa-solid fa-briefcase');

The widget's buttons post to admin_crud_scholars_publications_approve and _reject with the admin-action-approve / admin-action-reject tokens (omnibase/admin's #[AdminAction]), and come back to where they were.

A publication's status, a theme's kind and a supervision's level are PHP enums (PublicationStatus, ThemeKind, SupervisionLevel), mapped with Doctrine's enumType: on their string columns; in the back office each is a select of its cases (SelectField::new('status')), which name themselves in the scholar domain (TranslatableInterface). (They were kept as strings, with statusValue / kindValue / levelValue accessors for the selects, until omnibase could put an enum back into an entity's previous state and build a select from one: same columns, no migration.)

Proposer une modification docs/admin.md

Configuration reference

The scholar tree

Dumped with Symfony's YamlReferenceDumper from Base\Scholar\DependencyInjection\ScholarConfiguration:

scholar:

    # The most works scholar:sync reads from one source for one scholar.
    max_per_source:       2000

    # The omnischolar source whose author profile gives the counts (works, citations, h-index); null: none.
    metrics_source:       openalex

    # The omnischolar source the CV is read from (positions, degrees, distinctions), when the scholar has a profile there; null: the CV is typed only.
    cv_source:            orcid

    # Publications listed per page.
    per_page:             100

    # schema.org Person, ScholarlyArticle and Book on the pages.
    jsonld:               true

    # Write the abstracts into the BibTeX and RIS exports.
    abstracts_in_exports: false
Key Default Read by
max_per_source 2000 Synchronizer: the most works read from one source for one scholar
metrics_source openalex Synchronizer: the source whose author profile gives the counts; null, none
cv_source orcid Synchronizer: the source the CV lines are read from, when the scholar has a profile there; null, the CV is typed only
per_page 100 /publications
jsonld true the pages' schema.org
abstracts_in_exports false Citations: the abstracts in the BibTeX and RIS files

Each key is a container parameter: %scholar.max_per_source%, %scholar.metrics_source%, %scholar.cv_source%, %scholar.per_page%, %scholar.jsonld%, %scholar.abstracts_in_exports%.

The sources themselves are omnisense/omnischolar's (omnischolar.sources, Installation and configuration): a profile's source name is one of them.

Services

Service Role
Base\Scholar\Service\Synchronizer isAvailable(), sync($scholar), merge($scholar, $works)
Base\Scholar\Service\Citations reference($publication), export($format, $publications)
Base\Scholar\Service\JsonLd person(), publication(), list()
Base\Scholar\Service\Curriculum sections(), entries(), countHidden()
Base\Scholar\Service\WorkData a Work as the JSON column keeps it and back, every field included; what makes two records the same work (keys(), fingerprint())
Base\Scholar\Command\SyncCommand scholar:sync, --scholar=<id>
Base\Scholar\EventListener\SitemapListener the shown publications and the visible themes in /sitemap.xml
Base\Scholar\Twig\ScholarExtension the Twig functions and the scholar_type_label filter

Base\Scholar\Service\PublicationIndex is built by the synchronizer from a scholar's publications, not taken from the container: it finds the site's publication for a Work by any identifier ever seen for it (a DOI, a HAL id, an ISBN of any edition), else by its title and year, unless both carry a DOI and they differ (two versions of a dataset, an erratum).

Translations

The domain scholar, in English, French and German (ICU format). A site overrides a message in its own translations/scholar+intl-icu.<locale>.yaml.

Proposer une modification docs/configuration.md

Testing

Running the suite

From the package's checkout, with its dev dependencies:

vendor/bin/phpunit

Inside a host application:

php vendor/bin/phpunit -c vendor/omnibase/scholar/phpunit.xml.dist

The suite needs omnisense/omnischolar. omnisense/omnibase-harness does not have it, and its test scholar stops when the kernel boots (Class "Omnischolar\Export" not found); with omnisense/omnischolar's packages added to the harness's autoloader (its checkouts mounted), the suite on this checkout gives:

OK (40 tests, 257 assertions)

What the tests check

Test Checks
Controller\Admin\OpenToAdminsTest the administrator writes in the nine screens; anyone else is refused
Controller\CrudRequiredFieldsTest omnibase/admin's CrudFieldsTestCase on the nine screens
Entity\EnumFieldsTest a publication holds its status, a theme its kind, a supervision its level; the columns are strings of the enum's length; each case names itself in the scholar domain
Service\CitationsTest the reference line; the exports carry the site's overrides; the JSON-LD
Service\SynchronizerTest a new work waits to be validated; the site's overrides survive the next run; a work is found again by any identifier ever seen; a rejected work stays rejected and a manual one stays the site's; an auto-approving scholar's works go online at once
Service\WorkDataTest a work comes back whole; a book's editions come back and name it; the authors' line

Proposer une modification docs/testing.md