From f63476260bea85d62033196222d5cf4b18ecbc04 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 20 Aug 2014 23:29:34 -0700 Subject: [PATCH] New: Show episode file file deletions in history and episode activity --- .../EpisodeFiles/EpisodeFileModule.cs | 2 +- src/NzbDrone.Api/Series/SeriesModule.cs | 3 +- .../Datastore/DatabaseRelationshipFixture.cs | 4 ++- .../MediaFileTableCleanupServiceFixture.cs | 4 +-- .../UpgradeMediaFileServiceFixture.cs | 4 +-- .../HandleEpisodeFileDeletedFixture.cs | 10 +++--- src/NzbDrone.Core/Datastore/TableMapping.cs | 6 +++- src/NzbDrone.Core/History/History.cs | 3 +- src/NzbDrone.Core/History/HistoryService.cs | 33 ++++++++++++++++++- .../MediaFiles/DeleteMediaFileReason.cs | 10 ++++++ src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 5 ++- .../Events/EpisodeFileDeletedEvent.cs | 9 +++-- .../MediaFiles/MediaFileService.cs | 12 ++++--- .../MediaFileTableCleanupService.cs | 4 +-- .../MediaFiles/UpgradeMediaFileService.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Tv/EpisodeService.cs | 3 +- src/UI/Cells/EventTypeCell.js | 4 +++ src/UI/Content/icons.less | 4 +++ .../Details/HistoryDetailsLayoutTemplate.hbs | 1 + .../Details/HistoryDetailsViewTemplate.hbs | 27 +++++++++++++++ 21 files changed, 122 insertions(+), 29 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index 09f54299f..874f3aaa3 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -80,7 +80,7 @@ namespace NzbDrone.Api.EpisodeFiles _logger.Info("Deleting episode file: {0}", fullPath); _recycleBinProvider.DeleteFile(fullPath); - _mediaFileService.Delete(episodeFile); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); } private EpisodeFileResource MapToResource(Core.Tv.Series series, EpisodeFile episodeFile) diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index d880b2a0b..0427c5803 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -5,6 +5,7 @@ using FluentValidation; using NzbDrone.Common; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -188,7 +189,7 @@ namespace NzbDrone.Api.Series public void Handle(EpisodeFileDeletedEvent message) { - if (message.ForUpgrade) return; + if (message.Reason == DeleteMediaFileReason.Upgrade) return; BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId); } diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 574faef24..a457d4d52 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -93,7 +93,9 @@ namespace NzbDrone.Core.Test.Datastore options => options .IncludingAllRuntimeProperties() .Excluding(c => c.DateAdded) - .Excluding(c => c.Path)); + .Excluding(c => c.Path) + .Excluding(c => c.Series) + .Excluding(c => c.Episodes)); } [Test] diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs index 8ca6fa341..47f34a07d 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaFileTableCleanupServiceFixture.cs @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new CleanMediaFileDb(0)); - Mocker.GetMock().Verify(c => c.Delete(It.Is(e => e.RelativePath == DELETED_PATH), false), Times.Exactly(2)); + Mocker.GetMock().Verify(c => c.Delete(It.Is(e => e.RelativePath == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2)); } [Test] @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Execute(new CleanMediaFileDb(0)); - Mocker.GetMock().Verify(c => c.Delete(It.IsAny(), false), Times.Exactly(10)); + Mocker.GetMock().Verify(c => c.Delete(It.IsAny(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10)); } [Test] diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index b3214704c..574b65e36 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -126,7 +126,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.UpgradeEpisodeFile(_episodeFile, _localEpisode); - Mocker.GetMock().Verify(v => v.Delete(It.IsAny(), true), Times.Once()); + Mocker.GetMock().Verify(v => v.Delete(It.IsAny(), DeleteMediaFileReason.Upgrade), Times.Once()); } [Test] @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.UpgradeEpisodeFile(_episodeFile, _localEpisode); - Mocker.GetMock().Verify(v => v.Delete(_localEpisode.Episodes.Single().EpisodeFile.Value, true), Times.Once()); + Mocker.GetMock().Verify(v => v.Delete(_localEpisode.Episodes.Single().EpisodeFile.Value, DeleteMediaFileReason.Upgrade), Times.Once()); } [Test] diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/HandleEpisodeFileDeletedFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/HandleEpisodeFileDeletedFixture.cs index 869a01929..21503277e 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/HandleEpisodeFileDeletedFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeProviderTests/HandleEpisodeFileDeletedFixture.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests { GivenSingleEpisodeFile(); - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, false)); + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Once()); @@ -69,7 +69,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests { GivenMultiEpisodeFile(); - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, false)); + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Exactly(2)); @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedEpisodes) .Returns(true); - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, false)); + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() .Verify(v => v.Update(It.Is(e => e.Monitored == false)), Times.Once()); @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedEpisodes) .Returns(false); - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, false)); + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); Mocker.GetMock() .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeProviderTests .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedEpisodes) .Returns(true); - Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, true)); + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); Mocker.GetMock() .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 128574cb7..85836aa12 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -64,7 +64,11 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("EpisodeFiles") .Ignore(f => f.Path) - .Relationships.AutoMapICollectionOrComplexProperties(); + .Relationships.AutoMapICollectionOrComplexProperties() + .For("Episodes") + .LazyLoad(condition: parent => parent.Id > 0, + query: (db, parent) => db.Query().Where(c => c.EpisodeFileId == parent.Id).ToList()) + .HasOne(file => file.Series, file => file.SeriesId); Mapper.Entity().RegisterModel("Episodes") .Ignore(e => e.SeriesTitle) diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 4208039e4..21341430a 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.History Grabbed = 1, SeriesFolderImported = 2, DownloadFolderImported = 3, - DownloadFailed = 4 + DownloadFailed = 4, + EpisodeFileDeleted = 5 } } \ No newline at end of file diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index d3482cfde..18c747ac9 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles; @@ -31,7 +32,11 @@ namespace NzbDrone.Core.History void UpdateHistoryData(Int32 historyId, Dictionary data); } - public class HistoryService : IHistoryService, IHandle, IHandle, IHandle + public class HistoryService : IHistoryService, + IHandle, + IHandle, + IHandle, + IHandle { private readonly IHistoryRepository _historyRepository; private readonly Logger _logger; @@ -199,5 +204,31 @@ namespace NzbDrone.Core.History _historyRepository.Insert(history); } } + + public void Handle(EpisodeFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes) + { + _logger.Debug("Removing episode file from DB as part of cleanup routine."); + return; + } + + foreach (var episode in message.EpisodeFile.Episodes.Value) + { + var history = new History + { + EventType = HistoryEventType.EpisodeFileDeleted, + Date = DateTime.UtcNow, + Quality = message.EpisodeFile.Quality, + SourceTitle = message.EpisodeFile.Path, + SeriesId = message.EpisodeFile.SeriesId, + EpisodeId = episode.Id, + }; + + history.Data.Add("Reason", message.Reason.ToString()); + + _historyRepository.Insert(history); + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs b/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs new file mode 100644 index 000000000..918eedc31 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DeleteMediaFileReason.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.MediaFiles +{ + public enum DeleteMediaFileReason + { + MissingFromDisk, + Manual, + Upgrade, + NoLinkedEpisodes + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 163a1ea80..8da87eca4 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Marr.Data; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -18,7 +20,8 @@ namespace NzbDrone.Core.MediaFiles public String ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } - public LazyList Episodes { get; set; } + public LazyLoaded> Episodes { get; set; } + public LazyLoaded Series { get; set; } public override String ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs index 65548c6bb..2cbc177a2 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeFileDeletedEvent.cs @@ -1,17 +1,16 @@ -using System; -using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Events { public class EpisodeFileDeletedEvent : IEvent { public EpisodeFile EpisodeFile { get; private set; } - public Boolean ForUpgrade { get; private set; } + public DeleteMediaFileReason Reason { get; private set; } - public EpisodeFileDeletedEvent(EpisodeFile episodeFile, Boolean forUpgrade) + public EpisodeFileDeletedEvent(EpisodeFile episodeFile, DeleteMediaFileReason reason) { EpisodeFile = episodeFile; - ForUpgrade = forUpgrade; + Reason = reason; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 9e155cdf5..76b623004 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; @@ -14,7 +15,7 @@ namespace NzbDrone.Core.MediaFiles { EpisodeFile Add(EpisodeFile episodeFile); void Update(EpisodeFile episodeFile); - void Delete(EpisodeFile episodeFile, bool forUpgrade = false); + void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); List GetFilesWithoutMediaInfo(); @@ -49,11 +50,14 @@ namespace NzbDrone.Core.MediaFiles _mediaFileRepository.Update(episodeFile); } - public void Delete(EpisodeFile episodeFile, bool forUpgrade = false) + public void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason) { - _mediaFileRepository.Delete(episodeFile); + //Little hack so we have the episodes and series attached for the event consumers + episodeFile.Episodes.LazyLoad(); + episodeFile.Path = Path.Combine(episodeFile.Series.Value.Path, episodeFile.RelativePath); - _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, forUpgrade)); + _mediaFileRepository.Delete(episodeFile); + _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); } public List GetFilesBySeries(int seriesId) diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index de27a5cbc..45f31515e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -47,14 +47,14 @@ namespace NzbDrone.Core.MediaFiles if (!_diskProvider.FileExists(episodeFilePath)) { _logger.Debug("File [{0}] no longer exists on disk, removing from db", episodeFilePath); - _mediaFileService.Delete(episodeFile); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.MissingFromDisk); continue; } if (!episodes.Any(e => e.EpisodeFileId == episodeFile.Id)) { _logger.Debug("File [{0}] is not assigned to any episodes, removing from db", episodeFilePath); - _mediaFileService.Delete(episodeFile); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.NoLinkedEpisodes); continue; } diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 6d5a94be2..256e0e04b 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.MediaFiles } moveFileResult.OldFiles.Add(file); - _mediaFileService.Delete(file, true); + _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); } if (copyOnly) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c92a948cb..ce2d549da 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -460,6 +460,7 @@ + Code diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index cd46c6b99..922f25ffc 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -185,7 +186,7 @@ namespace NzbDrone.Core.Tv _logger.Debug("Detaching episode {0} from file.", episode.Id); episode.EpisodeFileId = 0; - if (!message.ForUpgrade && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes) + if (message.Reason != DeleteMediaFileReason.Upgrade && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes) { episode.Monitored = false; } diff --git a/src/UI/Cells/EventTypeCell.js b/src/UI/Cells/EventTypeCell.js index 47c179061..1ba180050 100644 --- a/src/UI/Cells/EventTypeCell.js +++ b/src/UI/Cells/EventTypeCell.js @@ -33,6 +33,10 @@ define( icon = 'icon-nd-download-failed'; toolTip = 'Episode download failed'; break; + case 'episodeFileDeleted': + icon = 'icon-nd-deleted'; + toolTip = 'Episode file deleted'; + break; default : icon = 'icon-question'; toolTip = 'unknown event'; diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 575315487..f9a53429f 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -195,3 +195,7 @@ .icon-nd-manual-search:before { .icon(@user); } + +.icon-nd-deleted:before { + .icon(@trash); +} diff --git a/src/UI/History/Details/HistoryDetailsLayoutTemplate.hbs b/src/UI/History/Details/HistoryDetailsLayoutTemplate.hbs index cdbfbfa04..455bcf0c4 100644 --- a/src/UI/History/Details/HistoryDetailsLayoutTemplate.hbs +++ b/src/UI/History/Details/HistoryDetailsLayoutTemplate.hbs @@ -7,6 +7,7 @@ {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} {{#if_eq eventType compare="downloadFolderImported"}}Episode Imported{{/if_eq}} + {{#if_eq eventType compare="episodeFileDeleted"}}Episode File Deleted{{/if_eq}} diff --git a/src/UI/History/Details/HistoryDetailsViewTemplate.hbs b/src/UI/History/Details/HistoryDetailsViewTemplate.hbs index 0a17b5161..db2fe1327 100644 --- a/src/UI/History/Details/HistoryDetailsViewTemplate.hbs +++ b/src/UI/History/Details/HistoryDetailsViewTemplate.hbs @@ -36,6 +36,7 @@ {{/with}} {{/if_eq}} + {{#if_eq eventType compare="downloadFailed"}}
@@ -48,6 +49,7 @@ {{/with}}
{{/if_eq}} + {{#if_eq eventType compare="downloadFolderImported"}}
@@ -68,4 +70,29 @@ {{/if}} {{/with}}
+{{/if_eq}} + +{{#if_eq eventType compare="episodeFileDeleted"}} +
+ +
Path:
+
{{sourceTitle}}
+ + {{#with data}} +
Reason:
+
+ {{#if_eq reason compare="Manual"}} + File was deleted by via UI + {{/if_eq}} + + {{#if_eq reason compare="MissingFromDisk"}} + NzbDrone was unable to find the file on disk so it was removed + {{/if_eq}} + + {{#if_eq reason compare="Upgrade"}} + File was deleted to imported an upgrade + {{/if_eq}} +
+ {{/with}} +
{{/if_eq}} \ No newline at end of file