From 00480a3d26bed6d228aff7a15eba99761d713d7c Mon Sep 17 00:00:00 2001
From: Juraj Roka <95219754+jr-rk@users.noreply.github.com>
Date: Thu, 21 Aug 2025 18:06:05 +0200
Subject: [PATCH 1/4] Added new feature for downloads of item's bitstreams
---
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 | 33 +++++++++++++++++++
.../publication/publication.component.html | 3 +-
.../untyped-item/untyped-item.component.html | 3 +-
src/assets/i18n/cs.json5 | 5 ++-
src/assets/i18n/en.json5 | 4 ++-
8 files changed, 57 insertions(+), 4 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/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..498a810f42b
--- /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..aeba7cc97d2
--- /dev/null
+++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts
@@ -0,0 +1,33 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { UsageReport } from 'src/app/core/statistics/models/usage-report.model';
+import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service';
+
+@Component({
+ selector: 'ds-total-downloads',
+ templateUrl: './total-downloads.component.html',
+ styleUrls: ['./total-downloads.component.scss']
+})
+export class TotalDownloadsComponent implements OnInit {
+
+ @Input() itemUuid: string;
+
+ totalDownloads: number | null = null;
+
+ label = 'item.page.files.downloads';
+
+ constructor(private usageReportDataService: UsageReportDataService) {}
+
+ ngOnInit(): void {
+ if (this.itemUuid) {
+ const reportType = 'TotalDownloads';
+ this.usageReportDataService.getStatistic(this.itemUuid, reportType)
+ .subscribe((report: UsageReport) => {
+ this.totalDownloads = report.points.reduce((total, point) => {
+ const values = point.values as any;
+ const views = values['views'] || values.views || 0;
+ return total + views;
+ }, 0);
+ });
+ }
+ }
+}
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html
index 584938f8cdf..5c00b504060 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/item-page/simple/item-types/publication/publication.component.html
@@ -23,7 +23,8 @@
-
+
+
-
+
+
Date: Thu, 21 Aug 2025 19:12:03 +0200
Subject: [PATCH 2/4] fixed Copilot's suggestions: any type & redundant
property access pattern
---
.../file-section/total-downloads.component.ts | 27 ++++++++++++++++---
1 file changed, 23 insertions(+), 4 deletions(-)
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 aeba7cc97d2..191c0287159 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,7 +1,23 @@
import { Component, OnInit, Input } from '@angular/core';
-import { UsageReport } from 'src/app/core/statistics/models/usage-report.model';
+import { UsageReport, Point } from 'src/app/core/statistics/models/usage-report.model';
import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service';
+/**
+ * Interface representing the actual structure of point.values as returned by the API
+ * This differs from the interface definition which shows it as an array (in usage-report.model.ts)
+ */
+interface PointValues {
+ views: number;
+ [key: string]: number; // Allow for other potential numeric properties
+}
+
+/**
+ * Extended Point interface with correct values typing
+ */
+interface DownloadPoint extends Omit {
+ values: PointValues;
+}
+
@Component({
selector: 'ds-total-downloads',
templateUrl: './total-downloads.component.html',
@@ -22,9 +38,12 @@ export class TotalDownloadsComponent implements OnInit {
const reportType = 'TotalDownloads';
this.usageReportDataService.getStatistic(this.itemUuid, reportType)
.subscribe((report: UsageReport) => {
- this.totalDownloads = report.points.reduce((total, point) => {
- const values = point.values as any;
- const views = values['views'] || values.views || 0;
+ // Type assertion to match the actual API response structure
+ // The API returns values as an object, not an array as defined in the interface
+ const points = report.points as unknown as DownloadPoint[];
+
+ this.totalDownloads = points.reduce((total, point) => {
+ const views = point.values.views || 0;
return total + views;
}, 0);
});
From 111b3fbf646b55a4f00adddc90761cc28d300a9f Mon Sep 17 00:00:00 2001
From: Juraj Roka <95219754+jr-rk@users.noreply.github.com>
Date: Thu, 21 Aug 2025 19:46:10 +0200
Subject: [PATCH 3/4] another copilots refactoring
---
.../statistics/models/usage-report.model.ts | 3 +-
.../file-section/total-downloads.component.ts | 47 ++++++++-----------
2 files changed, 21 insertions(+), 29 deletions(-)
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/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts
index 191c0287159..c378a87fd7b 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,22 +1,7 @@
import { Component, OnInit, Input } from '@angular/core';
-import { UsageReport, Point } from 'src/app/core/statistics/models/usage-report.model';
import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service';
-
-/**
- * Interface representing the actual structure of point.values as returned by the API
- * This differs from the interface definition which shows it as an array (in usage-report.model.ts)
- */
-interface PointValues {
- views: number;
- [key: string]: number; // Allow for other potential numeric properties
-}
-
-/**
- * Extended Point interface with correct values typing
- */
-interface DownloadPoint extends Omit {
- values: PointValues;
-}
+import { catchError } from 'rxjs/operators';
+import { of } from 'rxjs';
@Component({
selector: 'ds-total-downloads',
@@ -25,11 +10,11 @@ interface DownloadPoint extends Omit {
})
export class TotalDownloadsComponent implements OnInit {
- @Input() itemUuid: string;
+ @Input() itemUuid!: string;
totalDownloads: number | null = null;
- label = 'item.page.files.downloads';
+ readonly label = 'item.page.files.downloads';
constructor(private usageReportDataService: UsageReportDataService) {}
@@ -37,15 +22,21 @@ export class TotalDownloadsComponent implements OnInit {
if (this.itemUuid) {
const reportType = 'TotalDownloads';
this.usageReportDataService.getStatistic(this.itemUuid, reportType)
- .subscribe((report: UsageReport) => {
- // Type assertion to match the actual API response structure
- // The API returns values as an object, not an array as defined in the interface
- const points = report.points as unknown as DownloadPoint[];
-
- this.totalDownloads = points.reduce((total, point) => {
- const views = point.values.views || 0;
- return total + views;
- }, 0);
+ .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 = null;
+ }
});
}
}
From 3b636f456bc9ecaa8c1449098a8f64054e857eb5 Mon Sep 17 00:00:00 2001
From: Juraj Roka <95219754+jr-rk@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:32:20 +0200
Subject: [PATCH 4/4] Changed Milan's requests and comments
---
.../total-downloads.component.html | 4 +-
.../file-section/total-downloads.component.ts | 74 +++++++++++++------
2 files changed, 54 insertions(+), 24 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 498a810f42b..72db7109aa0 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,5 +1,5 @@
-
-
+
+
{{ 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 c378a87fd7b..d5cc17cbd08 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
@@ -3,6 +3,13 @@ import { UsageReportDataService } from 'src/app/core/statistics/usage-report-dat
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',
@@ -10,34 +17,57 @@ import { of } from 'rxjs';
})
export class TotalDownloadsComponent implements OnInit {
+ /**
+ * The UUID of the DSpace item for which to fetch download statistics.
+ */
@Input() itemUuid!: string;
- totalDownloads: number | null = null;
+ /**
+ * 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;
- readonly label = 'item.page.files.downloads';
+ /**
+ * The translation key for the downloadsLabel displayed alongside the download count.
+ */
+ readonly downloadsLabel = 'item.page.files.downloads';
- constructor(private usageReportDataService: UsageReportDataService) {}
+ 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) {
- 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 = null;
- }
- });
+ 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
+ }
+ });
}
}