Skip to content

Commit ea6ab24

Browse files
authored
UFAL/Cherrypick Total Downloads (#998)
* 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 * VSB-TUO/Make item view - Total Downloads - configurable (#996) * Implemented configurable showing of component * refactor: provide UsageReportDataService as singleton to avoid duplicate instances
1 parent 8de5005 commit ea6ab24

File tree

9 files changed

+138
-7
lines changed

9 files changed

+138
-7
lines changed

src/app/core/statistics/models/usage-report.model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ export interface Point {
4747
type: string;
4848
values: {
4949
views: number;
50-
}[];
50+
[key: string]: number;
51+
};
5152
}

src/app/core/statistics/usage-report-data.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { RequestParam } from '../cache/models/request-param.model';
2020
/**
2121
* A service to retrieve {@link UsageReport}s from the REST API
2222
*/
23-
@Injectable()
23+
@Injectable({ providedIn: 'root' })
2424
@dataService(USAGE_REPORT)
2525
export class UsageReportDataService extends IdentifiableDataService<UsageReport> implements SearchData<UsageReport> {
2626
private searchData: SearchDataImpl<UsageReport>;

src/app/item-page/item-page.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { ItemAlertsComponent } from './alerts/item-alerts.component';
5757
import { ItemVersionsModule } from './versions/item-versions.module';
5858
import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component';
5959
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
60+
import { TotalDownloadsComponent } from './simple/field-components/file-section/total-downloads.component';
6061
import { ItemSharedModule } from './item-shared.module';
6162
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
6263
import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component';
@@ -99,6 +100,7 @@ const ENTRY_COMPONENTS = [
99100

100101
const DECLARATIONS = [
101102
FileSectionComponent,
103+
TotalDownloadsComponent,
102104
ThemedFileSectionComponent,
103105
ItemPageComponent,
104106
ThemedItemPageComponent,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<ds-metadata-field-wrapper
2+
*ngIf="totalDownloadsEnabled | async"
3+
[label]="downloadsLabel | translate">
4+
<div class="total-downloads-section">
5+
<div class="total-downloads-count">
6+
<span>{{ totalDownloads }}</span>
7+
</div>
8+
</div>
9+
</ds-metadata-field-wrapper>

src/app/item-page/simple/field-components/file-section/total-downloads.component.scss

Whitespace-only changes.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Component, OnInit, Input } from '@angular/core';
2+
import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service';
3+
import { ConfigurationDataService } from 'src/app/core/data/configuration-data.service';
4+
import { catchError } from 'rxjs/operators';
5+
import { BehaviorSubject, of } from 'rxjs';
6+
7+
/**
8+
* Component that displays the total number of downloads for all bitstreams within a DSpace item.
9+
*
10+
* This component checks the 'item.view.total.downloads.enabled' configuration property
11+
* to determine if download statistics should be displayed. If enabled, it fetches download
12+
* statistics for a given item using its UUID and aggregates the download counts from all
13+
* bitstreams associated with that item. The result is displayed as a single total download count.
14+
*
15+
* If the configuration is disabled or set to 'false', the component will not be displayed.
16+
*/
17+
@Component({
18+
selector: 'ds-total-downloads',
19+
templateUrl: './total-downloads.component.html',
20+
styleUrls: ['./total-downloads.component.scss']
21+
})
22+
export class TotalDownloadsComponent implements OnInit {
23+
24+
/**
25+
* The UUID of the DSpace item for which to fetch download statistics.
26+
*/
27+
@Input() itemUuid!: string;
28+
29+
/**
30+
* The total number of downloads across all bitstreams for the item.
31+
* Defaults to 0 and will show 0 if no data is available or an error occurs.
32+
*/
33+
totalDownloads: number | null = 0;
34+
35+
/**
36+
* Flag indicating whether the total downloads feature is enabled in the configuration.
37+
* Uses BehaviorSubject to allow reactive updates. Defaults to false and will only be
38+
* set to true if the configuration explicitly contains 'true' value.
39+
*/
40+
totalDownloadsEnabled = new BehaviorSubject<boolean>(false);
41+
42+
/**
43+
* The translation key for the downloadsLabel displayed alongside the download count.
44+
*/
45+
readonly downloadsLabel = 'item.page.files.downloads';
46+
47+
48+
constructor(
49+
private usageReportDataService: UsageReportDataService,
50+
private configService: ConfigurationDataService
51+
) { }
52+
53+
/**
54+
* Fetches the configuration to check if total downloads should be shown,
55+
* and if enabled, fetches the total download statistics for the item specified by itemUuid.
56+
* The component will:
57+
* 1. Check the 'item.view.total.downloads.enabled' configuration property
58+
* 2. If enabled (configuration value is explicitly 'true'), call the UsageReportDataService
59+
* If configuration is not found or fails to load, defaults to false (disabled)
60+
* 3. Aggregate all download counts (views) from all bitstreams in the response
61+
* 4. Set the totalDownloads property with the sum
62+
* 5. Handle errors gracefully by returning null and logging the error
63+
*
64+
* @throws Will log an error to console if the API call fails, but won't throw an exception
65+
*/
66+
ngOnInit(): void {
67+
if (!this.itemUuid) {
68+
return;
69+
}
70+
71+
// First, check if total downloads feature is enabled in configuration
72+
this.configService.findByPropertyName('item.view.total.downloads.enabled')
73+
.pipe(
74+
catchError(error => {
75+
console.error('Failed to fetch total downloads configuration:', error);
76+
// Default to false if configuration cannot be retrieved
77+
return of(null);
78+
})
79+
)
80+
.subscribe(configData => {
81+
// Extract configuration value, default to 'false' if not found
82+
const itemViewTotalDownloadsEnabled = configData?.payload?.values?.[0];
83+
this.totalDownloadsEnabled.next(itemViewTotalDownloadsEnabled === 'true');
84+
85+
// Only fetch download statistics if the feature is enabled
86+
if (this.totalDownloadsEnabled.value) {
87+
this.fetchDownloadStatistics();
88+
} else {
89+
this.totalDownloads = null; // Ensure it's null when disabled
90+
}
91+
});
92+
}
93+
94+
/**
95+
* Private method to fetch download statistics from the usage report service.
96+
* This method is called only when the total downloads feature is enabled.
97+
*/
98+
private fetchDownloadStatistics(): void {
99+
const reportType = 'TotalDownloads';
100+
this.usageReportDataService.getStatistic(this.itemUuid, reportType)
101+
.pipe(
102+
catchError(error => {
103+
console.error('Failed to fetch total downloads statistics:', error);
104+
return of(null);
105+
})
106+
)
107+
.subscribe(report => {
108+
if (report) {
109+
this.totalDownloads = report.points.reduce((total, point) => {
110+
const views = point.values.views || 0;
111+
return total + views;
112+
}, 0);
113+
} else {
114+
this.totalDownloads = 0; // Show 0 instead of null when no data is available
115+
}
116+
});
117+
}
118+
}

src/app/statistics-page/statistics-page.module.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common';
44
import { CoreModule } from '../core/core.module';
55
import { SharedModule } from '../shared/shared.module';
66
import { StatisticsModule } from '../statistics/statistics.module';
7-
import { UsageReportDataService } from '../core/statistics/usage-report-data.service';
87
import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component';
98
import { StatisticsTableComponent } from './statistics-table/statistics-table.component';
109
import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component';
@@ -35,9 +34,6 @@ const components = [
3534
StatisticsModule.forRoot()
3635
],
3736
declarations: components,
38-
providers: [
39-
UsageReportDataService,
40-
],
4137
exports: components
4238
})
4339

src/assets/i18n/cs.json5

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3794,6 +3794,9 @@
37943794
// "item.page.files": "Files",
37953795
"item.page.files": "Soubory",
37963796

3797+
// "item.page.files.downloads": "Downloads",
3798+
"item.page.files.downloads": "Stažení",
3799+
37973800
// "item.page.filesection.description": "Description:",
37983801
"item.page.filesection.description": "Popis:",
37993802

@@ -9331,4 +9334,4 @@
93319334

93329335
// "clarin-license-agreement-page.download-error": "An error occurred while downloading the file. Please try again later.",
93339336
"clarin-license-agreement-page.download-error": "Při stahování souboru došlo k chybě. Zkuste to prosím znovu později.",
9334-
}
9337+
}

src/assets/i18n/en.json5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,6 +2529,8 @@
25292529

25302530
"item.page.files": "Files",
25312531

2532+
"item.page.files.downloads": "Downloads",
2533+
25322534
"item.page.filesection.description": "Description:",
25332535

25342536
"item.page.filesection.download": "Download",

0 commit comments

Comments
 (0)