mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
Merge pull request #685 from bpatrik/top-pick-sending
Top pick sending per e-mail
This commit is contained in:
commit
065c57f897
47
package-lock.json
generated
47
package-lock.json
generated
@ -23,11 +23,12 @@
|
||||
"image-size": "1.0.2",
|
||||
"locale": "0.1.0",
|
||||
"node-geocoder": "4.2.0",
|
||||
"nodemailer": "6.9.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"sharp": "0.31.3",
|
||||
"ts-exif-parser": "0.2.2",
|
||||
"ts-node-iptc": "1.0.11",
|
||||
"typeconfig": "2.1.0",
|
||||
"typeconfig": "2.1.2",
|
||||
"typeorm": "0.3.12",
|
||||
"xml2js": "0.4.23"
|
||||
},
|
||||
@ -75,6 +76,7 @@
|
||||
"@types/leaflet.markercluster": "1.5.1",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-geocoder": "4.2.0",
|
||||
"@types/nodemailer": "6.4.9",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/xml2js": "0.4.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
@ -4891,6 +4893,15 @@
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
"integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@ -16598,6 +16609,14 @@
|
||||
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
|
||||
"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/non-layered-tidy-tree-layout": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
|
||||
@ -21670,9 +21689,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typeconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.0.tgz",
|
||||
"integrity": "sha512-2Sn2lB8nG9lOvy2jY/4U0HCkqJqc7Fpf8uF5hXaB/+YVnjexX05bfOxUpxIB0fh+Qob7TrVkHxt/2R3aacj8Cw==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.2.tgz",
|
||||
"integrity": "sha512-4Zw3q9LmJ3OeVFMxpvbRYpcrR5mG7TTZUr+bYPepXvWCuqLC4fXLD+cjS1hl8e1oSmum6Xfl4vheFx33VevlqA==",
|
||||
"dependencies": {
|
||||
"minimist": "1.2.8"
|
||||
}
|
||||
@ -26709,6 +26728,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
"integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@ -35797,6 +35825,11 @@
|
||||
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==",
|
||||
"dev": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
|
||||
"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA=="
|
||||
},
|
||||
"non-layered-tidy-tree-layout": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
|
||||
@ -39736,9 +39769,9 @@
|
||||
}
|
||||
},
|
||||
"typeconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.0.tgz",
|
||||
"integrity": "sha512-2Sn2lB8nG9lOvy2jY/4U0HCkqJqc7Fpf8uF5hXaB/+YVnjexX05bfOxUpxIB0fh+Qob7TrVkHxt/2R3aacj8Cw==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.1.2.tgz",
|
||||
"integrity": "sha512-4Zw3q9LmJ3OeVFMxpvbRYpcrR5mG7TTZUr+bYPepXvWCuqLC4fXLD+cjS1hl8e1oSmum6Xfl4vheFx33VevlqA==",
|
||||
"requires": {
|
||||
"minimist": "1.2.8"
|
||||
}
|
||||
|
@ -46,13 +46,14 @@
|
||||
"image-size": "1.0.2",
|
||||
"locale": "0.1.0",
|
||||
"node-geocoder": "4.2.0",
|
||||
"nodemailer": "6.9.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"sharp": "0.31.3",
|
||||
"ts-exif-parser": "0.2.2",
|
||||
"ts-node-iptc": "1.0.11",
|
||||
"typeconfig": "2.1.0",
|
||||
"xml2js": "0.4.23",
|
||||
"typeorm": "0.3.12"
|
||||
"typeconfig": "2.1.2",
|
||||
"typeorm": "0.3.12",
|
||||
"xml2js": "0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "15.0.0",
|
||||
@ -95,6 +96,7 @@
|
||||
"@types/leaflet.markercluster": "1.5.1",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-geocoder": "4.2.0",
|
||||
"@types/nodemailer": "6.4.9",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/xml2js": "0.4.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
|
5
src/backend/Environment.ts
Normal file
5
src/backend/Environment.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Keeps the environment context
|
||||
* Only use it in the Config constructor
|
||||
*/
|
||||
export const ServerEnvironment: { sendMailAvailable?: boolean } = {};
|
@ -3,9 +3,7 @@ import {promises as fsp} from 'fs';
|
||||
import * as archiver from 'archiver';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||
import {
|
||||
ParentDirectoryDTO,
|
||||
} from '../../common/entities/DirectoryDTO';
|
||||
import {ParentDirectoryDTO,} from '../../common/entities/DirectoryDTO';
|
||||
import {ObjectManagers} from '../model/ObjectManagers';
|
||||
import {ContentWrapper} from '../../common/entities/ConentWrapper';
|
||||
import {ProjectPath} from '../ProjectPath';
|
||||
@ -14,13 +12,11 @@ import {UserDTOUtils} from '../../common/entities/UserDTO';
|
||||
import {MediaDTO, MediaDTOUtils} from '../../common/entities/MediaDTO';
|
||||
import {QueryParams} from '../../common/QueryParams';
|
||||
import {VideoProcessing} from '../model/fileprocessing/VideoProcessing';
|
||||
import {
|
||||
SearchQueryDTO,
|
||||
SearchQueryTypes,
|
||||
} from '../../common/entities/SearchQueryDTO';
|
||||
import {SearchQueryDTO, SearchQueryTypes,} from '../../common/entities/SearchQueryDTO';
|
||||
import {LocationLookupException} from '../exceptions/LocationLookupException';
|
||||
import {SupportedFormats} from '../../common/SupportedFormats';
|
||||
import {ServerTime} from './ServerTimingMWs';
|
||||
import {SortingMethods} from '../../common/entities/SortingMethods';
|
||||
|
||||
export class GalleryMWs {
|
||||
@ServerTime('1.db', 'List Directory')
|
||||
@ -325,16 +321,16 @@ export class GalleryMWs {
|
||||
req.params['searchQueryDTO'] as string
|
||||
);
|
||||
|
||||
const photo =
|
||||
await ObjectManagers.getInstance().SearchManager.getRandomPhoto(query);
|
||||
if (!photo) {
|
||||
const photos =
|
||||
await ObjectManagers.getInstance().SearchManager.getNMedia(query, [SortingMethods.random], 1, true);
|
||||
if (!photos || photos.length !== 1) {
|
||||
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'No photo found'));
|
||||
}
|
||||
|
||||
req.params['mediaPath'] = path.join(
|
||||
photo.directory.path,
|
||||
photo.directory.name,
|
||||
photo.name
|
||||
photos[0].directory.path,
|
||||
photos[0].directory.name,
|
||||
photos[0].name
|
||||
);
|
||||
return next();
|
||||
} catch (e) {
|
||||
|
@ -9,6 +9,7 @@ import {SharingDTO} from '../../common/entities/SharingDTO';
|
||||
import {Utils} from '../../common/Utils';
|
||||
import {LoggerRouter} from '../routes/LoggerRouter';
|
||||
import {TAGS} from '../../common/config/public/ClientConfig';
|
||||
import {ConfigDiagnostics} from '../model/diagnostics/ConfigDiagnostics';
|
||||
|
||||
const forcedDebug = process.env['NODE_ENV'] === 'debug';
|
||||
|
||||
|
@ -27,7 +27,6 @@ export class SettingsMWs {
|
||||
try {
|
||||
let settings = req.body.settings; // Top level settings JSON
|
||||
const settingsPath: string = req.body.settingsPath; // Name of the top level settings
|
||||
|
||||
const transformer = await Config.original();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
@ -30,11 +30,11 @@ import {
|
||||
} from '../../../common/entities/SearchQueryDTO';
|
||||
import {GalleryManager} from './GalleryManager';
|
||||
import {ObjectManagers} from '../ObjectManagers';
|
||||
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
|
||||
import {DatabaseType} from '../../../common/config/private/PrivateConfig';
|
||||
import {Utils} from '../../../common/Utils';
|
||||
import {FileEntity} from './enitites/FileEntity';
|
||||
import {SQL_COLLATE} from './enitites/EntityUtils';
|
||||
import {SortingMethods} from '../../../common/entities/SortingMethods';
|
||||
|
||||
export class SearchManager {
|
||||
private DIRECTORY_SELECT = [
|
||||
@ -348,19 +348,62 @@ export class SearchManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getRandomPhoto(query: SearchQueryDTO): Promise<PhotoDTO> {
|
||||
|
||||
private static setSorting<T>(
|
||||
query: SelectQueryBuilder<T>,
|
||||
sortings: SortingMethods[]
|
||||
): SelectQueryBuilder<T> {
|
||||
if (!sortings || !Array.isArray(sortings)) {
|
||||
return query;
|
||||
}
|
||||
if (sortings.includes(SortingMethods.random) && sortings.length > 1) {
|
||||
throw new Error('Error during applying sorting: Can\' randomize and also sort the result. Bad input:' + sortings.map(s => SortingMethods[s]).join(', '));
|
||||
}
|
||||
for (const sort of sortings) {
|
||||
switch (sort) {
|
||||
case SortingMethods.descDate:
|
||||
query.addOrderBy('media.metadata.creationDate', 'DESC');
|
||||
break;
|
||||
case SortingMethods.ascDate:
|
||||
query.addOrderBy('media.metadata.creationDate', 'ASC');
|
||||
break;
|
||||
case SortingMethods.descRating:
|
||||
query.addOrderBy('media.metadata.rating', 'DESC');
|
||||
break;
|
||||
case SortingMethods.ascRating:
|
||||
query.addOrderBy('media.metadata.rating', 'ASC');
|
||||
break;
|
||||
case SortingMethods.descName:
|
||||
query.addOrderBy('media.name', 'DESC');
|
||||
break;
|
||||
case SortingMethods.ascName:
|
||||
query.addOrderBy('media.name', 'ASC');
|
||||
break;
|
||||
case SortingMethods.random:
|
||||
if (Config.Database.type === DatabaseType.mysql) {
|
||||
query.groupBy('RAND(), media.id');
|
||||
} else {
|
||||
query.groupBy('RANDOM()');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public async getNMedia(query: SearchQueryDTO, sortings: SortingMethods[], take: number, photoOnly = false) {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const sqlQuery: SelectQueryBuilder<PhotoEntity> = connection
|
||||
.getRepository(PhotoEntity)
|
||||
.getRepository(photoOnly ? PhotoEntity : MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.select(['media', ...this.DIRECTORY_SELECT])
|
||||
.innerJoin('media.directory', 'directory')
|
||||
.where(await this.prepareAndBuildWhereQuery(query));
|
||||
SearchManager.setSorting(sqlQuery, sortings);
|
||||
|
||||
return sqlQuery.limit(take).getMany();
|
||||
|
||||
if (Config.Database.type === DatabaseType.mysql) {
|
||||
return await sqlQuery.groupBy('RAND(), media.id').limit(1).getOne();
|
||||
}
|
||||
return await sqlQuery.groupBy('RANDOM()').limit(1).getOne();
|
||||
}
|
||||
|
||||
public async getCount(query: SearchQueryDTO): Promise<number> {
|
||||
|
@ -26,6 +26,9 @@ import {
|
||||
import {SearchQueryParser} from '../../../common/SearchQueryParser';
|
||||
import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO';
|
||||
import {Utils} from '../../../common/Utils';
|
||||
import {createTransport} from 'nodemailer';
|
||||
import {EmailMessagingType, MessagingConfig} from '../../../common/config/private/MessagingConfig';
|
||||
import {ServerEnvironment} from '../../Environment';
|
||||
|
||||
const LOG_TAG = '[ConfigDiagnostics]';
|
||||
|
||||
@ -79,6 +82,14 @@ export class ConfigDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
private static async testEmailMessagingConfig(Messaging: MessagingConfig, config: PrivateConfigClass): Promise<void> {
|
||||
Logger.debug(LOG_TAG, 'Testing EmailMessaging config');
|
||||
|
||||
if (Messaging.Email.type === EmailMessagingType.sendmail && ServerEnvironment.sendMailAvailable === false) {
|
||||
throw new Error('sendmail e-mail sending method is not supported as the sendmail application cannot be found in the OS.');
|
||||
}
|
||||
}
|
||||
|
||||
static testVideoConfig(videoConfig: ServerVideoConfig,
|
||||
config: PrivateConfigClass): Promise<void> {
|
||||
Logger.debug(LOG_TAG, 'Testing video config with ffmpeg test');
|
||||
@ -285,15 +296,49 @@ export class ConfigDiagnostics {
|
||||
await ConfigDiagnostics.testSharingConfig(config.Sharing, config);
|
||||
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
|
||||
await ConfigDiagnostics.testMapConfig(config.Map);
|
||||
await ConfigDiagnostics.testEmailMessagingConfig(config.Messaging, config);
|
||||
|
||||
}
|
||||
|
||||
static async checkAndSetEnvironment(): Promise<void> {
|
||||
Logger.debug(LOG_TAG, 'Checking sendmail availability');
|
||||
const transporter = createTransport({
|
||||
sendmail: true,
|
||||
});
|
||||
try {
|
||||
ServerEnvironment.sendMailAvailable = await transporter.verify();
|
||||
} catch (e) {
|
||||
ServerEnvironment.sendMailAvailable = false;
|
||||
}
|
||||
Config.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable;
|
||||
if (!ServerEnvironment.sendMailAvailable) {
|
||||
Config.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
Logger.info(LOG_TAG, 'Sendmail is not available on the OS. You will need to use an SMTP server if you wish the app to send mails.');
|
||||
}
|
||||
}
|
||||
|
||||
static async runDiagnostics(): Promise<void> {
|
||||
|
||||
if (process.env['NODE_ENV'] === 'debug') {
|
||||
NotificationManager.warning('You are running the application with NODE_ENV=debug. This exposes a lot of debug information that can be a security vulnerability. Set NODE_ENV=production, when you finished debugging.');
|
||||
}
|
||||
|
||||
try {
|
||||
await ConfigDiagnostics.checkAndSetEnvironment();
|
||||
} catch (ex) {
|
||||
const err: Error = ex;
|
||||
NotificationManager.error(
|
||||
'Error during checking environment',
|
||||
err.toString()
|
||||
);
|
||||
Logger.error(
|
||||
LOG_TAG,
|
||||
'Error during checking environment',
|
||||
err.toString()
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await ConfigDiagnostics.testDatabase(Config.Database);
|
||||
} catch (ex) {
|
||||
@ -525,5 +570,23 @@ export class ConfigDiagnostics {
|
||||
);
|
||||
Config.Map.mapProvider = MapProviders.OpenStreetMap;
|
||||
}
|
||||
try {
|
||||
await ConfigDiagnostics.testEmailMessagingConfig(Config.Messaging, Config);
|
||||
} catch (ex) {
|
||||
const err: Error = ex;
|
||||
NotificationManager.warning(
|
||||
'Setting to SMTP method.',
|
||||
err.toString()
|
||||
);
|
||||
Logger.warn(
|
||||
LOG_TAG,
|
||||
'Setting to SMTP method.',
|
||||
err.toString()
|
||||
);
|
||||
Config.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {PreviewRestJob} from './jobs/PreviewResetJob';
|
||||
import {GPXCompressionJob} from './jobs/GPXCompressionJob';
|
||||
import {AlbumRestJob} from './jobs/AlbumResetJob';
|
||||
import {GPXCompressionResetJob} from './jobs/GPXCompressionResetJob';
|
||||
import {TopPickSendJob} from './jobs/TopPickSendJob';
|
||||
|
||||
export class JobRepository {
|
||||
private static instance: JobRepository = null;
|
||||
@ -45,3 +46,4 @@ JobRepository.Instance.register(new GPXCompressionJob());
|
||||
JobRepository.Instance.register(new TempFolderCleaningJob());
|
||||
JobRepository.Instance.register(new AlbumRestJob());
|
||||
JobRepository.Instance.register(new GPXCompressionResetJob());
|
||||
JobRepository.Instance.register(new TopPickSendJob());
|
||||
|
@ -154,6 +154,7 @@ export abstract class Job<T extends Record<string, any> = Record<string, any>> i
|
||||
this.run();
|
||||
} catch (e) {
|
||||
Logger.error(LOG_TAG, e);
|
||||
this.Progress.State = JobProgressStates.failed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { ObjectManagers } from '../../ObjectManagers';
|
||||
import {
|
||||
ConfigTemplateEntry,
|
||||
DefaultsJobs,
|
||||
} from '../../../../common/entities/job/JobDTO';
|
||||
import { Job } from './Job';
|
||||
import { Config } from '../../../../common/config/private/Config';
|
||||
import { DatabaseType } from '../../../../common/config/private/PrivateConfig';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
|
||||
export class PreviewFillingJob extends Job {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Preview Filling']];
|
||||
@ -18,7 +13,7 @@ export class PreviewFillingJob extends Job {
|
||||
}
|
||||
|
||||
protected async init(): Promise<void> {
|
||||
// abstract function
|
||||
this.status = 'Persons';
|
||||
}
|
||||
|
||||
protected async step(): Promise<boolean> {
|
||||
|
116
src/backend/model/jobs/jobs/TopPickSendJob.ts
Normal file
116
src/backend/model/jobs/jobs/TopPickSendJob.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
|
||||
import {Job} from './Job';
|
||||
import {backendTexts} from '../../../../common/BackendTexts';
|
||||
import {SortingMethods} from '../../../../common/entities/SortingMethods';
|
||||
import {DatePatternFrequency, DatePatternSearch, SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {PhotoEntity} from '../../database/enitites/PhotoEntity';
|
||||
import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger';
|
||||
|
||||
|
||||
export class TopPickSendJob extends Job<{
|
||||
searchQuery: SearchQueryDTO,
|
||||
sortBy: SortingMethods[],
|
||||
pickAmount: number,
|
||||
emailTo: string,
|
||||
emailFrom: string,
|
||||
emailSubject: string,
|
||||
emailText: string,
|
||||
}> {
|
||||
public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']];
|
||||
public readonly Supported: boolean = true;
|
||||
public readonly ConfigTemplate: ConfigTemplateEntry[] = [
|
||||
{
|
||||
id: 'searchQuery',
|
||||
type: 'SearchQuery',
|
||||
name: backendTexts.searchQuery.name,
|
||||
description: backendTexts.searchQuery.description,
|
||||
defaultValue: {
|
||||
type: SearchQueryTypes.date_pattern,
|
||||
daysLength: 7,
|
||||
frequency: DatePatternFrequency.every_year
|
||||
} as DatePatternSearch,
|
||||
}, {
|
||||
id: 'sortby',
|
||||
type: 'sort-array',
|
||||
name: backendTexts.sortBy.name,
|
||||
description: backendTexts.sortBy.description,
|
||||
defaultValue: [SortingMethods.descRating],
|
||||
}, {
|
||||
id: 'pickAmount',
|
||||
type: 'number',
|
||||
name: backendTexts.pickAmount.name,
|
||||
description: backendTexts.pickAmount.description,
|
||||
defaultValue: 5,
|
||||
}, {
|
||||
id: 'emailTo',
|
||||
type: 'email',
|
||||
name: backendTexts.emailTo.name,
|
||||
description: backendTexts.emailTo.description,
|
||||
defaultValue: '',
|
||||
}, {
|
||||
id: 'emailFrom',
|
||||
type: 'email',
|
||||
name: backendTexts.emailFrom.name,
|
||||
description: backendTexts.emailFrom.description,
|
||||
defaultValue: 'norelpy@pigallery2.com',
|
||||
}, {
|
||||
id: 'emailSubject',
|
||||
type: 'string',
|
||||
name: backendTexts.emailSubject.name,
|
||||
description: backendTexts.emailSubject.description,
|
||||
defaultValue: 'Latest photos for you',
|
||||
}, {
|
||||
id: 'emailText',
|
||||
type: 'string',
|
||||
name: backendTexts.emailText.name,
|
||||
description: backendTexts.emailText.description,
|
||||
defaultValue: 'I hand picked these photos just for you:',
|
||||
},
|
||||
];
|
||||
private status: 'Listing' | 'Sending' = 'Listing';
|
||||
private mediaList: PhotoEntity[] = [];
|
||||
|
||||
|
||||
protected async init(): Promise<void> {
|
||||
this.status = 'Listing';
|
||||
this.mediaList = [];
|
||||
this.Progress.Left = 2;
|
||||
}
|
||||
|
||||
|
||||
protected async step(): Promise<boolean> {
|
||||
|
||||
switch (this.status) {
|
||||
case 'Listing':
|
||||
if (!await this.stepListing()) {
|
||||
this.status = 'Sending';
|
||||
}
|
||||
return true;
|
||||
case 'Sending':
|
||||
await this.stepSending();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async stepListing(): Promise<boolean> {
|
||||
this.Progress.log('Collecting Photos and videos to Send');
|
||||
this.Progress.Processed++;
|
||||
this.mediaList = await ObjectManagers.getInstance().SearchManager.getNMedia(this.config.searchQuery, this.config.sortBy, this.config.pickAmount);
|
||||
// console.log(this.mediaList);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async stepSending(): Promise<boolean> {
|
||||
this.Progress.log('Sending emails');
|
||||
const messenger = new EmailMediaMessenger();
|
||||
await messenger.sendMedia({
|
||||
from: this.config.emailFrom,
|
||||
to: this.config.emailTo,
|
||||
subject: this.config.emailSubject,
|
||||
text: this.config.emailText
|
||||
}, this.mediaList);
|
||||
this.Progress.Processed++;
|
||||
return false;
|
||||
}
|
||||
}
|
94
src/backend/model/mediamessengers/EmailMediaMessenger.ts
Normal file
94
src/backend/model/mediamessengers/EmailMediaMessenger.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import {createTransport, Transporter} from 'nodemailer';
|
||||
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
|
||||
import {Config} from '../../../common/config/private/Config';
|
||||
import {EmailMessagingType} from '../../../common/config/private/MessagingConfig';
|
||||
import {PhotoProcessing} from '../fileprocessing/PhotoProcessing';
|
||||
import {ThumbnailSourceType} from '../threading/PhotoWorker';
|
||||
import {ProjectPath} from '../../ProjectPath';
|
||||
import * as path from 'path';
|
||||
import {PhotoMetadata} from '../../../common/entities/PhotoDTO';
|
||||
import {Utils} from '../../../common/Utils';
|
||||
|
||||
export class EmailMediaMessenger {
|
||||
transporter: Transporter;
|
||||
|
||||
constructor() {
|
||||
if (Config.Messaging.Email.type === EmailMessagingType.sendmail) {
|
||||
this.transporter = createTransport({
|
||||
sendmail: true
|
||||
});
|
||||
} else {
|
||||
this.transporter = createTransport({
|
||||
host: Config.Messaging.Email.smtp.host,
|
||||
port: Config.Messaging.Email.smtp.port,
|
||||
secure: Config.Messaging.Email.smtp.secure,
|
||||
requireTLS: Config.Messaging.Email.smtp.requireTLS,
|
||||
auth: {
|
||||
user: Config.Messaging.Email.smtp.user,
|
||||
pass: Config.Messaging.Email.smtp.password
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getThumbnail(m: MediaDTO) {
|
||||
return await PhotoProcessing.generateThumbnail(
|
||||
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
|
||||
Config.Media.Thumbnail.thumbnailSizes[0],
|
||||
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public async sendMedia(mailSettings: {
|
||||
from: string,
|
||||
to: string,
|
||||
subject: string,
|
||||
text: string
|
||||
}, media: MediaDTO[]) {
|
||||
|
||||
const attachments = [];
|
||||
const htmlStart = '<h1 style="text-align: center; margin-bottom: 2em">' + Config.Server.applicationTitle + '</h1>\n' +
|
||||
'<h3>' + mailSettings.text + '</h3>\n' +
|
||||
'<table style="margin-left: auto; margin-right: auto;">\n' +
|
||||
' <tbody>\n';
|
||||
const htmlEnd = ' </tr>\n' +
|
||||
' </tbody>\n' +
|
||||
'</table>';
|
||||
let htmlMiddle = '';
|
||||
for (let i = 0; i < media.length; ++i) {
|
||||
const thPath = await this.getThumbnail(media[i]);
|
||||
const linkUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', path.join(media[i].directory.path, media[i].directory.name));
|
||||
const location = (media[0].metadata as PhotoMetadata).positionData?.country ?
|
||||
(media[0].metadata as PhotoMetadata).positionData?.country :
|
||||
((media[0].metadata as PhotoMetadata).positionData?.city ?
|
||||
(media[0].metadata as PhotoMetadata).positionData?.city : '');
|
||||
const caption = (new Date(media[0].metadata.creationDate)).getFullYear() + (location ? ', ' + location : '');
|
||||
attachments.push({
|
||||
filename: media[i].name,
|
||||
path: thPath,
|
||||
cid: 'img' + i
|
||||
});
|
||||
if (i % 2 == 0) {
|
||||
htmlMiddle += '<tr>';
|
||||
}
|
||||
htmlMiddle += '<td>\n' +
|
||||
' <a style="display: block;text-align: center;" href="' + linkUrl + '"><img alt="' + media[i].name + '" style="max-width: 200px; height: 150px" src="cid:img' + i + '"/></a>\n' +
|
||||
caption +
|
||||
' </td>\n';
|
||||
|
||||
if (i % 2 == 1 || i === media.length - 1) {
|
||||
htmlMiddle += '</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
return await this.transporter.sendMail({
|
||||
from: mailSettings.from,
|
||||
to: mailSettings.to,
|
||||
subject: mailSettings.subject,
|
||||
html: htmlStart + htmlMiddle + htmlEnd,
|
||||
attachments: attachments
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,14 @@
|
||||
export type backendText = number;
|
||||
export const backendTexts = {
|
||||
indexedFilesOnly: { name: 10, description: 12 },
|
||||
sizeToGenerate: { name: 20, description: 22 },
|
||||
indexChangesOnly: { name: 30, description: 32 },
|
||||
indexedFilesOnly: {name: 10, description: 12},
|
||||
sizeToGenerate: {name: 20, description: 22},
|
||||
indexChangesOnly: {name: 30, description: 32},
|
||||
searchQuery: {name: 40, description: 42},
|
||||
sortBy: {name: 50, description: 52},
|
||||
pickAmount: {name: 60, description: 62},
|
||||
emailTo: {name: 70, description: 72},
|
||||
emailFrom: {name: 80, description: 82},
|
||||
emailSubject: {name: 90, description: 92},
|
||||
emailText: {name: 100, description: 102}
|
||||
|
||||
};
|
||||
|
@ -6,6 +6,8 @@ import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
|
||||
import {IConfigClass} from 'typeconfig/common';
|
||||
import {PasswordHelper} from '../../../backend/model/PasswordHelper';
|
||||
import {TAGS} from '../public/ClientConfig';
|
||||
import {ServerEnvironment} from '../../../backend/Environment';
|
||||
import {EmailMessagingType} from './MessagingConfig';
|
||||
|
||||
declare const process: any;
|
||||
|
||||
@ -16,7 +18,8 @@ const isTesting = ['afterEach', 'after', 'beforeEach', 'before', 'describe', 'it
|
||||
.every((fn) => (global as any)[fn] instanceof Function);
|
||||
|
||||
@ConfigClass<IConfigClass<TAGS> & ServerConfig>({
|
||||
configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/assets/config.json'),
|
||||
configPath: path.join(__dirname, !isTesting ? './../../../../config.json' : './../../../../test/backend/tmp/config.json'),
|
||||
crateConfigPathIfNotExists: isTesting,
|
||||
saveIfNotExist: true,
|
||||
attachDescription: true,
|
||||
enumsAsString: true,
|
||||
@ -79,6 +82,12 @@ export class PrivateConfigClass extends ServerConfig {
|
||||
require('../../../../package.json').buildCommitHash;
|
||||
this.Environment.upTime = upTime;
|
||||
this.Environment.isDocker = !!process.env.PI_DOCKER;
|
||||
if (typeof ServerEnvironment.sendMailAvailable !== 'undefined') {
|
||||
this.Environment.sendMailAvailable = ServerEnvironment.sendMailAvailable;
|
||||
if (!this.Environment.sendMailAvailable) { //onNewValue is not yet available as a callback
|
||||
this.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async original(): Promise<PrivateConfigClass & IConfigClass> {
|
||||
|
115
src/common/config/private/MessagingConfig.ts
Normal file
115
src/common/config/private/MessagingConfig.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
||||
import {SubConfigClass} from '../../../../node_modules/typeconfig/src/decorators/class/SubConfigClass';
|
||||
import {ConfigPriority, TAGS} from '../public/ClientConfig';
|
||||
import {ConfigProperty} from '../../../../node_modules/typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {ServerConfig} from './PrivateConfig';
|
||||
declare let $localize: (s: TemplateStringsArray) => string;
|
||||
|
||||
if (typeof $localize === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
global.$localize = (s) => s;
|
||||
}
|
||||
export enum EmailMessagingType {
|
||||
sendmail = 1,
|
||||
SMTP = 2,
|
||||
}
|
||||
|
||||
@SubConfigClass<TAGS>({softReadonly: true})
|
||||
export class EmailSMTPMessagingConfig {
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Host`,
|
||||
priority: ConfigPriority.advanced,
|
||||
hint: 'smtp.example.com'
|
||||
},
|
||||
description: $localize`SMTP host server`
|
||||
})
|
||||
host: string = '';
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Port`,
|
||||
priority: ConfigPriority.advanced,
|
||||
},
|
||||
description: $localize`SMTP server's port`
|
||||
})
|
||||
port: number = 587;
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`isSecure`,
|
||||
priority: ConfigPriority.advanced,
|
||||
},
|
||||
description: $localize`Is the connection secure. See https://nodemailer.com/smtp/#tls-options for more details`
|
||||
})
|
||||
secure: boolean = false;
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`TLS required`,
|
||||
priority: ConfigPriority.advanced,
|
||||
},
|
||||
description: $localize`if this is true and secure is false then Nodemailer (used library in the background) tries to use STARTTLS. See https://nodemailer.com/smtp/#tls-options for more details`
|
||||
})
|
||||
requireTLS: boolean = true;
|
||||
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`User`,
|
||||
priority: ConfigPriority.advanced,
|
||||
},
|
||||
description: $localize`User to connect to the SMTP server.`
|
||||
})
|
||||
user: string = '';
|
||||
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Password`,
|
||||
priority: ConfigPriority.advanced,
|
||||
},
|
||||
type: 'password',
|
||||
description: $localize`Password to connect to the SMTP server.`
|
||||
})
|
||||
password: string = '';
|
||||
|
||||
}
|
||||
|
||||
@SubConfigClass<TAGS>({softReadonly: true})
|
||||
export class EmailMessagingConfig {
|
||||
@ConfigProperty<EmailMessagingType, EmailMessagingConfig>({
|
||||
type: EmailMessagingType,
|
||||
tags:
|
||||
{
|
||||
name: $localize`Sending method`,
|
||||
priority: ConfigPriority.advanced,
|
||||
uiDisabled: (sc: EmailMessagingConfig, c: ServerConfig) => !c.Environment.sendMailAvailable
|
||||
} as TAGS,
|
||||
description: $localize`Sendmail uses the built in unix binary if available. STMP connects to any STMP server of your choice.`
|
||||
})
|
||||
type: EmailMessagingType = EmailMessagingType.sendmail;
|
||||
|
||||
@ConfigProperty({
|
||||
tags:
|
||||
{
|
||||
name: $localize`SMTP`,
|
||||
relevant: (c: any) => c.type === EmailMessagingType.SMTP,
|
||||
}
|
||||
})
|
||||
smtp?: EmailSMTPMessagingConfig = new EmailSMTPMessagingConfig();
|
||||
|
||||
}
|
||||
|
||||
@SubConfigClass<TAGS>({softReadonly: true})
|
||||
export class MessagingConfig {
|
||||
@ConfigProperty({
|
||||
tags:
|
||||
{
|
||||
name: $localize`Email`,
|
||||
},
|
||||
description: $localize`The app uses Nodemailer in the background for sending e-mails. Refer to https://nodemailer.com/usage/ if some options are not clear.`
|
||||
})
|
||||
Email: EmailMessagingConfig = new EmailMessagingConfig();
|
||||
}
|
@ -30,6 +30,7 @@ import {DefaultsJobs} from '../../entities/job/JobDTO';
|
||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO';
|
||||
import {SortingMethods} from '../../entities/SortingMethods';
|
||||
import {UserRoles} from '../../entities/UserDTO';
|
||||
import {EmailMessagingType, MessagingConfig} from './MessagingConfig';
|
||||
|
||||
declare let $localize: (s: TemplateStringsArray) => string;
|
||||
|
||||
@ -395,7 +396,7 @@ export class ServerMetaFileConfig extends ClientMetaFileConfig {
|
||||
uiJob: [{
|
||||
job: DefaultsJobs[DefaultsJobs['GPX Compression']],
|
||||
relevant: (c) => c.MetaFile.GPXCompressing.enabled
|
||||
},{
|
||||
}, {
|
||||
job: DefaultsJobs[DefaultsJobs['Delete Compressed GPX']],
|
||||
relevant: (c) => c.MetaFile.GPXCompressing.enabled
|
||||
}]
|
||||
@ -903,6 +904,7 @@ export class ServerPreviewConfig {
|
||||
@SubConfigClass({softReadonly: true})
|
||||
export class ServerMediaConfig extends ClientMediaConfig {
|
||||
@ConfigProperty({
|
||||
|
||||
tags: {
|
||||
name: $localize`Images folder`,
|
||||
priority: ConfigPriority.basic,
|
||||
@ -1049,8 +1051,19 @@ export class ServerEnvironmentConfig {
|
||||
buildCommitHash: string | undefined;
|
||||
@ConfigProperty({volatile: true})
|
||||
isDocker: boolean | undefined;
|
||||
@ConfigProperty<boolean, ServerConfig, TAGS>({
|
||||
volatile: true,
|
||||
onNewValue: (value, config) => {
|
||||
if (value === false) {
|
||||
config.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
}
|
||||
},
|
||||
description: 'App updates on start-up if sendmail binary is available'
|
||||
})
|
||||
sendMailAvailable: boolean | undefined;
|
||||
}
|
||||
|
||||
|
||||
@SubConfigClass<TAGS>({softReadonly: true})
|
||||
export class ServerConfig extends ClientConfig {
|
||||
|
||||
@ -1144,6 +1157,16 @@ export class ServerConfig extends ClientConfig {
|
||||
})
|
||||
Duplicates: ServerDuplicatesConfig = new ServerDuplicatesConfig();
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Messaging`,
|
||||
uiIcon: 'chat',
|
||||
githubIssue: 683
|
||||
} as TAGS,
|
||||
description: $localize`The App can send messages (like photos on the same day a year ago. aka: "Top Pick"). Here you can configure the delivery method.`
|
||||
})
|
||||
Messaging: MessagingConfig = new MessagingConfig();
|
||||
|
||||
@ConfigProperty({
|
||||
tags: {
|
||||
name: $localize`Jobs`,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {backendText} from '../../BackendTexts';
|
||||
|
||||
export type fieldType = 'string' | 'number' | 'boolean' | 'number-array';
|
||||
export type fieldType = 'string' | 'email' | 'number' | 'boolean' | 'number-array' | 'SearchQuery' | 'sort-array';
|
||||
|
||||
export enum DefaultsJobs {
|
||||
Indexing = 1,
|
||||
@ -13,7 +13,8 @@ export enum DefaultsJobs {
|
||||
'Preview Reset' = 8,
|
||||
'GPX Compression' = 9,
|
||||
'Album Reset' = 10,
|
||||
'Delete Compressed GPX' = 11
|
||||
'Delete Compressed GPX' = 11,
|
||||
'Top Pick Sending' = 12
|
||||
}
|
||||
|
||||
export interface ConfigTemplateEntry {
|
||||
|
@ -4,6 +4,7 @@ export enum JobProgressStates {
|
||||
interrupted = 3,
|
||||
canceled = 4,
|
||||
finished = 5,
|
||||
failed = 6,
|
||||
}
|
||||
|
||||
export interface JobProgressLogDTO {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { BackendtextService } from './backendtext.service';
|
||||
import { backendTexts } from '../../../common/BackendTexts';
|
||||
import {inject, TestBed} from '@angular/core/testing';
|
||||
import {BackendtextService} from './backendtext.service';
|
||||
import {backendTexts} from '../../../common/BackendTexts';
|
||||
|
||||
describe('BackendTextService', () => {
|
||||
beforeEach(() => {
|
||||
@ -9,7 +9,7 @@ describe('BackendTextService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should call UserDTO service login', inject(
|
||||
it('should have valid text for all keys', inject(
|
||||
[BackendtextService],
|
||||
(backendTextService: BackendtextService) => {
|
||||
const getTexts = (obj: any) => {
|
||||
@ -18,7 +18,7 @@ describe('BackendTextService', () => {
|
||||
getTexts(obj[key]);
|
||||
continue;
|
||||
}
|
||||
expect(backendTextService.get(obj[key])).not.toBe(null);
|
||||
expect(backendTextService.get(obj[key])).not.toEqual(null, 'Error for key: ' + obj[key] +', ' + key);
|
||||
}
|
||||
};
|
||||
getTexts(backendTexts);
|
||||
|
@ -19,6 +19,34 @@ export class BackendtextService {
|
||||
return $localize`Index changes only`;
|
||||
case backendTexts.indexChangesOnly.description:
|
||||
return $localize`Only indexes a folder if it got changed.`;
|
||||
case backendTexts.searchQuery.name:
|
||||
return $localize`Search query`;
|
||||
case backendTexts.searchQuery.description:
|
||||
return $localize`Search query to list photos and videos.`;
|
||||
case backendTexts.sortBy.name:
|
||||
return $localize`Sorting`;
|
||||
case backendTexts.sortBy.description:
|
||||
return $localize`Sorts the photos and videos by this.`;
|
||||
case backendTexts.pickAmount.name:
|
||||
return $localize`Pick`;
|
||||
case backendTexts.pickAmount.description:
|
||||
return $localize`Number of photos and videos to pick.`;
|
||||
case backendTexts.emailTo.name:
|
||||
return $localize`E-mail to`;
|
||||
case backendTexts.emailTo.description:
|
||||
return $localize`E-mail address of the recipient.`;
|
||||
case backendTexts.emailFrom.name:
|
||||
return $localize`E-mail From`;
|
||||
case backendTexts.emailFrom.description:
|
||||
return $localize`E-mail sender address.`;
|
||||
case backendTexts.emailSubject.name:
|
||||
return $localize`Subject`;
|
||||
case backendTexts.emailSubject.description:
|
||||
return $localize`E-mail subject.`;
|
||||
case backendTexts.emailText.name:
|
||||
return $localize`Message`;
|
||||
case backendTexts.emailText.description:
|
||||
return $localize`E-mail text.`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -71,6 +71,10 @@
|
||||
let-confPath="confPath">
|
||||
<div class="alert alert-secondary" role="alert" *ngIf="rStates.description">
|
||||
{{rStates.description}}
|
||||
<a *ngIf="rStates.tags?.githubIssue"
|
||||
[href]="'https://github.com/bpatrik/pigallery2/issues/'+rStates.tags?.githubIssue">
|
||||
<ng-container i18n>See</ng-container>
|
||||
#{{rStates.tags?.githubIssue}}.</a>
|
||||
</div>
|
||||
<ng-container *ngFor="let ck of getKeys(rStates)">
|
||||
<ng-container *ngIf="!(rStates.value.__state[ck].shouldHide && rStates.value.__state[ck].shouldHide())">
|
||||
|
@ -115,6 +115,8 @@ export class JobProgressComponent implements OnDestroy, OnChanges {
|
||||
return $localize`interrupted`;
|
||||
case JobProgressStates.finished:
|
||||
return $localize`finished`;
|
||||
case JobProgressStates.failed:
|
||||
return $localize`failed`;
|
||||
default:
|
||||
return 'unknown state';
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
app-gallery-search-field {
|
||||
width: 100%;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
<div *ngFor="let schedule of sortedSchedules() as sortedSchedules; let i= index">
|
||||
<div class="card bg-body-tertiary mt-2 mb-2 no-changed-settings {{shouldIdent(schedule,sortedSchedules[i-1])? 'ms-4' : ''}}">
|
||||
<div
|
||||
class="card bg-body-tertiary mt-2 mb-2 no-changed-settings {{shouldIdent(schedule,sortedSchedules[i-1])? 'ms-4' : ''}}">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div (click)="showDetails[schedule.name]=!showDetails[schedule.name]">
|
||||
@ -175,6 +176,14 @@
|
||||
[(ngModel)]="schedule.config[configEntry.id]" required>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'email'">
|
||||
<input type="email" class="form-control" [name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
placeholder="adress@domain.com"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="schedule.config[configEntry.id]" required>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'number'">
|
||||
<input type="number" class="form-control" [name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
@ -189,6 +198,54 @@
|
||||
(ngModelChange)="setNumberArray(schedule.config,configEntry.id,$event); onChange($event);"
|
||||
[ngModel]="getNumberArray($any(schedule.config),configEntry.id)" required>
|
||||
</ng-container>
|
||||
|
||||
<app-gallery-search-field
|
||||
*ngSwitchCase="'SearchQuery'"
|
||||
[(ngModel)]="schedule.config[configEntry.id]"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
[name]="configEntry.id+'_'+i"
|
||||
(change)="onChange($event)"
|
||||
placeholder="Search Query">
|
||||
</app-gallery-search-field>
|
||||
|
||||
|
||||
<ng-container *ngSwitchCase="'sort-array'">
|
||||
<ng-container *ngFor="let _ of AsSortArray(schedule.config[configEntry.id]); let j=index">
|
||||
<div class="row col-12 mt-1 m-0 p-0">
|
||||
<div class="col p-0">
|
||||
<select
|
||||
[id]="configEntry.id+'_'+i+'_'+j"
|
||||
[name]="configEntry.id+'_'+i+'_'+j"
|
||||
(ngModelChange)="onChange($event)"
|
||||
class="form-select" [(ngModel)]="AsSortArray(schedule.config[configEntry.id])[j]">
|
||||
<option *ngFor="let opt of SortingMethods" [ngValue]="opt.key">{{opt.value}}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
|
||||
</div>
|
||||
<ng-container>
|
||||
<div class="col-auto pe-0">
|
||||
<button class="btn btn-secondary float-end"
|
||||
[id]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
|
||||
[name]="'list_btn_'+configEntry.id+'_'+i+'_'+j"
|
||||
(click)="removeSorting(schedule.config[configEntry.id],j)"><span
|
||||
class="oi oi-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-12 p-0">
|
||||
<button class="btn btn-primary mt-1 float-end"
|
||||
[id]="'btn_add_'+configEntry.id+'_'+i"
|
||||
[name]="'btn_add_'+configEntry.id+'_'+i"
|
||||
(click)="AddNewSorting(schedule.config[configEntry.id])" i18n>+ Add
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
|
@ -19,6 +19,8 @@ import {
|
||||
ScheduledJobTriggerConfig
|
||||
} from '../../../../../common/config/private/PrivateConfig';
|
||||
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
|
||||
import {enumToTranslatedArray} from '../../EnumTranslations';
|
||||
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-workflow',
|
||||
@ -61,6 +63,9 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
||||
allowParallelRun: false,
|
||||
};
|
||||
|
||||
SortingMethods = enumToTranslatedArray(SortingMethods);
|
||||
|
||||
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
@ -278,4 +283,17 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
|
||||
|
||||
AsSortArray(configElement: string | number | string[] | number[]): SortingMethods[] {
|
||||
return configElement as SortingMethods[];
|
||||
}
|
||||
|
||||
removeSorting(configElement: string | number | string[] | number[], j: number): void {
|
||||
(configElement as SortingMethods[]).splice(j);
|
||||
}
|
||||
|
||||
AddNewSorting(configElement: string | number | string[] | number[]): void {
|
||||
(configElement as SortingMethods[]).push(SortingMethods.ascDate)
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import {Config} from '../../../../../src/common/config/private/Config';
|
||||
import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection';
|
||||
import {Server} from '../../../../../src/backend/server';
|
||||
import {DatabaseType, ServerConfig} from '../../../../../src/common/config/private/PrivateConfig';
|
||||
import {ProjectPath} from '../../../../../src/backend/ProjectPath';
|
||||
import {TAGS} from '../../../../../src/common/config/public/ClientConfig';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
const chai: any = require('chai');
|
||||
@ -34,9 +34,8 @@ describe('SettingsRouter', () => {
|
||||
describe('/GET settings', () => {
|
||||
it('it should GET the settings', async () => {
|
||||
Config.Users.authenticationRequired = false;
|
||||
Config.Users.unAuthenticatedUserRole = UserRoles.Admin;
|
||||
const originalSettings = await Config.original();
|
||||
// originalSettings.Server.sessionSecret = null;
|
||||
// originalSettings.Users.enforcedUsers = null;
|
||||
const srv = new Server();
|
||||
await srv.onStarted.wait();
|
||||
const result = await chai.request(srv.App)
|
||||
@ -46,7 +45,9 @@ describe('SettingsRouter', () => {
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
(result.body.result as ServerConfig).Environment.upTime = null;
|
||||
(result.body.result as ServerConfig).Environment.sendMailAvailable = null;
|
||||
originalSettings.Environment.upTime = null;
|
||||
originalSettings.Environment.sendMailAvailable = null;
|
||||
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({
|
||||
attachState: true,
|
||||
attachVolatile: true,
|
||||
|
@ -6,6 +6,11 @@ import {SettingsMWs} from '../../../../../src/backend/middlewares/admin/Settings
|
||||
import {ServerUserConfig} from '../../../../../src/common/config/private/PrivateConfig';
|
||||
import {Config} from '../../../../../src/common/config/private/Config';
|
||||
import {UserRoles} from '../../../../../src/common/entities/UserDTO';
|
||||
import {ConfigClassBuilder} from '../../../../../node_modules/typeconfig/node';
|
||||
import {ServerEnvironment} from '../../../../../src/backend/Environment';
|
||||
import {EmailMessagingType} from '../../../../../src/common/config/private/MessagingConfig';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
|
||||
declare const describe: any;
|
||||
@ -14,11 +19,16 @@ declare const beforeEach: any;
|
||||
|
||||
describe('Settings middleware', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
ObjectManagers.reset();
|
||||
const tempDir = path.join(__dirname, '../../../tmp');
|
||||
beforeEach(async () => {
|
||||
await ObjectManagers.reset();
|
||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
it('should save empty enforced users settings', (done: (err?: any) => void) => {
|
||||
ServerEnvironment.sendMailAvailable = false;
|
||||
Config.Environment.sendMailAvailable = false;
|
||||
Config.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
const req: any = {
|
||||
session: {},
|
||||
sessionOptions: {},
|
||||
@ -26,15 +36,17 @@ describe('Settings middleware', () => {
|
||||
params: {},
|
||||
body: {
|
||||
settingsPath: 'Users',
|
||||
settings: new ServerUserConfig()
|
||||
settings: ConfigClassBuilder.attachPrivateInterface(new ServerUserConfig()).toJSON()
|
||||
}
|
||||
};
|
||||
req.body.settings.enforcedUsers = [];
|
||||
const next: any = (err: ErrorDTO) => {
|
||||
try {
|
||||
expect(err).to.be.undefined;
|
||||
expect(Config.Users.enforcedUsers.length).to.be.equal(0);
|
||||
done();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
done(err);
|
||||
}
|
||||
};
|
||||
@ -43,6 +55,10 @@ describe('Settings middleware', () => {
|
||||
|
||||
});
|
||||
it('should save enforced users settings', (done: (err?: any) => void) => {
|
||||
|
||||
ServerEnvironment.sendMailAvailable = false;
|
||||
Config.Environment.sendMailAvailable = false;
|
||||
Config.Messaging.Email.type = EmailMessagingType.SMTP;
|
||||
const req: any = {
|
||||
session: {},
|
||||
sessionOptions: {},
|
||||
@ -66,11 +82,17 @@ describe('Settings middleware', () => {
|
||||
expect(Config.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||
expect(Config.Users.enforcedUsers.length).to.be.equal(1);
|
||||
Config.original().then((cfg) => {
|
||||
expect(cfg.Users.enforcedUsers.length).to.be.equal(1);
|
||||
expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||
done();
|
||||
try {
|
||||
expect(cfg.Users.enforcedUsers.length).to.be.equal(1);
|
||||
expect(cfg.Users.enforcedUsers[0].name).to.be.equal('Apple');
|
||||
done();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
done(err);
|
||||
}
|
||||
}).catch(done);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
done(err);
|
||||
}
|
||||
};
|
||||
|
@ -711,7 +711,6 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
|
||||
const albums = await am.getAlbums();
|
||||
expect(albums[0].preview).to.be.an('object');
|
||||
delete albums[0].preview;
|
||||
console.log(albums);
|
||||
expect(albums).to.be.deep.equal([
|
||||
{
|
||||
id: 1,
|
||||
|
@ -35,6 +35,7 @@ import {AutoCompleteItem} from '../../../../../src/common/entities/AutoCompleteI
|
||||
import {Config} from '../../../../../src/common/config/private/Config';
|
||||
import {SearchQueryParser} from '../../../../../src/common/SearchQueryParser';
|
||||
import {FileDTO} from '../../../../../src/common/entities/FileDTO';
|
||||
import {SortingMethods} from '../../../../../src/common/entities/SortingMethods';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
|
||||
@ -1405,14 +1406,14 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
} as TextSearch;
|
||||
|
||||
// eslint-disable-next-line
|
||||
expect(await sm.getRandomPhoto(query)).to.not.exist;
|
||||
expect(await sm.getNMedia(query, [SortingMethods.random], 1, true)).to.deep.equalInAnyOrder([]);
|
||||
|
||||
query = ({
|
||||
text: 'wookiees',
|
||||
matchType: TextSearchQueryMatchTypes.exact_match,
|
||||
type: SearchQueryTypes.keyword
|
||||
} as TextSearch);
|
||||
expect(Utils.clone(await sm.getRandomPhoto(query))).to.deep.equalInAnyOrder(searchifyMedia(pFaceLess));
|
||||
expect(Utils.clone(await sm.getNMedia(query, [SortingMethods.random], 1, true))).to.deep.equalInAnyOrder([searchifyMedia(pFaceLess)]);
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user