Email Notifications are now fully customizable and work! #865

We also have reactive forms in the UI, should apply this to other settings.
This commit is contained in:
Jamie.Rees 2017-06-15 16:28:43 +01:00
parent 3b0b35f760
commit 9a4dbb3dce
21 changed files with 361 additions and 232 deletions

View file

@ -1,4 +1,5 @@
using Ombi.Core.Claims; using System;
using Ombi.Core.Claims;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Rule; using Ombi.Core.Rule;
using Ombi.Core.Rules; using Ombi.Core.Rules;
@ -6,7 +7,12 @@ using Ombi.Store.Entities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Principal; using System.Security.Principal;
using Hangfire;
using Ombi.Core.Models.Requests.Movie;
using Ombi.Core.Models.Search; using Ombi.Core.Models.Search;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Models;
namespace Ombi.Core.Engine.Interfaces namespace Ombi.Core.Engine.Interfaces
{ {

View file

@ -1,12 +1,10 @@
using Hangfire; using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Models.Requests.Movie; using Ombi.Core.Models.Requests.Movie;
using Ombi.Core.Models.Search; using Ombi.Core.Models.Search;
using Ombi.Core.Rules; using Ombi.Core.Rules;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications; using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -16,24 +14,22 @@ using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Notifications;
using Ombi.Store;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
public class MovieRequestEngine : BaseMediaEngine, IMovieRequestEngine public class MovieRequestEngine : BaseMediaEngine, IMovieRequestEngine
{ {
public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user,
INotificationService notificationService, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log) : base(user, requestService, r) INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log) : base(user, requestService, r)
{ {
MovieApi = movieApi; MovieApi = movieApi;
NotificationService = notificationService; NotificationHelper = helper;
Sender = sender; Sender = sender;
Logger = log; Logger = log;
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
private INotificationService NotificationService { get; } private INotificationHelper NotificationHelper { get; }
private IMovieSender Sender { get; } private IMovieSender Sender { get; }
private ILogger<MovieRequestEngine> Logger { get; } private ILogger<MovieRequestEngine> Logger { get; }
@ -84,6 +80,7 @@ namespace Ombi.Core.Engine
// }); // });
//} //}
var requestModel = new MovieRequestModel var requestModel = new MovieRequestModel
{ {
ProviderId = movieInfo.Id, ProviderId = movieInfo.Id,
@ -98,64 +95,41 @@ namespace Ombi.Core.Engine
Status = movieInfo.Status, Status = movieInfo.Status,
RequestedDate = DateTime.UtcNow, RequestedDate = DateTime.UtcNow,
Approved = false, Approved = false,
RequestedUser =Username, RequestedUser = Username,
Issues = IssueState.None Issues = IssueState.None
}; };
try var ruleResults = RunRequestRules(requestModel).ToList();
if (ruleResults.Any(x => !x.Success))
{ {
var ruleResults = RunRequestRules(requestModel).ToList(); return new RequestEngineResult
if (ruleResults.Any(x => !x.Success)) {
ErrorMessage = ruleResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.Message)).Message
};
}
if (requestModel.Approved) // The rules have auto approved this
{
var result = await Sender.Send(requestModel);
if (result.Success && result.MovieSent)
{
return await AddMovieRequest(requestModel, /*settings,*/
$"{fullMovieName} has been successfully added!");
}
if (!result.Success)
{
Logger.LogWarning("Tried auto sending movie but failed. Message: {0}", result.Message);
return new RequestEngineResult return new RequestEngineResult
{ {
ErrorMessage = ruleResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.Message)).Message Message = result.Message,
ErrorMessage = result.Message,
RequestAdded = false
}; };
if (requestModel.Approved) // The rules have auto approved this
{
var result = await Sender.Send(requestModel);
if (result.Success && result.MovieSent)
{
return await AddMovieRequest(requestModel, /*settings,*/
$"{fullMovieName} has been successfully added!");
}
if (!result.Success)
{
Logger.LogWarning("Tried auto sending movie but failed. Message: {0}", result.Message);
return new RequestEngineResult
{
Message = result.Message,
ErrorMessage = result.Message,
RequestAdded = false
};
}
} }
return await AddMovieRequest(requestModel, /*settings,*/
$"{fullMovieName} has been successfully added!");
}
catch (Exception e)
{
//Log.Fatal(e);
//await FaultQueue.QueueItemAsync(model, movieInfo.Id.ToString(), RequestType.Movie, FaultType.RequestFault, e.Message);
var notification = new NotificationOptions
{
DateTime = DateTime.Now,
RequestedUser = Username,
RequestType = RequestType.Movie,
Title = model.Title,
NotificationType = NotificationType.ItemAddedToFaultQueue
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notification).Wait());
//return Response.AsJson(new JsonResponseModel
//{
// Result = true,
// Message = $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"
//});
} }
return null; return await AddMovieRequest(requestModel, /*settings,*/
$"{fullMovieName} has been successfully added!");
} }
public async Task<IEnumerable<MovieRequestModel>> GetRequests(int count, int position) public async Task<IEnumerable<MovieRequestModel>> GetRequests(int count, int position)
@ -210,21 +184,9 @@ namespace Ombi.Core.Engine
if (ShouldSendNotification(model.Type)) if (ShouldSendNotification(model.Type))
{ {
var notificationModel = new NotificationOptions NotificationHelper.NewRequest(model);
{
Title = model.Title,
RequestedUser = Username,
DateTime = DateTime.Now,
NotificationType = NotificationType.NewRequest,
RequestType = model.Type,
ImgSrc = model.Type == RequestType.Movie
? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}"
: model.PosterPath
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel).Wait());
} }
//var limit = await RequestLimitRepo.GetAllAsync(); //var limit = await RequestLimitRepo.GetAllAsync();
//var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type); //var usersLimit = limit.FirstOrDefault(x => x.Username == Username && x.RequestType == model.Type);
//if (usersLimit == null) //if (usersLimit == null)

View file

@ -15,23 +15,21 @@ using System.Linq;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Core.Engine.Interfaces; using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Notifications;
using Ombi.Store;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine
{ {
public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IRequestServiceMain requestService, IPrincipal user,
INotificationService notificationService, IMapper map, INotificationHelper helper, IMapper map,
IRuleEvaluator rule) : base(user, requestService, rule) IRuleEvaluator rule) : base(user, requestService, rule)
{ {
TvApi = tvApi; TvApi = tvApi;
NotificationService = notificationService; NotificationHelper = helper;
Mapper = map; Mapper = map;
} }
private INotificationService NotificationService { get; } private INotificationHelper NotificationHelper { get; }
private ITvMazeApi TvApi { get; } private ITvMazeApi TvApi { get; }
private IMapper Mapper { get; } private IMapper Mapper { get; }
@ -270,24 +268,11 @@ namespace Ombi.Core.Engine
return await AfterRequest(model); return await AfterRequest(model);
} }
private async Task<RequestEngineResult> AfterRequest(TvRequestModel model) private Task<RequestEngineResult> AfterRequest(TvRequestModel model)
{ {
if (ShouldSendNotification(model.Type)) if (ShouldSendNotification(model.Type))
{ {
var n = new NotificationOptions(); NotificationHelper.NewRequest(model);
n.Title = model.Title;
n.RequestedUser = Username;
n.DateTime = DateTime.Now;
n.NotificationType = NotificationType.NewRequest;
n.RequestType = model.Type;
n.ImgSrc = model.PosterPath;
BackgroundJob.Enqueue(() =>
NotificationService.Publish(n).Wait()
);
} }
//var limit = await RequestLimitRepo.GetAllAsync(); //var limit = await RequestLimitRepo.GetAllAsync();
@ -308,7 +293,7 @@ namespace Ombi.Core.Engine
// await RequestLimitRepo.UpdateAsync(usersLimit); // await RequestLimitRepo.UpdateAsync(usersLimit);
//} //}
return new RequestEngineResult { RequestAdded = true }; return Task.FromResult(new RequestEngineResult { RequestAdded = true });
} }
public async Task<IEnumerable<TvRequestModel>> GetApprovedRequests() public async Task<IEnumerable<TvRequestModel>> GetApprovedRequests()

View file

@ -0,0 +1,9 @@
using Ombi.Core.Models.Requests;
namespace Ombi.Core
{
public interface INotificationHelper
{
void NewRequest(BaseRequestModel model);
}
}

View file

@ -0,0 +1,36 @@
using System;
using Hangfire;
using Ombi.Core.Models.Requests;
using Ombi.Core.Notifications;
using Ombi.Helpers;
using Ombi.Notifications.Models;
using Ombi.Store.Entities;
namespace Ombi.Core
{
public class NotificationHelper : INotificationHelper
{
public NotificationHelper(INotificationService service)
{
NotificationService = service;
}
private INotificationService NotificationService { get; }
public void NewRequest(BaseRequestModel model)
{
var notificationModel = new NotificationOptions
{
Title = model.Title,
RequestedUser = model.RequestedUser,
DateTime = DateTime.Now,
NotificationType = NotificationType.NewRequest,
RequestType = model.Type,
ImgSrc = model.Type == RequestType.Movie
? $"https://image.tmdb.org/t/p/w300/{model.PosterPath}"
: model.PosterPath
};
BackgroundJob.Enqueue(() => NotificationService.Publish(notificationModel));
}
}
}

View file

@ -84,6 +84,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IRequestServiceMain, RequestService>(); services.AddTransient<IRequestServiceMain, RequestService>();
services.AddTransient(typeof(IRequestService<>), typeof(JsonRequestService<>)); services.AddTransient(typeof(IRequestService<>), typeof(JsonRequestService<>));
services.AddSingleton<INotificationService, NotificationService>(); services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<INotificationHelper, NotificationHelper>();
} }
public static void RegisterJobs(this IServiceCollection services) public static void RegisterJobs(this IServiceCollection services)

View file

@ -40,31 +40,43 @@ namespace Ombi.Notifications.Agents
return true; return true;
} }
private async Task<NotificationMessageContent> LoadTemplate(NotificationType type, NotificationOptions model) private async Task<NotificationMessage> LoadTemplate(NotificationType type, NotificationOptions model, EmailNotificationSettings settings)
{ {
var template = await TemplateRepository.GetTemplate(NotificationAgent.Email, type); var template = await TemplateRepository.GetTemplate(NotificationAgent.Email, type);
if (!template.Enabled)
{
return null;
}
// Need to do the parsing // Need to do the parsing
var resolver = new NotificationMessageResolver(); var resolver = new NotificationMessageResolver();
return resolver.ParseMessage(template, new NotificationMessageCurlys(model.RequestedUser, model.Title, DateTime.Now.ToString("D"), var parsed = resolver.ParseMessage(template, new NotificationMessageCurlys(model.RequestedUser, model.Title, DateTime.Now.ToString("D"),
model.NotificationType.ToString(), null)); model.NotificationType.ToString(), null));
}
protected override async Task NewRequest(NotificationOptions model, EmailNotificationSettings settings)
{
var template = await LoadTemplate(NotificationType.NewRequest, model);
var email = new EmailBasicTemplate(); var email = new EmailBasicTemplate();
var html = email.LoadTemplate(template.Subject, template.Message, model.ImgSrc); var html = email.LoadTemplate(parsed.Subject, parsed.Message, model.ImgSrc);
var message = new NotificationMessage var message = new NotificationMessage
{ {
Message = html, Message = html,
Subject = $"Ombi: New {model.RequestType} request for {model.Title}!", Subject = parsed.Subject,
To = settings.AdminEmail, To = settings.AdminEmail,
}; };
return message;
}
protected override async Task NewRequest(NotificationOptions model, EmailNotificationSettings settings)
{
var message = await LoadTemplate(NotificationType.NewRequest, model, settings);
if (message == null)
{
return;
}
message.Other.Add("PlainTextBody", $"Hello! The user '{model.RequestedUser}' has requested the {model.RequestType} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime:f}"); message.Other.Add("PlainTextBody", $"Hello! The user '{model.RequestedUser}' has requested the {model.RequestType} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime:f}");
await Send(message, settings); await Send(message, settings);
@ -72,18 +84,11 @@ namespace Ombi.Notifications.Agents
protected override async Task Issue(NotificationOptions model, EmailNotificationSettings settings) protected override async Task Issue(NotificationOptions model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate(); var message = await LoadTemplate(NotificationType.Issue, model, settings);
var html = email.LoadTemplate( if (message == null)
$"Ombi: New issue for {model.Title}!",
$"Hello! The user '{model.RequestedUser}' has reported a new issue {model.Body} for the title {model.Title}!",
model.ImgSrc);
var message = new NotificationMessage
{ {
Message = html, return;
Subject = $"Ombi: New issue for {model.Title}!", }
To = settings.AdminEmail,
};
message.Other.Add("PlainTextBody", $"Hello! The user '{model.RequestedUser}' has reported a new issue {model.Body} for the title {model.Title}!"); message.Other.Add("PlainTextBody", $"Hello! The user '{model.RequestedUser}' has reported a new issue {model.Body} for the title {model.Title}!");
@ -113,18 +118,11 @@ namespace Ombi.Notifications.Agents
protected override async Task RequestDeclined(NotificationOptions model, EmailNotificationSettings settings) protected override async Task RequestDeclined(NotificationOptions model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate(); var message = await LoadTemplate(NotificationType.RequestDeclined, model, settings);
var html = email.LoadTemplate( if (message == null)
"Ombi: Your request has been declined",
$"Hello! Your request for {model.Title} has been declined, Sorry!",
model.ImgSrc);
var message = new NotificationMessage
{ {
Message = html, return;
Subject = $"Ombi: Your request has been declined", }
To = model.UserEmail,
};
message.Other.Add("PlainTextBody", $"Hello! Your request for {model.Title} has been declined, Sorry!"); message.Other.Add("PlainTextBody", $"Hello! Your request for {model.Title} has been declined, Sorry!");
@ -134,18 +132,11 @@ namespace Ombi.Notifications.Agents
protected override async Task RequestApproved(NotificationOptions model, EmailNotificationSettings settings) protected override async Task RequestApproved(NotificationOptions model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate(); var message = await LoadTemplate(NotificationType.RequestApproved, model, settings);
var html = email.LoadTemplate( if (message == null)
"Ombi: Your request has been approved!",
$"Hello! Your request for {model.Title} has been approved!",
model.ImgSrc);
var message = new NotificationMessage
{ {
Message = html, return;
Subject = $"Ombi: Your request has been approved!", }
To = model.UserEmail,
};
message.Other.Add("PlainTextBody", $"Hello! Your request for {model.Title} has been approved!"); message.Other.Add("PlainTextBody", $"Hello! Your request for {model.Title} has been approved!");
@ -154,19 +145,11 @@ namespace Ombi.Notifications.Agents
protected override async Task AvailableRequest(NotificationOptions model, EmailNotificationSettings settings) protected override async Task AvailableRequest(NotificationOptions model, EmailNotificationSettings settings)
{ {
var email = new EmailBasicTemplate(); var message = await LoadTemplate(NotificationType.RequestAvailable, model, settings);
var html = email.LoadTemplate( if (message == null)
$"Ombi: {model.Title} is now available!",
$"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)",
model.ImgSrc);
var message = new NotificationMessage
{ {
Message = html, return;
Subject = $"Ombi: {model.Title} is now available!", }
To = model.UserEmail,
};
message.Other.Add("PlainTextBody", $"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)"); message.Other.Add("PlainTextBody", $"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)");

View file

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Core.Settings; using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Store; using Ombi.Store;
using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
namespace Ombi.Notifications namespace Ombi.Notifications
@ -36,7 +39,6 @@ namespace Ombi.Notifications
{ {
return; return;
} }
try try
{ {
switch (model.NotificationType) switch (model.NotificationType)

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Ombi.Store.Entities; using Ombi.Store.Entities;
namespace Ombi.Notifications namespace Ombi.Notifications
@ -18,19 +19,31 @@ namespace Ombi.Notifications
Type = type; Type = type;
Issue = issue; Issue = issue;
} }
// User Defined
private string RequestedUser { get; } private string RequestedUser { get; }
private string Title { get; } private string Title { get; }
private string RequestedDate { get; } private string RequestedDate { get; }
private string Type { get; } private string Type { get; }
private string Issue { get; } private string Issue { get; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");
private string ShortDate => DateTime.Now.ToString("d");
private string LongTime => DateTime.Now.ToString("T");
private string ShortTime => DateTime.Now.ToString("t");
public Dictionary<string, string> Curlys => new Dictionary<string, string> public Dictionary<string, string> Curlys => new Dictionary<string, string>
{ {
{nameof(RequestedUser), RequestedUser }, {nameof(RequestedUser), RequestedUser },
{nameof(Title), Title }, {nameof(Title), Title },
{nameof(RequestedDate), RequestedDate }, {nameof(RequestedDate), RequestedDate },
{nameof(Type), Type }, {nameof(Type), Type },
{nameof(Issue), Issue } {nameof(Issue), Issue },
{nameof(LongDate),LongDate},
{nameof(ShortDate),ShortDate},
{nameof(LongTime),LongTime},
{nameof(ShortTime),ShortTime},
}; };
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -64,25 +63,27 @@ namespace Ombi.Store.Context
{ {
foreach (var notificationType in allTypes) foreach (var notificationType in allTypes)
{ {
var notificationToAdd = new NotificationTemplates(); NotificationTemplates notificationToAdd;
switch (notificationType) switch (notificationType)
{ {
case NotificationType.NewRequest: case NotificationType.NewRequest:
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! The user '{requestedUser}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}", Message = "Hello! The user '{RequestedUser}' has requested the {Type} '{Title}'! Please log in to approve this request. Request Date: {RequestedDate}",
Subject = "Ombi: New {Type} request for {Title}!", Subject = "Ombi: New {Type} request for {Title}!",
Agent = agent, Agent = agent,
Enabled = true,
}; };
break; break;
case NotificationType.Issue: case NotificationType.Issue:
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! The user '{requestedUser}' has reported a new issue for the title {Title}! </br> {Issue}", Message = "Hello! The user '{RequestedUser}' has reported a new issue for the title {Title}! </br> {Issue}",
Subject = "Ombi: New issue for {Title}!", Subject = "Ombi: New issue for {Title}!",
Agent = agent, Agent = agent,
Enabled = true,
}; };
break; break;
case NotificationType.RequestAvailable: case NotificationType.RequestAvailable:
@ -92,6 +93,7 @@ namespace Ombi.Store.Context
Message = "Hello! You requested {Title} on Ombi! This is now available! :)", Message = "Hello! You requested {Title} on Ombi! This is now available! :)",
Subject = "Ombi: {Title} is now available!", Subject = "Ombi: {Title} is now available!",
Agent = agent, Agent = agent,
Enabled = true,
}; };
break; break;
case NotificationType.RequestApproved: case NotificationType.RequestApproved:
@ -101,6 +103,7 @@ namespace Ombi.Store.Context
Message = "Hello! Your request for {Title} has been approved!", Message = "Hello! Your request for {Title} has been approved!",
Subject = "Ombi: your request has been approved", Subject = "Ombi: your request has been approved",
Agent = agent, Agent = agent,
Enabled = true,
}; };
break; break;
case NotificationType.AdminNote: case NotificationType.AdminNote:
@ -110,12 +113,11 @@ namespace Ombi.Store.Context
case NotificationType.RequestDeclined: case NotificationType.RequestDeclined:
notificationToAdd = new NotificationTemplates notificationToAdd = new NotificationTemplates
{ {
//"Ombi: Your request has been declined",
//$"Hello! Your request for {model.Title} has been declined, Sorry!",
NotificationType = notificationType, NotificationType = notificationType,
Message = "Hello! Your request for {Title} has been declined, Sorry!", Message = "Hello! Your request for {Title} has been declined, Sorry!",
Subject = "Ombi: your request has been declined", Subject = "Ombi: your request has been declined",
Agent = agent, Agent = agent,
Enabled = true,
}; };
break; break;
case NotificationType.ItemAddedToFaultQueue: case NotificationType.ItemAddedToFaultQueue:

View file

@ -10,5 +10,6 @@ namespace Ombi.Store.Entities
public NotificationAgent Agent { get; set; } public NotificationAgent Agent { get; set; }
public string Subject { get; set; } public string Subject { get; set; }
public string Message { get; set; } public string Message { get; set; }
public bool Enabled { get; set; }
} }
} }

View file

@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS NotificationTemplates
NotificationType INTEGER NOT NULL, NotificationType INTEGER NOT NULL,
Agent INTEGER NOT NULL, Agent INTEGER NOT NULL,
Subject BLOB NULL, Subject BLOB NULL,
Message BLOB NULL Message BLOB NULL,
Enabled INTEGER NOT NULL
); );

View file

@ -14,19 +14,17 @@ import { ICustomizationSettings } from './interfaces/ISettings';
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router, private settingsService: SettingsService constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router, private settingsService: SettingsService)
) { {
} }
customizationSettings: ICustomizationSettings; customizationSettings: ICustomizationSettings;
user: ILocalUser; user: ILocalUser;
ngOnInit(): void { ngOnInit() : void {
this.user = this.authService.claims(); this.user = this.authService.claims();
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x);
this.router.events.subscribe(() => { this.router.events.subscribe(() => {
@ -34,7 +32,6 @@ export class AppComponent implements OnInit {
this.user = this.authService.claims(); this.user = this.authService.claims();
this.showNav = this.authService.loggedIn(); this.showNav = this.authService.loggedIn();
}); });
} }
hasRole(role: string): boolean { hasRole(role: string): boolean {

View file

@ -22,6 +22,7 @@ export interface INotificationTemplates {
message: string, message: string,
notificationType: NotificationType, notificationType: NotificationType,
notificationAgent: NotificationAgent, notificationAgent: NotificationAgent,
enabled:boolean,
} }
export enum NotificationAgent { export enum NotificationAgent {

View file

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { FormGroup, Validators, ValidatorFn } from '@angular/forms';
@Injectable()
export class ValidationService {
/**
* Disable validation on a control
* @param form
* @param name
*/
public disableValidation(form: FormGroup, name: string): void {
form.controls[name].clearValidators();
form.controls[name].updateValueAndValidity();
}
/**
* Enable validation with the default validation attribute of required
* @param form
* @param name
*/
public enableValidation(form: FormGroup, name: string): void;
public enableValidation(form: FormGroup, name: string, validators?: ValidatorFn[]): void {
if (validators) {
// If we provide some use them
form.controls[name].setValidators(validators);
} else {
// It's just required by default
form.controls[name].setValidators([Validators.required]);
}
form.controls[name].updateValueAndValidity();
}
}

View file

@ -1,82 +1,99 @@
 
<settings-menu></settings-menu> <settings-menu></settings-menu>
<div *ngIf="settings"> <div *ngIf="emailForm">
<fieldset> <fieldset>
<legend>Email Notifications</legend> <legend>Email Notifications</legend>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <form novalidate [formGroup]="emailForm" (ngSubmit)="onSubmit(emailForm)">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.enabled" ng-checked="settings.enabled"> <div class="form-group">
<label for="enable">Enabled</label> <div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enabled</label>
</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<input type="checkbox" id="Authentication" [(ngModel)]="settings.authentication" ng-checked="settings.authentication"><label for="Authentication">Enable SMTP Authentication</label> <input type="checkbox" id="Authentication" formControlName="authentication"><label for="Authentication">Enable SMTP Authentication</label>
</div>
</div> </div>
</div> <div *ngIf="emailForm.invalid && emailForm.dirty" class="alert alert-danger">
<div *ngIf="emailForm.get('host').hasError('required')">Host is required</div>
<div class="form-group"> <div *ngIf="emailForm.get('port').hasError('required')">The Port is required</div>
<label for="host" class="control-label">SMTP Host</label> <div *ngIf="emailForm.get('sender').hasError('required')">The Email Sender is required</div>
<div> <div *ngIf="emailForm.get('sender').hasError('email')">The Email Sender needs to be a valid email address</div>
<input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" [(ngModel)]="settings.host" value="{{settings.host}}"> <div *ngIf="emailForm.get('adminEmail').hasError('required')">The Email Sender is required</div>
<div *ngIf="emailForm.get('adminEmail').hasError('email')">The Admin Email needs to be a valid email address</div>
<div *ngIf="emailForm.get('username').hasError('required')">The Username is required</div>
<div *ngIf="emailForm.get('password').hasError('required')">The Password is required</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label for="portNumber" class="control-label">SMTP Port</label> <label for="host" class="control-label">SMTP Host</label>
<div> <div>
<input type="text" [(ngModel)]="settings.port" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" value="{{settings.port}}"> <input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" formControlName="host">
</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label for="sender" class="control-label">Email Sender</label> <label for="portNumber" class="control-label">SMTP Port</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="sender" name="sender" [(ngModel)]="settings.sender" value="{{settings.sender}}" tooltipPosition="top" pTooltip="The email address that the emails will be sent from"> <input type="text" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" formControlName="port">
</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label for="adminEmail" class="control-label">Admin Email</label> <label for="sender" class="control-label">Email Sender</label>
<div> <div>
<input type="text" class="form-control form-control-custom " id="adminEmail" name="adminEmail" [(ngModel)]="settings.adminEmail" value="{{settings.adminEmail}}" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)"> <input type="text" class="form-control form-control-custom " id="sender" name="sender" formControlName="sender" tooltipPosition="top" pTooltip="The email address that the emails will be sent from">
</div>
</div> </div>
</div>
<div class="form-group">
<div class="form-group" *ngIf="settings.authentication"> <label for="adminEmail" class="control-label">Admin Email</label>
<label for="username" class="control-label">Username</label> <div>
<div> <input type="text" class="form-control form-control-custom " id="adminEmail" name="adminEmail" formControlName="adminEmail" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)">
<input type="text" class="form-control form-control-custom " id="username" name="username" [(ngModel)]="settings.username" value="{{settings.username}}" pTooltip="The username if authentication is enabled" tooltipPosition="top"> </div>
</div> </div>
</div>
<div class="form-group" *ngIf="settings.authentication">
<label for="password" class="control-label">Password</label> <div class="form-group" *ngIf="emailForm.controls['username'].validator">
<div> <label for="username" class="control-label">Username</label>
<input type="password" class="form-control form-control-custom " id="password" name="password" [(ngModel)]="settings.password" value="{{settings.password}}" pTooltip="The password if authentication is enabled" tooltipPosition="top"> <div>
<input type="text" class="form-control form-control-custom " id="username" name="username" formControlName="username" pTooltip="The username if authentication is enabled" tooltipPosition="top">
</div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group" *ngIf="emailForm.get('password').validator">
<div> <label for="password" class="control-label">Password</label>
<button id="testPlex" type="submit" (click)="test()" class="btn btn-primary-outline">Test Connectivity <div id="spinner"></div></button> <div>
<input type="password" class="form-control form-control-custom " id="password" name="password" formControlName="password" pTooltip="The password if authentication is enabled" tooltipPosition="top">
</div>
</div> </div>
</div>
<div class="form-group">
<div>
<div class="form-group"> <button id="testPlex" type="submit" (click)="test()" class="btn btn-primary-outline">
<div> Test Connectivity
<button (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button> <div id="spinner"></div>
</button>
</div>
</div> </div>
</div>
<div class="form-group">
<div>
<button [disabled]="emailForm.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</form>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<notification-templates [templates]="settings.notificationTemplates"></notification-templates> <notification-templates [templates]="templates"></notification-templates>
</div> </div>
</fieldset> </fieldset>
</div> </div>

View file

@ -1,34 +1,87 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { IEmailNotificationSettings, NotificationType } from '../../interfaces/INotifcationSettings'; import { INotificationTemplates, IEmailNotificationSettings, NotificationType } from '../../interfaces/INotifcationSettings';
import { SettingsService } from '../../services/settings.service'; import { SettingsService } from '../../services/settings.service';
import { NotificationService } from "../../services/notification.service"; import { NotificationService } from "../../services/notification.service";
import { ValidationService } from "../../services/helpers/validation.service";
@Component({ @Component({
templateUrl: './emailnotification.component.html', templateUrl: './emailnotification.component.html',
}) })
export class EmailNotificationComponent implements OnInit { export class EmailNotificationComponent implements OnInit {
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private fb: FormBuilder,
private validationService: ValidationService) { }
constructor(private settingsService: SettingsService, private notificationService: NotificationService) { }
settings: IEmailNotificationSettings;
NotificationType = NotificationType; NotificationType = NotificationType;
templates: INotificationTemplates[];
emailForm: FormGroup;
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getEmailNotificationSettings().subscribe(x => this.settings = x); this.settingsService.getEmailNotificationSettings().subscribe(x => {
this.templates = x.notificationTemplates;
this.emailForm = this.fb.group({
enabled: [x.enabled],
authentication: [x.authentication],
host: [x.host, [Validators.required]],
password: [x.password],
port: [x.port, [Validators.required]],
sender: [x.sender, [Validators.required, Validators.email]],
username: [x.username],
adminEmail: [x.adminEmail, [Validators.required, Validators.email]],
});
if (x.authentication) {
this.validationService.enableValidation(this.emailForm, 'username');
this.validationService.enableValidation(this.emailForm, 'password');
}
this.subscribeToAuthChanges();
});
} }
test() { onSubmit(form: FormGroup) {
// TODO Emby Service console.log(form.value, form.valid);
}
save() { if (form.invalid) {
this.settingsService.saveEmailNotificationSettings(this.settings).subscribe(x => { this.notificationService.error("Validation", "Please check your entered values");
return
}
var settings = <IEmailNotificationSettings>form.value;
settings.notificationTemplates = this.templates;
this.settingsService.saveEmailNotificationSettings(settings).subscribe(x => {
if (x) { if (x) {
this.notificationService.success("Settings Saved", "Successfully saved Email settings"); this.notificationService.success("Settings Saved", "Successfully saved Email settings");
} else { } else {
this.notificationService.success("Settings Saved", "There was an error when saving the Email settings"); this.notificationService.success("Settings Saved", "There was an error when saving the Email settings");
} }
}); });
}
save() {
}
private subscribeToAuthChanges() {
const authCtrl = this.emailForm.controls.authentication;
const changes$ = authCtrl.valueChanges;
changes$.subscribe((auth: boolean) => {
if (auth) {
this.validationService.enableValidation(this.emailForm, 'username');
this.validationService.enableValidation(this.emailForm, 'password');
} else {
this.validationService.disableValidation(this.emailForm, 'username');
this.validationService.disableValidation(this.emailForm, 'password');
}
});
} }
} }

View file

@ -1,18 +1,27 @@
 <i class="fa fa-info-circle" tooltipPosition="top" [escape]="false" pTooltip="{{helpText}}"></i>
<ngb-accordion [closeOthers]="true" activeIds="0-header"> <ngb-accordion [closeOthers]="true" activeIds="0-header">
<ngb-panel *ngFor="let template of templates" id="{{template.notificationType}}" title="{{NotificationType[template.notificationType] | humanize}}"> <ngb-panel *ngFor="let template of templates" id="{{template.notificationType}}" title="{{NotificationType[template.notificationType] | humanize}}">
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">Subject</label> <div class="checkbox">
<input type="checkbox" id="enabled" [(ngModel)]="template.enabled" ng-checked="template.enabled"><label for="enabled">Enable</label>
</div>
</div>
<div class="form-group">
<label class="control-label">Subject</label>
<div> <div>
<input type="text" class="form-control form-control-custom" [(ngModel)]="template.subject" value="{{template.subject}}"> <input type="text" class="form-control form-control-custom" [(ngModel)]="template.subject" value="{{template.subject}}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">Message</label> <label class="control-label">Message</label>
<div> <div>
<textarea type="text" class="form-control form-control-custom" [(ngModel)]="template.message" value="{{template.message}}"></textarea> <textarea type="text" class="form-control form-control-custom" [(ngModel)]="template.message" value="{{template.message}}"></textarea>
</div> </div>

View file

@ -9,4 +9,16 @@ import { INotificationTemplates, NotificationType } from '../../interfaces/INoti
export class NotificationTemplate { export class NotificationTemplate {
@Input() templates: INotificationTemplates[]; @Input() templates: INotificationTemplates[];
NotificationType = NotificationType; NotificationType = NotificationType;
helpText = `
{RequestedUser} : The User who requested the content <br/>
{RequestedDate} : The Date the media was requested <br/>
{Title} : The title of the request e.g. Lion King <br/>
{Type} : The request type e.g. Movie/Tv Show <br/>
{LongDate} : 15 June 2017 <br/>
{ShortDate} : 15/06/2017 <br/>
{LongTime} : 16:02:34 <br/>
{ShortTime} : 16:02 <br/>
`
} }

View file

@ -1,6 +1,6 @@
import { NgModule, } from '@angular/core'; import { NgModule, } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { NgbModule, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
@ -9,6 +9,7 @@ import { AuthGuard } from '../auth/auth.guard';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
import { SonarrService } from '../services/applications/sonarr.service'; import { SonarrService } from '../services/applications/sonarr.service';
import { RadarrService } from '../services/applications/radarr.service'; import { RadarrService } from '../services/applications/radarr.service';
import { ValidationService } from '../services/helpers/validation.service';
import { OmbiComponent } from './ombi/ombi.component'; import { OmbiComponent } from './ombi/ombi.component';
import { PlexComponent } from './plex/plex.component'; import { PlexComponent } from './plex/plex.component';
@ -23,7 +24,7 @@ import { NotificationTemplate } from './notifications/notificationtemplate.compo
import { SettingsMenuComponent } from './settingsmenu.component'; import { SettingsMenuComponent } from './settingsmenu.component';
import { HumanizePipe } from '../pipes/HumanizePipe'; import { HumanizePipe } from '../pipes/HumanizePipe';
import { MenuModule, InputSwitchModule, InputTextModule, TooltipModule } from 'primeng/primeng'; import { MenuModule, InputSwitchModule, InputTextModule, TooltipModule, AutoCompleteModule } from 'primeng/primeng';
const routes: Routes = [ const routes: Routes = [
{ path: 'Settings/Ombi', component: OmbiComponent, canActivate: [AuthGuard] }, { path: 'Settings/Ombi', component: OmbiComponent, canActivate: [AuthGuard] },
@ -40,6 +41,7 @@ const routes: Routes = [
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
MenuModule, MenuModule,
InputSwitchModule, InputSwitchModule,
@ -47,7 +49,8 @@ const routes: Routes = [
AuthModule, AuthModule,
NgbModule, NgbModule,
TooltipModule, TooltipModule,
NgbAccordionModule NgbAccordionModule,
AutoCompleteModule
], ],
declarations: [ declarations: [
SettingsMenuComponent, SettingsMenuComponent,
@ -70,6 +73,7 @@ const routes: Routes = [
AuthService, AuthService,
RadarrService, RadarrService,
AuthGuard, AuthGuard,
ValidationService
], ],
}) })

View file

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Models; using Ombi.Models;
using Ombi.Notifications.Models;
namespace Ombi.Controllers namespace Ombi.Controllers
{ {