Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display Altmetric badges on simple item view #2496

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ item:
# Rounded to the nearest size in the list of selectable sizes on the
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
pageSize: 5
# Show the Altmetric badge in the simple item page.
showAltmetricBadge: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change to false because the default value is false.


# Collection Page Config
collection:
Expand Down
2 changes: 2 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ rest:
host: sandbox.dspace.org
port: 443
nameSpace: /server
item:
showAltmetricBadge: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed as it shouldn't be necessary. All default config values are only in app/config/default-app-config.ts. (That's why this file has very few configurations in it)

12 changes: 11 additions & 1 deletion src/app/item-page/item-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component
import {
ThemedFullFileSectionComponent
} from './full/field-components/file-section/themed-full-file-section.component';
import { ItemPageAltmetricFieldComponent } from './simple/field-components/specific-field/metrics/altmetric/item-page-altmetric-field.component';
import { AltmetricDirective } from './simple/field-components/specific-field/metrics/altmetric/item-page-altmetric-field.directive';
import { ItemPageMetricsFieldComponent } from './simple/field-components/specific-field/metrics/item-page-metrics-field.component';

const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
Expand Down Expand Up @@ -103,6 +106,12 @@ const DECLARATIONS = [
ItemAlertsComponent,
ThemedItemAlertsComponent,
BitstreamRequestACopyPageComponent,
ItemPageMetricsFieldComponent,
ItemPageAltmetricFieldComponent,
];

const DIRECTIVES = [
AltmetricDirective
];

@NgModule({
Expand All @@ -124,10 +133,11 @@ const DECLARATIONS = [
],
declarations: [
...DECLARATIONS,

...DIRECTIVES,
],
exports: [
...DECLARATIONS,
...DIRECTIVES,
]
})
export class ItemPageModule {
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to use this for PlumX, however, the format for PlumX used this format: <a href="https://plu.mx/plum/a/?doi=10.1371/journal.pone.0056506" class="plumx-details"></a>

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<span
dsAltmetricData
[item]="item"
class="altmetric-embed"
data-hide-no-mentions="true"
data-badge-type="donut"
data-badge-popover="right"
data-link-target="_blank"
></span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { AfterViewInit, Component, EventEmitter, HostListener, Inject, Input, Output } from '@angular/core';
import { ExternalScriptLoaderService } from 'src/app/shared/utils/scripts-loader/external-script-loader.service';
import {
ExternalScriptsNames,
ExternalScriptsStatus,
} from 'src/app/shared/utils/scripts-loader/external-script.model';
import { Item } from '../../../../../../core/shared/item.model';
import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface';

@Component({
selector: 'ds-item-page-altmetric-field',
templateUrl: './item-page-altmetric-field.component.html',
})
export class ItemPageAltmetricFieldComponent implements AfterViewInit {
@Input() item: Item;

@Output() widgetLoaded = new EventEmitter<boolean>();

constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
private scriptLoader: ExternalScriptLoaderService
) {}

ngAfterViewInit() {
if (!this.appConfig.item.showAltmetricBadge) {
return;
}

this.scriptLoader
.load(ExternalScriptsNames.ALTMETRIC)
.then((data) => this.reloadBadge(data))
.catch((error) => console.error(error));
}

/**
* We ensure that the badge is visible after the script is loaded
* @param data The data returned from the promise
*/
private reloadBadge(data: any[]) {
Copy link

@blancoj blancoj May 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to point out that I was trying to implement this, but instead of for Altmetrics, for Dimensions. I basically cp/pt'd the files that were Altmetric specific, but renamed them and some of the variables/constants in the code to "dimensions." I had it working except that when I left the item page and then came back to the page the badge was gone. The issue came from this method. I thought that I could use _dimensison_embed_init as the method, but not that did not work. That method does not exists in the Dimensions' javascript. I reached out to the community and @sergius02 told me to change this method to this:

private reloadBadge(data: any[]) {
  if (data.find((element) => this.isLoaded(element))) {
    const initClass = '__dimensions_embed';
    const initMethod = 'addBadges';
    window[initClass][initMethod]();
  }
}

Then it worked.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sergius02, I've tried this method but for Plumx. The PlumX developer page says to call this method window.__plumX.widgets.init(); but unfortunately if I navigate away from the page and return later, I received a TypeError: window[initClass] is undefined error. I tried replacing the value of initClass above with __plumX.widgets.init but the error persists. Any ideas how to resolve this issue?

if (data.find((element) => this.isLoaded(element))) {
const initMethod = '_altmetric_embed_init';
window[initMethod]();
}
}

/**
* Check if the script has been previously loaded in the DOM
* @param element The resolve element from the promise
* @returns true if the script has been already loaded, false if not
*/
private isLoaded(element: any): unknown {
return (
element.script === ExternalScriptsNames.ALTMETRIC &&
element.status === ExternalScriptsStatus.ALREADY_LOADED
);
}

@HostListener('window:altmetric:show', ['$event'])
private onWidgetShow(event: Event) {
this.widgetLoaded.emit(true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import { Item } from 'src/app/core/shared/item.model';

/**
* This directive adds the data-* attribute for the Altmetric badge dependening on the first identifier found in the item
*/
@Directive({
selector: '[dsAltmetricData]',
})
export class AltmetricDirective implements OnInit {
@Input() item: Item;

constructor(private renderer: Renderer2, private elementRef: ElementRef) {}

ngOnInit(): void {
const identifier = this.obtainFirstValidID(this.initItemIdentifiers());
if (identifier !== undefined) {
this.renderer.setAttribute(
this.elementRef.nativeElement,
identifier.name,
this.applyRegex(identifier.value, identifier.regex)
);
}
}

/**
* This initialize an array of identifiers founded in the item.
* It search for DOI, Handle, PMID, ISBN, ARXIV and URI.
* Some identifiers may be stored with more text than the ID so this objects has a regex property to clean it
*/
private initItemIdentifiers(): any[] {
return [
{
name: 'data-doi',
value: this.item.firstMetadataValue('dc.identifier.doi'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is correct. Currently DSpace is storing DOIs in dc.identifier.uri and/or dc.identifier based on this ticket DSpace/DSpace#5565

Maybe we need a way to check for DOIs in all three of these fields? I'm OK with the first check being dc.identifier.doi...but if not found there, we may want to check other fields for DOIs too.

regex: /https?:\/\/(dx\.)?doi\.org\//gi,
},
{
name: 'data-handle',
value: this.item.firstMetadataValue('dc.identifier.uri'),
regex: /http?:\/\/hdl\.handle\.net\//gi,
},
{
name: 'data-pmid',
value: this.item.firstMetadataValue('dc.identifier.pmid'),
regex: '',
},
{
name: 'data-isbn',
value: this.item.firstMetadataValue('dc.identifier.isbn'),
regex: '',
},
{
name: 'data-arxiv-id',
value: this.item.firstMetadataValue('dc.identifier.arxiv'),
regex: '',
},
{
name: 'data-uri',
value: this.item.firstMetadataValue('dc.identifier.uri'),
regex: '',
},
];
}

/**
* This function obtains the first valid ID from the item
* @returns Returns first valid identifier (not undefined), undefined otherwise
*/
private obtainFirstValidID(itemIdentifiers: any[]): any {
return itemIdentifiers.find((element) => element.value !== undefined);
}

/**
* Apply the specified regex to clean the metadata and obtain only the ID
* @param value The metadata value
* @param regex The regex to apply
* @returns The result is the ID clean
*/
private applyRegex(value: string, regex: string): string {
return value.replace(regex, '');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="item-page-field">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<div class="simple-view-element">
<h2 *ngIf="showTitle" class="simple-view-element-header">
{{ "item.page.metrics" | translate }}
</h2>
<div class="simple-view-element-body">
<ds-item-page-altmetric-field (widgetLoaded)="someWidgetHasLoaded($event)" [item]="item"></ds-item-page-altmetric-field>
</div>
</div>
</ds-metadata-field-wrapper>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component, Input } from '@angular/core';
import { ItemPageFieldComponent } from '../item-page-field.component';
import { Item } from 'src/app/core/shared/item.model';

@Component({
selector: 'ds-item-page-metrics-field',
templateUrl: './item-page-metrics-field.component.html',
styleUrls: [
'../../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component.scss',
],
})
export class ItemPageMetricsFieldComponent extends ItemPageFieldComponent {

@Input() item: Item;

public showTitle = false;

public someWidgetHasLoaded(widgetLoaded: boolean) {
if (widgetLoaded) {
this.showTitle = true;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
[fields]="['dc.publisher']"
[label]="'publication.page.publisher'">
</ds-generic-item-page-field>
<ds-item-page-metrics-field [item]="object"></ds-item-page-metrics-field>
</div>
<div class="col-xs-12 col-md-7">
<ds-related-items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
[fields]="['dc.publisher']"
[label]="'item.page.publisher'">
</ds-generic-item-page-field>
<ds-item-page-metrics-field [item]="object"></ds-item-page-metrics-field>
</div>
<div class="col-xs-12 col-md-6">
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
Expand Down
4 changes: 3 additions & 1 deletion src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
import { ExternalScriptLoaderService } from './utils/scripts-loader/external-script-loader.service';
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
import { DynamicComponentLoaderDirective } from './abstract-component-loader/dynamic-component-loader.directive';
Expand Down Expand Up @@ -472,7 +473,8 @@ const ENTRY_COMPONENTS = [
const PROVIDERS = [
TruncatableService,
MockAdminGuard,
AbstractTrackableComponent
AbstractTrackableComponent,
ExternalScriptLoaderService,
];

const DIRECTIVES = [
Expand Down
103 changes: 103 additions & 0 deletions src/app/shared/utils/scripts-loader/external-script-loader.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Injectable } from '@angular/core';
import {
ExternalScriptsList,
ExternalScriptsStatus,
} from './external-script.model';

declare const document: any;

/**
* Service used to load external scripts in the DOM when it cannot be loaded from the standard angular methods
*/
@Injectable()
export class ExternalScriptLoaderService {
private scriptsStored: any = {};

Check warning on line 14 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L14

Added line #L14 was not covered by tests

constructor() {
ExternalScriptsList.forEach(
({ name, src }) => (this.scriptsStored[name] = { loaded: false, src })

Check warning on line 18 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L17-L18

Added lines #L17 - L18 were not covered by tests
);
}

/**
* Load the scripts in the DOM and return every {@link Promise}
* @param scriptsToLoad Scripts to load, see {@link scriptsToLoad}
* @returns An array of the scripts promises
*/
load(...scriptsToLoad: string[]): Promise<any[]> {
let promises: any[] = [];
scriptsToLoad.forEach((script) => promises.push(this.loadScript(script)));
return Promise.all(promises);

Check warning on line 30 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L28-L30

Added lines #L28 - L30 were not covered by tests
}

private loadScript(name: string) {
return new Promise((resolve) => {

Check warning on line 34 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L34

Added line #L34 was not covered by tests
if (this.isAlreadyLoaded(name)) {
resolve(

Check warning on line 36 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L36

Added line #L36 was not covered by tests
this.createResolveResult(
name,
true,
ExternalScriptsStatus.ALREADY_LOADED
)
);
} else {
let script = this.createScriptHTMLElement(name);

Check warning on line 44 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L44

Added line #L44 was not covered by tests
if (this.areWeInIE(script)) {
this.configureOnReadyState(name, script, resolve);

Check warning on line 46 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L46

Added line #L46 was not covered by tests
} else {
this.configureOnLoad(name, script, resolve);

Check warning on line 48 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L48

Added line #L48 was not covered by tests
}
this.configureOnError(name, script, resolve);
document.getElementsByTagName('head')[0].appendChild(script);

Check warning on line 51 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L50-L51

Added lines #L50 - L51 were not covered by tests
}
});
}

private isAlreadyLoaded(name: string): boolean {
return this.scriptsStored[name].loaded;

Check warning on line 57 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L57

Added line #L57 was not covered by tests
}

private areWeInIE(script: any): boolean {
return script.readyState;

Check warning on line 61 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L61

Added line #L61 was not covered by tests
}

private createResolveResult(script: string, loaded: boolean, status: string) {
return { script, loaded, status };

Check warning on line 65 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L65

Added line #L65 was not covered by tests
}

private createScriptHTMLElement(name: string): any {
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = this.scriptsStored[name].src;

Check warning on line 71 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L69-L71

Added lines #L69 - L71 were not covered by tests

return script;

Check warning on line 73 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L73

Added line #L73 was not covered by tests
}

private configureOnReadyState(name: string, script: any, resolve: any) {
script.onreadystatechange = () => {

Check warning on line 77 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L77

Added line #L77 was not covered by tests
if (script.readyState === 'loaded' || script.readyState === 'complete') {
script.onreadystatechange = null;
this.scriptsStored[name].loaded = true;
resolve(

Check warning on line 81 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L79-L81

Added lines #L79 - L81 were not covered by tests
this.createResolveResult(name, true, ExternalScriptsStatus.LOADED)
);
}
};
}

private configureOnLoad(name: string, script: any, resolve: any) {
script.onload = () => {
this.scriptsStored[name].loaded = true;
resolve(

Check warning on line 91 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L89-L91

Added lines #L89 - L91 were not covered by tests
this.createResolveResult(name, true, ExternalScriptsStatus.LOADED)
);
};
}

private configureOnError(name: string, script: any, resolve: any) {
script.onerror = (error: any) =>
resolve(

Check warning on line 99 in src/app/shared/utils/scripts-loader/external-script-loader.service.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/utils/scripts-loader/external-script-loader.service.ts#L98-L99

Added lines #L98 - L99 were not covered by tests
this.createResolveResult(name, false, ExternalScriptsStatus.NOT_LOADED)
);
}
}
Loading
Loading