Feature/support deleting symbol profile data (#669)

* Add support for deleting symbol profile data

* Update changelog
This commit is contained in:
Thomas Kaul 2022-02-03 20:56:39 +01:00 committed by GitHub
parent 5d8bde5a70
commit dc424a86ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 176 additions and 44 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for deleting symbol profile data in the admin control panel
### Changed ### Changed
- Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) - Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`)

View File

@ -11,6 +11,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
HttpException, HttpException,
Inject, Inject,
@ -195,9 +196,10 @@ export class AdminController {
return this.adminService.getMarketData(); return this.adminService.getMarketData();
} }
@Get('market-data/:symbol') @Get('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getMarketDataBySymbol( public async getMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> { ): Promise<AdminMarketDataDetails> {
if ( if (
@ -212,7 +214,7 @@ export class AdminController {
); );
} }
return this.adminService.getMarketDataBySymbol(symbol); return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
} }
@Put('market-data/:dataSource/:symbol/:dateString') @Put('market-data/:dataSource/:symbol/:dateString')
@ -248,6 +250,27 @@ export class AdminController {
}); });
} }
@Delete('profile-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
public async deleteProfileData(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
if (
!hasPermission(
this.request.user.permissions,
permissions.accessAdminControl
)
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.adminService.deleteProfileData({ dataSource, symbol });
}
@Put('settings/:key') @Put('settings/:key')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async updateProperty( public async updateProperty(

View File

@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller'; import { AdminController } from './admin.controller';
@ -20,7 +21,8 @@ import { AdminService } from './admin.service';
MarketDataModule, MarketDataModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,
SubscriptionModule SubscriptionModule,
SymbolProfileModule
], ],
controllers: [AdminController], controllers: [AdminController],
providers: [AdminService], providers: [AdminService],

View File

@ -5,6 +5,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
import { import {
AdminData, AdminData,
@ -13,7 +14,7 @@ import {
AdminMarketDataItem AdminMarketDataItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client'; import { DataSource, Property } from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
@Injectable() @Injectable()
@ -25,9 +26,21 @@ export class AdminService {
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService private readonly subscriptionService: SubscriptionService,
private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async deleteProfileData({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
await this.marketDataService.deleteMany({ dataSource, symbol });
await this.symbolProfileService.delete({ dataSource, symbol });
}
public async get(): Promise<AdminData> { public async get(): Promise<AdminData> {
return { return {
dataGatheringProgress: dataGatheringProgress:
@ -121,16 +134,21 @@ export class AdminService {
}; };
} }
public async getMarketDataBySymbol( public async getMarketDataBySymbol({
aSymbol: string dataSource,
): Promise<AdminMarketDataDetails> { symbol
}: {
dataSource: DataSource;
symbol: string;
}): Promise<AdminMarketDataDetails> {
return { return {
marketData: await this.marketDataService.marketDataItems({ marketData: await this.marketDataService.marketDataItems({
orderBy: { orderBy: {
date: 'asc' date: 'asc'
}, },
where: { where: {
symbol: aSymbol dataSource,
symbol
} }
}) })
}; };

View File

@ -9,6 +9,21 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
export class MarketDataService { export class MarketDataService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async deleteMany({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.prismaService.marketData.deleteMany({
where: {
dataSource,
symbol
}
});
}
public async get({ public async get({
date, date,
symbol symbol

View File

@ -4,14 +4,26 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, SymbolProfile } from '@prisma/client'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
import { continents, countries } from 'countries-list'; import { continents, countries } from 'countries-list';
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface'; import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
@Injectable() @Injectable()
export class SymbolProfileService { export class SymbolProfileService {
constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async delete({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.prismaService.symbolProfile.delete({
where: { dataSource_symbol: { dataSource, symbol } }
});
}
public async getSymbolProfiles( public async getSymbolProfiles(
symbols: string[] symbols: string[]

View File

@ -20,6 +20,7 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './admin-market-data.html' templateUrl: './admin-market-data.html'
}) })
export class AdminMarketDataComponent implements OnDestroy, OnInit { export class AdminMarketDataComponent implements OnDestroy, OnInit {
public currentDataSource: DataSource;
public currentSymbol: string; public currentSymbol: string;
public defaultDateFormat = DEFAULT_DATE_FORMAT; public defaultDateFormat = DEFAULT_DATE_FORMAT;
public marketData: AdminMarketDataItem[] = []; public marketData: AdminMarketDataItem[] = [];
@ -43,6 +44,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
} }
public onDeleteProfileData({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService
.deleteProfileData({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
public onGatherProfileDataBySymbol({ public onGatherProfileDataBySymbol({
dataSource, dataSource,
symbol symbol
@ -69,22 +83,33 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
.subscribe(() => {}); .subscribe(() => {});
} }
public setCurrentSymbol(aSymbol: string) {
this.marketDataDetails = [];
if (this.currentSymbol === aSymbol) {
this.currentSymbol = '';
} else {
this.currentSymbol = aSymbol;
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
}
}
public onMarketDataChanged(withRefresh: boolean = false) { public onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) { if (withRefresh) {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
this.fetchAdminMarketDataBySymbol(this.currentSymbol); this.fetchAdminMarketDataBySymbol({
dataSource: this.currentDataSource,
symbol: this.currentSymbol
});
}
}
public setCurrentProfile({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.marketDataDetails = [];
if (this.currentSymbol === symbol) {
this.currentDataSource = undefined;
this.currentSymbol = '';
} else {
this.currentDataSource = dataSource;
this.currentSymbol = symbol;
this.fetchAdminMarketDataBySymbol({ dataSource, symbol });
} }
} }
@ -104,9 +129,15 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}); });
} }
private fetchAdminMarketDataBySymbol(aSymbol: string) { private fetchAdminMarketDataBySymbol({
this.dataService dataSource,
.fetchAdminMarketDataBySymbol(aSymbol) symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketData }) => { .subscribe(({ marketData }) => {
this.marketDataDetails = marketData; this.marketDataDetails = marketData;

View File

@ -16,7 +16,7 @@
<ng-container *ngFor="let item of marketData; let i = index"> <ng-container *ngFor="let item of marketData; let i = index">
<tr <tr
class="cursor-pointer mat-row" class="cursor-pointer mat-row"
(click)="setCurrentSymbol(item.symbol)" (click)="setCurrentProfile({ dataSource: item.dataSource, symbol: item.symbol })"
> >
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td> <td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td> <td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
@ -49,11 +49,19 @@
> >
Gather Profile Data Gather Profile Data
</button> </button>
<button
i18n
mat-menu-item
[disabled]="item.activityCount !== 0"
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
>
Delete Profile Data
</button>
</mat-menu> </mat-menu>
</td> </td>
</tr> </tr>
<tr *ngIf="currentSymbol === item.symbol" class="mat-row"> <tr *ngIf="currentSymbol === item.symbol" class="mat-row">
<td class="p-1" colspan="4"> <td class="p-1" colspan="6">
<gf-admin-market-data-detail <gf-admin-market-data-detail
[dataSource]="item.dataSource" [dataSource]="item.dataSource"
[marketData]="marketDataDetails" [marketData]="marketDataDetails"

View File

@ -3,8 +3,10 @@ import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { map, Observable } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -12,6 +14,37 @@ import { format } from 'date-fns';
export class AdminService { export class AdminService {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public deleteProfileData({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.delete<void>(
`/api/admin/profile-data/${dataSource}/${symbol}`
);
}
public fetchAdminMarketDataBySymbol({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): Observable<AdminMarketDataDetails> {
return this.http
.get<any>(`/api/admin/market-data/${dataSource}/${symbol}`)
.pipe(
map((data) => {
for (const item of data.marketData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public gatherMax() { public gatherMax() {
return this.http.post<void>(`/api/admin/gather/max`, {}); return this.http.post<void>(`/api/admin/gather/max`, {});
} }

View File

@ -18,7 +18,6 @@ import {
Accounts, Accounts,
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails,
Export, Export,
InfoItem, InfoItem,
PortfolioChart, PortfolioChart,
@ -69,19 +68,6 @@ export class DataService {
return this.http.get<AdminMarketData>('/api/admin/market-data'); return this.http.get<AdminMarketData>('/api/admin/market-data');
} }
public fetchAdminMarketDataBySymbol(
aSymbol: string
): Observable<AdminMarketDataDetails> {
return this.http.get<any>(`/api/admin/market-data/${aSymbol}`).pipe(
map((data) => {
for (const item of data.marketData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public deleteAccess(aId: string) { public deleteAccess(aId: string) {
return this.http.delete<any>(`/api/access/${aId}`); return this.http.delete<any>(`/api/access/${aId}`);
} }