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
6 changes: 4 additions & 2 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ cache:
anonymousCache:
# Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled.
# As all pages are cached in server memory, increasing this value will increase memory needs.
# Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory.
# Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory.
max: 0
# Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached
# copy is automatically refreshed on the next request.
Expand Down 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 Expand Up @@ -382,7 +384,7 @@ vocabularies:
vocabulary: 'srsc'
enabled: true

# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'
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 @@ -282,6 +282,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';

Expand Down Expand Up @@ -473,7 +474,8 @@ const ENTRY_COMPONENTS = [
const PROVIDERS = [
TruncatableService,
MockAdminGuard,
AbstractTrackableComponent
AbstractTrackableComponent,
ExternalScriptLoaderService,
];

const DIRECTIVES = [
Expand Down
Loading
Loading