From b96dbde1bc09a400f70277b7fce9aa20b81ae77a Mon Sep 17 00:00:00 2001 From: jurinecko <95219754+jr-rk@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:09:27 +0200 Subject: [PATCH 1/3] VSB-TUO/Display total downloads for each item (#961) * Added new feature for downloads of item's bitstreams * Fixed Copilot's suggestions: any type & redundant property access pattern --- .../statistics/models/usage-report.model.ts | 3 +- src/app/item-page/item-page.module.ts | 6 ++ .../total-downloads.component.html | 7 ++ .../total-downloads.component.scss | 0 .../file-section/total-downloads.component.ts | 73 +++++++++++++++++++ src/assets/i18n/cs.json5 | 5 +- src/assets/i18n/en.json5 | 2 + 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.html create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.scss create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.ts diff --git a/src/app/core/statistics/models/usage-report.model.ts b/src/app/core/statistics/models/usage-report.model.ts index da976a8b903..ff8ae6fcdcb 100644 --- a/src/app/core/statistics/models/usage-report.model.ts +++ b/src/app/core/statistics/models/usage-report.model.ts @@ -47,5 +47,6 @@ export interface Point { type: string; values: { views: number; - }[]; + [key: string]: number; + }; } diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 18bbc4fd606..26040cfcb18 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -57,6 +57,7 @@ import { ItemAlertsComponent } from './alerts/item-alerts.component'; import { ItemVersionsModule } from './versions/item-versions.module'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { FileSectionComponent } from './simple/field-components/file-section/file-section.component'; +import { TotalDownloadsComponent } from './simple/field-components/file-section/total-downloads.component'; import { ItemSharedModule } from './item-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component'; @@ -90,6 +91,7 @@ import { ClarinIdentifierItemFieldComponent } from './simple/field-components/cl import { ClarinDateItemFieldComponent } from './simple/field-components/clarin-date-item-field/clarin-date-item-field.component'; import { ClarinDescriptionItemFieldComponent } from './simple/field-components/clarin-description-item-field/clarin-description-item-field.component'; import { ClarinFilesSectionComponent } from './clarin-files-section/clarin-files-section.component'; +import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -99,6 +101,7 @@ const ENTRY_COMPONENTS = [ const DECLARATIONS = [ FileSectionComponent, + TotalDownloadsComponent, ThemedFileSectionComponent, ItemPageComponent, ThemedItemPageComponent, @@ -181,6 +184,9 @@ const DECLARATIONS = [ ], exports: [ ...DECLARATIONS, + ], + providers: [ + UsageReportDataService, ] }) export class ItemPageModule { diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html new file mode 100644 index 00000000000..72db7109aa0 --- /dev/null +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html @@ -0,0 +1,7 @@ + +
+
+ {{ totalDownloads }} +
+
+
diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.scss b/src/app/item-page/simple/field-components/file-section/total-downloads.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts new file mode 100644 index 00000000000..d5cc17cbd08 --- /dev/null +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service'; +import { catchError } from 'rxjs/operators'; +import { of } from 'rxjs'; + +/** + * Component that displays the total number of downloads for all bitstreams within a DSpace item. + * + * This component fetches download statistics for a given item using its UUID and aggregates + * the download counts from all bitstreams associated with that item. The result is displayed + * as a single total download count. + */ +@Component({ + selector: 'ds-total-downloads', + templateUrl: './total-downloads.component.html', + styleUrls: ['./total-downloads.component.scss'] +}) +export class TotalDownloadsComponent implements OnInit { + + /** + * The UUID of the DSpace item for which to fetch download statistics. + */ + @Input() itemUuid!: string; + + /** + * The total number of downloads across all bitstreams for the item. + * Defaults to 0 and will show 0 if no data is available or an error occurs. + */ + totalDownloads: number = 0; + + /** + * The translation key for the downloadsLabel displayed alongside the download count. + */ + readonly downloadsLabel = 'item.page.files.downloads'; + + + constructor(private usageReportDataService: UsageReportDataService) { } + + /** + * Fetches the total download statistics for the item specified by itemUuid. + * The component will: + * 1. Call the UsageReportDataService with the item UUID and 'TotalDownloads' report type + * 2. Aggregate all download counts (views) from all bitstreams in the response + * 3. Set the totalDownloads property with the sum + * 4. Handle errors gracefully by setting totalDownloads to 0 and logging the error + * + * @throws Will log an error to console if the API call fails, but won't throw an exception + */ + ngOnInit(): void { + if (!this.itemUuid) { + return; + } + + const reportType = 'TotalDownloads'; + this.usageReportDataService.getStatistic(this.itemUuid, reportType) + .pipe( + catchError(error => { + console.error('Failed to fetch total downloads statistics:', error); + return of(null); + }) + ) + .subscribe(report => { + if (report) { + this.totalDownloads = report.points.reduce((total, point) => { + const views = point.values.views || 0; + return total + views; + }, 0); + } else { + this.totalDownloads = 0; // Show 0 instead of null when no data is available + } + }); + } +} diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index f928e07d09f..1ad16c781ff 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -3794,6 +3794,9 @@ // "item.page.files": "Files", "item.page.files": "Soubory", + // "item.page.files.downloads": "Downloads", + "item.page.files.downloads": "Stažení", + // "item.page.filesection.description": "Description:", "item.page.filesection.description": "Popis:", @@ -11542,4 +11545,4 @@ // "clarin-license-agreement-page.download-error": "An error occurred while downloading the file. Please try again later.", "clarin-license-agreement-page.download-error": "Při stahování souboru došlo k chybě. Zkuste to prosím znovu později.", -} \ No newline at end of file +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index c6006102d8d..b6c69c0c90e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2529,6 +2529,8 @@ "item.page.files": "Files", + "item.page.files.downloads": "Downloads", + "item.page.filesection.description": "Description:", "item.page.filesection.download": "Download", From 4a7166e46367e1898213297dbaf87b54a19bf155 Mon Sep 17 00:00:00 2001 From: jurinecko <95219754+jr-rk@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:42:01 +0200 Subject: [PATCH 2/3] VSB-TUO/Make item view - Total Downloads - configurable (#996) * Implemented configurable showing of component --- .../total-downloads.component.html | 4 +- .../file-section/total-downloads.component.ts | 67 ++++++++++++++++--- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html index 72db7109aa0..5b2d9bbb117 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html @@ -1,4 +1,6 @@ - +
{{ totalDownloads }} diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts index d5cc17cbd08..25f3d2119c0 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -1,14 +1,18 @@ import { Component, OnInit, Input } from '@angular/core'; import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service'; +import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service'; import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; /** * Component that displays the total number of downloads for all bitstreams within a DSpace item. * - * This component fetches download statistics for a given item using its UUID and aggregates - * the download counts from all bitstreams associated with that item. The result is displayed - * as a single total download count. + * This component checks the 'item.view.total.downloads.enabled' configuration property + * to determine if download statistics should be displayed. If enabled, it fetches download + * statistics for a given item using its UUID and aggregates the download counts from all + * bitstreams associated with that item. The result is displayed as a single total download count. + * + * If the configuration is disabled or set to 'false', the component will not be displayed. */ @Component({ selector: 'ds-total-downloads', @@ -26,7 +30,14 @@ export class TotalDownloadsComponent implements OnInit { * The total number of downloads across all bitstreams for the item. * Defaults to 0 and will show 0 if no data is available or an error occurs. */ - totalDownloads: number = 0; + totalDownloads: number | null = 0; + + /** + * Flag indicating whether the total downloads feature is enabled in the configuration. + * Uses BehaviorSubject to allow reactive updates. Defaults to false and will only be + * set to true if the configuration explicitly contains 'true' value. + */ + totalDownloadsEnabled = new BehaviorSubject(false); /** * The translation key for the downloadsLabel displayed alongside the download count. @@ -34,15 +45,21 @@ export class TotalDownloadsComponent implements OnInit { readonly downloadsLabel = 'item.page.files.downloads'; - constructor(private usageReportDataService: UsageReportDataService) { } + constructor( + private usageReportDataService: UsageReportDataService, + private configService: ConfigurationDataService + ) { } /** - * Fetches the total download statistics for the item specified by itemUuid. + * Fetches the configuration to check if total downloads should be shown, + * and if enabled, fetches the total download statistics for the item specified by itemUuid. * The component will: - * 1. Call the UsageReportDataService with the item UUID and 'TotalDownloads' report type - * 2. Aggregate all download counts (views) from all bitstreams in the response - * 3. Set the totalDownloads property with the sum - * 4. Handle errors gracefully by setting totalDownloads to 0 and logging the error + * 1. Check the 'item.view.total.downloads.enabled' configuration property + * 2. If enabled (configuration value is explicitly 'true'), call the UsageReportDataService + * If configuration is not found or fails to load, defaults to false (disabled) + * 3. Aggregate all download counts (views) from all bitstreams in the response + * 4. Set the totalDownloads property with the sum + * 5. Handle errors gracefully by returning null and logging the error * * @throws Will log an error to console if the API call fails, but won't throw an exception */ @@ -51,6 +68,34 @@ export class TotalDownloadsComponent implements OnInit { return; } + // First, check if total downloads feature is enabled in configuration + this.configService.findByPropertyName('item.view.total.downloads.enabled') + .pipe( + catchError(error => { + console.error('Failed to fetch total downloads configuration:', error); + // Default to false if configuration cannot be retrieved + return of(null); + }) + ) + .subscribe(configData => { + // Extract configuration value, default to 'false' if not found + const itemViewTotalDownloadsEnabled = configData?.payload?.values?.[0]; + this.totalDownloadsEnabled.next(itemViewTotalDownloadsEnabled === 'true'); + + // Only fetch download statistics if the feature is enabled + if (this.totalDownloadsEnabled.value) { + this.fetchDownloadStatistics(); + } else { + this.totalDownloads = null; // Ensure it's null when disabled + } + }); + } + + /** + * Private method to fetch download statistics from the usage report service. + * This method is called only when the total downloads feature is enabled. + */ + private fetchDownloadStatistics(): void { const reportType = 'TotalDownloads'; this.usageReportDataService.getStatistic(this.itemUuid, reportType) .pipe( From 4ff27d6df732d6c9a2ae032ba5878ad37adb26ff Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:21:38 +0100 Subject: [PATCH 3/3] refactor: provide UsageReportDataService as singleton to avoid duplicate instances --- src/app/core/statistics/usage-report-data.service.ts | 2 +- src/app/item-page/item-page.module.ts | 4 ---- src/app/statistics-page/statistics-page.module.ts | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app/core/statistics/usage-report-data.service.ts b/src/app/core/statistics/usage-report-data.service.ts index 5ce186f4ca6..96961a863d5 100644 --- a/src/app/core/statistics/usage-report-data.service.ts +++ b/src/app/core/statistics/usage-report-data.service.ts @@ -20,7 +20,7 @@ import { RequestParam } from '../cache/models/request-param.model'; /** * A service to retrieve {@link UsageReport}s from the REST API */ -@Injectable() +@Injectable({ providedIn: 'root' }) @dataService(USAGE_REPORT) export class UsageReportDataService extends IdentifiableDataService implements SearchData { private searchData: SearchDataImpl; diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 26040cfcb18..db9446838eb 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -91,7 +91,6 @@ import { ClarinIdentifierItemFieldComponent } from './simple/field-components/cl import { ClarinDateItemFieldComponent } from './simple/field-components/clarin-date-item-field/clarin-date-item-field.component'; import { ClarinDescriptionItemFieldComponent } from './simple/field-components/clarin-description-item-field/clarin-description-item-field.component'; import { ClarinFilesSectionComponent } from './clarin-files-section/clarin-files-section.component'; -import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -184,9 +183,6 @@ const DECLARATIONS = [ ], exports: [ ...DECLARATIONS, - ], - providers: [ - UsageReportDataService, ] }) export class ItemPageModule { diff --git a/src/app/statistics-page/statistics-page.module.ts b/src/app/statistics-page/statistics-page.module.ts index 75726de94cc..62993094a8b 100644 --- a/src/app/statistics-page/statistics-page.module.ts +++ b/src/app/statistics-page/statistics-page.module.ts @@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; import { StatisticsModule } from '../statistics/statistics.module'; -import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; import { StatisticsTableComponent } from './statistics-table/statistics-table.component'; import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; @@ -35,9 +34,6 @@ const components = [ StatisticsModule.forRoot() ], declarations: components, - providers: [ - UsageReportDataService, - ], exports: components })