New: Added global Remote Path mapping table to replace individual Local Category Path settings.

pull/4/head
Taloth Saldono 10 years ago
parent 8281063698
commit 525f1aa9dd

@ -96,6 +96,8 @@
<Compile Include="ClientSchema\SelectOption.cs" />
<Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandResource.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingModule.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
<Compile Include="Config\UiConfigModule.cs" />
<Compile Include="Config\UiConfigResource.cs" />
<Compile Include="Config\DownloadClientConfigModule.cs" />

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Api.Mapping;
using NzbDrone.Common;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Validation.Paths;
using Omu.ValueInjecter;
namespace NzbDrone.Api.Config
{
public class RemotePathMappingModule : NzbDroneRestModule<RemotePathMappingResource>
{
private readonly IRemotePathMappingService _remotePathMappingService;
public RemotePathMappingModule(IConfigService configService, IRemotePathMappingService remotePathMappingService, PathExistsValidator pathExistsValidator)
{
_remotePathMappingService = remotePathMappingService;
GetResourceAll = GetMappings;
GetResourceById = GetMappingById;
CreateResource = CreateMapping;
DeleteResource = DeleteMapping;
UpdateResource = UpdateMapping;
SharedValidator.RuleFor(c => c.Host)
.NotEmpty();
// We cannot use IsValidPath here, because it's a remote path, possibly other OS.
SharedValidator.RuleFor(c => c.RemotePath)
.NotEmpty();
SharedValidator.RuleFor(c => c.LocalPath)
.Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
.SetValidator(pathExistsValidator);
}
private RemotePathMappingResource GetMappingById(int id)
{
return _remotePathMappingService.Get(id).InjectTo<RemotePathMappingResource>();
}
private int CreateMapping(RemotePathMappingResource rootFolderResource)
{
return GetNewId<RemotePathMapping>(_remotePathMappingService.Add, rootFolderResource);
}
private List<RemotePathMappingResource> GetMappings()
{
return ToListResource(_remotePathMappingService.All);
}
private void DeleteMapping(int id)
{
_remotePathMappingService.Remove(id);
}
private void UpdateMapping(RemotePathMappingResource resource)
{
var mapping = _remotePathMappingService.Get(resource.Id);
mapping.InjectFrom(resource);
_remotePathMappingService.Update(mapping);
}
}
}

@ -0,0 +1,12 @@
using System;
using NzbDrone.Api.REST;
namespace NzbDrone.Api.Config
{
public class RemotePathMappingResource : RestResource
{
public String Host { get; set; }
public String RemotePath { get; set; }
public String LocalPath { get; set; }
}
}

@ -11,6 +11,8 @@ using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Test.Download.DownloadClientTests
{
@ -29,13 +31,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), null))
.Returns(CreateRemoteEpisode());
.Returns(() => CreateRemoteEpisode());
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal(It.IsAny<String>(), It.IsAny<String>()))
.Returns<String, String>((h,r) => r);
}
protected virtual RemoteEpisode CreateRemoteEpisode()

@ -13,6 +13,7 @@ using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Test.Common;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
{
@ -92,11 +93,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
.Returns(configItems);
}
protected void GivenMountPoint(String mountPath)
{
(Subject.Definition.Settings as NzbgetSettings).TvCategoryLocalPath = mountPath;
}
protected void GivenFailedDownload()
{
Mocker.GetMock<INzbgetProxy>()
@ -251,7 +247,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
[Test]
public void should_return_status_with_mounted_outputdir()
{
GivenMountPoint(@"O:\mymount".AsOsAgnostic());
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv"))
.Returns(@"O:\mymount".AsOsAgnostic());
var result = Subject.GetStatus();
@ -263,7 +261,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests
[Test]
public void should_remap_storage_if_mounted()
{
GivenMountPoint(@"O:\mymount".AsOsAgnostic());
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"))
.Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic());
GivenQueue(null);
GivenHistory(_completed);

@ -14,6 +14,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
{
@ -106,11 +107,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
.Returns(_config);
}
protected void GivenMountPoint(String mountPath)
{
(Subject.Definition.Settings as SabnzbdSettings).TvCategoryLocalPath = mountPath;
}
protected void GivenFailedDownload()
{
Mocker.GetMock<ISabnzbdProxy>()
@ -303,7 +299,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
[Test]
public void should_remap_storage_if_mounted()
{
GivenMountPoint(@"O:\mymount".AsOsAgnostic());
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"))
.Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic());
GivenQueue(null);
GivenHistory(_completed);
@ -361,7 +359,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
[Test]
public void should_return_status_with_mounted_outputdir()
{
GivenMountPoint(@"O:\mymount".AsOsAgnostic());
Mocker.GetMock<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv"))
.Returns(@"O:\mymount".AsOsAgnostic());
GivenQueue(null);

@ -197,6 +197,7 @@
<Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecificationFixture.cs" />
<Compile Include="MediaFiles\ImportApprovedEpisodesFixture.cs" />
<Compile Include="MediaFiles\MediaFileRepositoryFixture.cs" />
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
<Compile Include="OrganizerTests\CleanFixture.cs" />
<Compile Include="MediaFiles\MediaFileServiceTests\FilterFixture.cs" />
<Compile Include="MediaFiles\MediaFileTableCleanupServiceFixture.cs" />

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using FizzWare.NBuilder;
namespace NzbDrone.Core.Test.RemotePathMappingsTests
{
[TestFixture]
public class RemotePathMappingServiceFixture : CoreTest<RemotePathMappingService>
{
[SetUp]
public void Setup()
{
Mocker.GetMock<IDiskProvider>()
.Setup(m => m.FolderExists(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IRemotePathMappingRepository>()
.Setup(s => s.All())
.Returns(new List<RemotePathMapping>());
}
private void GivenMapping()
{
var mappings = Builder<RemotePathMapping>.CreateListOfSize(1)
.All()
.With(v => v.Host = "my-server.localdomain")
.With(v => v.RemotePath = "/mnt/storage/")
.With(v => v.LocalPath = @"D:\mountedstorage\".AsOsAgnostic())
.BuildListOfNew();
Mocker.GetMock<IRemotePathMappingRepository>()
.Setup(s => s.All())
.Returns(mappings);
}
private void WithNonExistingFolder()
{
Mocker.GetMock<IDiskProvider>()
.Setup(m => m.FolderExists(It.IsAny<string>()))
.Returns(false);
}
[TestCase("my-first-server.localdomain", "/mnt/storage", @"D:\storage1")]
[TestCase("my-server.localdomain", "/mnt/storage2", @"D:\storage2")]
public void should_be_able_to_add_new_mapping(String host, String remotePath, String localPath)
{
GivenMapping();
localPath = localPath.AsOsAgnostic();
var mapping = new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath };
Subject.Add(mapping);
Mocker.GetMock<IRemotePathMappingRepository>().Verify(c => c.Insert(mapping), Times.Once());
}
[Test]
public void should_be_able_to_remove_mapping()
{
Subject.Remove(1);
Mocker.GetMock<IRemotePathMappingRepository>().Verify(c => c.Delete(1), Times.Once());
}
[TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")]
[TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage2")]
public void adding_duplicated_mapping_should_throw(String host, String remotePath, String localPath)
{
localPath = localPath.AsOsAgnostic();
GivenMapping();
var mapping = new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath };
Assert.Throws<InvalidOperationException>(() => Subject.Add(mapping));
}
[TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")]
[TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")]
public void should_remap_remote_to_local(String host, String remotePath, String expectedLocalPath)
{
expectedLocalPath = expectedLocalPath.AsOsAgnostic();
GivenMapping();
var result = Subject.RemapRemoteToLocal(host, remotePath);
result.Should().Be(expectedLocalPath);
}
[TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")]
[TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")]
[TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")]
[TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")]
public void should_remap_local_to_remote(String host, String expectedRemotePath, String localPath)
{
localPath = localPath.AsOsAgnostic();
GivenMapping();
var result = Subject.RemapLocalToRemote(host, localPath);
result.Should().Be(expectedRemotePath);
}
}
}

@ -0,0 +1,17 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(63)]
public class add_remotepathmappings : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("RemotePathMappings")
.WithColumn("Host").AsString()
.WithColumn("RemotePath").AsString()
.WithColumn("LocalPath").AsString();
}
}
}

@ -16,6 +16,7 @@ using NzbDrone.Core.Jobs;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
@ -87,6 +88,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<PendingRelease>().RegisterModel("PendingReleases")
.Ignore(e => e.RemoteEpisode);
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
}
private static void RegisterMappers()

@ -12,6 +12,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Nzbget
{
@ -24,8 +25,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(httpClient, configService, diskProvider, parsingService, logger)
: base(httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_proxy = proxy;
}
@ -145,7 +147,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);
historyItem.OutputPath = item.DestDir;
historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, item.DestDir);
historyItem.Category = item.Category;
historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus);
historyItem.Status = DownloadItemStatus.Completed;
@ -172,31 +174,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public override IEnumerable<DownloadClientItem> GetItems()
{
Dictionary<String, String> config = null;
NzbgetCategory category = null;
try
{
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
config = _proxy.GetConfig(Settings);
category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory);
}
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
yield break;
}
MigrateLocalCategoryPath();
foreach (var downloadClientItem in GetQueue().Concat(GetHistory()))
{
if (downloadClientItem.Category == Settings.TvCategory)
{
if (category != null)
{
RemapStorage(downloadClientItem, category.DestDir, Settings.TvCategoryLocalPath);
}
downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title);
if (downloadClientItem.RemoteEpisode == null) continue;
@ -230,14 +213,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
if (category != null)
{
if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
status.OutputRootFolders = new List<String> { category.DestDir };
}
else
{
status.OutputRootFolders = new List<String> { Settings.TvCategoryLocalPath };
}
status.OutputRootFolders = new List<String> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.DestDir) };
}
return status;
@ -279,11 +255,6 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
failures.AddIfNotNull(TestConnection());
failures.AddIfNotNull(TestCategory());
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
failures.AddIfNotNull(TestFolder(Settings.TvCategoryLocalPath, "TvCategoryLocalPath"));
}
}
private ValidationFailure TestConnection()
@ -333,5 +304,35 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return result;
}
// TODO: Remove around January 2015, this code moves the settings to the RemotePathMappingService.
private void MigrateLocalCategoryPath()
{
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
try
{
_logger.Debug("Has legacy TvCategoryLocalPath, trying to migrate to RemotePathMapping list.");
var config = _proxy.GetConfig(Settings);
var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory);
if (category != null)
{
var localPath = Settings.TvCategoryLocalPath;
Settings.TvCategoryLocalPath = null;
_remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.DestDir, localPath);
_logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name);
}
}
catch (DownloadClientException ex)
{
_logger.ErrorException("Unable to migrate local category path", ex);
throw;
}
}
}
}
}

@ -49,16 +49,16 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox)]
public String TvCategory { get; set; }
[FieldDefinition(5, Label = "Category Local Path", Type = FieldType.Textbox, Advanced = true, HelpText = "Local path to the category output dir. Useful if Nzbget runs on another computer.")]
// TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate.
public String TvCategoryLocalPath { get; set; }
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public Int32 RecentTvPriority { get; set; }
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public Int32 OlderTvPriority { get; set; }
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)]
public Boolean UseSsl { get; set; }
public ValidationResult Validate()

@ -8,6 +8,7 @@ using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@ -22,8 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(configService, diskProvider, parsingService, logger)
: base(configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_httpClient = httpClient;
}

@ -13,6 +13,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
@ -25,8 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(httpClient, configService, diskProvider, parsingService, logger)
: base(httpClient, configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_proxy = proxy;
}
@ -147,11 +149,13 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
historyItem.Status = DownloadItemStatus.Downloading;
}
if (!sabHistoryItem.Storage.IsNullOrWhiteSpace())
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, sabHistoryItem.Storage);
if (!outputPath.IsNullOrWhiteSpace())
{
historyItem.OutputPath = sabHistoryItem.Storage;
historyItem.OutputPath = outputPath;
var parent = sabHistoryItem.Storage.GetParentPath();
var parent = outputPath.GetParentPath();
while (parent != null)
{
if (Path.GetFileName(parent) == sabHistoryItem.Title)
@ -170,31 +174,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public override IEnumerable<DownloadClientItem> GetItems()
{
SabnzbdConfig config = null;
SabnzbdCategory category = null;
try
{
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
config = _proxy.GetConfig(Settings);
category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory);
}
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
yield break;
}
MigrateLocalCategoryPath();
foreach (var downloadClientItem in GetQueue().Concat(GetHistory()))
{
if (downloadClientItem.Category == Settings.TvCategory)
{
if (category != null)
{
RemapStorage(downloadClientItem, category.FullPath, Settings.TvCategoryLocalPath);
}
downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title);
if (downloadClientItem.RemoteEpisode == null) continue;
@ -323,14 +308,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
if (category != null)
{
if (Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
status.OutputRootFolders = new List<String> { category.FullPath };
}
else
{
status.OutputRootFolders = new List<String> { Settings.TvCategoryLocalPath };
}
status.OutputRootFolders = new List<String> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) };
}
return status;
@ -342,12 +320,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
failures.AddIfNotNull(TestAuthentication());
failures.AddIfNotNull(TestGlobalConfig());
failures.AddIfNotNull(TestCategory());
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
failures.AddIfNotNull(TestFolder(Settings.TvCategoryLocalPath, "TvCategoryLocalPath"));
failures.AddIfNotNull(TestCategoryLocalPath());
}
}
private ValidationFailure TestConnection()
@ -447,14 +419,34 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
return null;
}
private ValidationFailure TestCategoryLocalPath()
private void MigrateLocalCategoryPath()
{
if (Settings.Host == "127.0.0.1" || Settings.Host == "localhost")
// TODO: Remove around January 2015, this code moves the settings to the RemotePathMappingService.
if (!Settings.TvCategoryLocalPath.IsNullOrWhiteSpace())
{
return new ValidationFailure("TvCategoryLocalPath", "Do not set when SABnzbd is running on the same system as NzbDrone");
}
try
{
_logger.Debug("Has legacy TvCategoryLocalPath, trying to migrate to RemotePathMapping list.");
return null;
var config = _proxy.GetConfig(Settings);
var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory);
if (category != null)
{
var localPath = Settings.TvCategoryLocalPath;
Settings.TvCategoryLocalPath = null;
_remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.FullPath, localPath);
_logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name);
}
}
catch (DownloadClientException ex)
{
_logger.ErrorException("Unable to migrate local category path", ex);
throw;
}
}
}
}
}

@ -25,9 +25,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
RuleFor(c => c.Password).NotEmpty()
.WithMessage("Password is required when API key is not configured")
.When(c => String.IsNullOrWhiteSpace(c.ApiKey));
RuleFor(c => c.TvCategory).NotEmpty().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath));
RuleFor(c => c.TvCategoryLocalPath).IsValidPath().When(c => !String.IsNullOrWhiteSpace(c.TvCategoryLocalPath));
}
}
@ -62,16 +59,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(5, Label = "Category", Type = FieldType.Textbox)]
public String TvCategory { get; set; }
[FieldDefinition(6, Label = "Category Local Path", Type = FieldType.Textbox, Advanced = true, HelpText = "Local path to the category output dir. Useful if Sabnzbd runs on another computer.")]
// TODO: Remove around January 2015, this setting was superceded by the RemotePathMappingService, but has to remain for a while to properly migrate.
public String TvCategoryLocalPath { get; set; }
[FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
[FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public Int32 RecentTvPriority { get; set; }
[FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
[FieldDefinition(7, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public Int32 OlderTvPriority { get; set; }
[FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)]
[FieldDefinition(8, Label = "Use SSL", Type = FieldType.Checkbox)]
public Boolean UseSsl { get; set; }
public ValidationResult Validate()

@ -14,6 +14,7 @@ using NzbDrone.Core.MediaFiles;
using NLog;
using Omu.ValueInjecter;
using FluentValidation.Results;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.UsenetBlackhole
{
@ -27,8 +28,9 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(configService, diskProvider, parsingService, logger)
: base(configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_diskScanService = diskScanService;
_httpClient = httpClient;

@ -12,6 +12,7 @@ using NzbDrone.Core.Configuration;
using NLog;
using FluentValidation.Results;
using NzbDrone.Core.Validation;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download
{
@ -21,6 +22,7 @@ namespace NzbDrone.Core.Download
protected readonly IConfigService _configService;
protected readonly IDiskProvider _diskProvider;
protected readonly IParsingService _parsingService;
protected readonly IRemotePathMappingService _remotePathMappingService;
protected readonly Logger _logger;
public Type ConfigContract
@ -49,11 +51,12 @@ namespace NzbDrone.Core.Download
}
}
protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, Logger logger)
protected DownloadClientBase(IConfigService configService, IDiskProvider diskProvider, IParsingService parsingService, IRemotePathMappingService remotePathMappingService, Logger logger)
{
_configService = configService;
_diskProvider = diskProvider;
_parsingService = parsingService;
_remotePathMappingService = remotePathMappingService;
_logger = logger;
}
@ -84,23 +87,6 @@ namespace NzbDrone.Core.Download
return remoteEpisode;
}
protected void RemapStorage(DownloadClientItem downloadClientItem, String remotePath, String localPath)
{
if (downloadClientItem.OutputPath.IsNullOrWhiteSpace() || localPath.IsNullOrWhiteSpace())
{
return;
}
remotePath = remotePath.TrimEnd('/', '\\');
localPath = localPath.TrimEnd('/', '\\');
if (downloadClientItem.OutputPath.StartsWith(remotePath))
{
downloadClientItem.OutputPath = localPath + downloadClientItem.OutputPath.Substring(remotePath.Length);
downloadClientItem.OutputPath = downloadClientItem.OutputPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
}
public ValidationResult Test()
{
var failures = new List<ValidationFailure>();

@ -15,6 +15,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Configuration;
using NLog;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download
{
@ -24,11 +25,12 @@ namespace NzbDrone.Core.Download
protected readonly IHttpClient _httpClient;
protected UsenetClientBase(IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
Logger logger)
: base(configService, diskProvider, parsingService, logger)
IConfigService configService,
IDiskProvider diskProvider,
IParsingService parsingService,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(configService, diskProvider, parsingService, remotePathMappingService, logger)
{
_httpClient = httpClient;
}

@ -223,6 +223,7 @@
<Compile Include="Datastore\Migration\057_convert_episode_file_path_to_relative.cs" />
<Compile Include="Datastore\Migration\058_drop_epsiode_file_path.cs" />
<Compile Include="Datastore\Migration\059_add_enable_options_to_indexers.cs" />
<Compile Include="Datastore\Migration\063_add_remotepathmappings.cs" />
<Compile Include="Datastore\Migration\061_clear_bad_scene_names.cs" />
<Compile Include="Datastore\Migration\060_remove_enable_from_indexers.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
@ -560,6 +561,9 @@
<Compile Include="MetadataSource\Trakt\TraktException.cs" />
<Compile Include="MetadataSource\TraktProxy.cs" />
<Compile Include="MetadataSource\Tvdb\TvdbProxy.cs" />
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingService.cs" />
<Compile Include="Notifications\DownloadMessage.cs" />
<Compile Include="Notifications\Email\Email.cs">
<SubType>Code</SubType>

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.RemotePathMappings
{
public class RemotePathMapping : ModelBase
{
public String Host { get; set; }
public String RemotePath { get; set; }
public String LocalPath { get; set; }
}
}

@ -0,0 +1,27 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.RemotePathMappings
{
public interface IRemotePathMappingRepository : IBasicRepository<RemotePathMapping>
{
}
public class RemotePathMappingRepository : BasicRepository<RemotePathMapping>, IRemotePathMappingRepository
{
public RemotePathMappingRepository(IDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
protected override bool PublishModelEvents
{
get
{
return true;
}
}
}
}

@ -0,0 +1,222 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Tv;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Download;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.RemotePathMappings
{
public interface IRemotePathMappingService
{
List<RemotePathMapping> All();
RemotePathMapping Add(RemotePathMapping mapping);
void Remove(int id);
RemotePathMapping Get(int id);
RemotePathMapping Update(RemotePathMapping mapping);
String RemapRemoteToLocal(String host, String remotePath);
String RemapLocalToRemote(String host, String localPath);
// TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings.
void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath);
}
public class RemotePathMappingService : IRemotePathMappingService
{
// TODO: Remove DownloadClientRepository reference around January 2015. Used to migrate legacy Local Category Path settings.
private readonly IDownloadClientRepository _downloadClientRepository;
private readonly IRemotePathMappingRepository _remotePathMappingRepository;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
private readonly ICached<List<RemotePathMapping>> _cache;
public RemotePathMappingService(IDownloadClientRepository downloadClientRepository,
IRemotePathMappingRepository remotePathMappingRepository,
IDiskProvider diskProvider,
ICacheManager cacheManager,
Logger logger)
{
_downloadClientRepository = downloadClientRepository;
_remotePathMappingRepository = remotePathMappingRepository;
_diskProvider = diskProvider;
_logger = logger;
_cache = cacheManager.GetCache<List<RemotePathMapping>>(GetType());
}
public List<RemotePathMapping> All()
{
return _cache.Get("all", () => _remotePathMappingRepository.All().ToList(), TimeSpan.FromSeconds(10));
}
public RemotePathMapping Add(RemotePathMapping mapping)
{
mapping.LocalPath = CleanPath(mapping.LocalPath);
mapping.RemotePath = CleanPath(mapping.RemotePath);
var all = All();
ValidateMapping(all, mapping);
var result = _remotePathMappingRepository.Insert(mapping);
_cache.Clear();
return result;
}
public void Remove(int id)
{
_remotePathMappingRepository.Delete(id);
_cache.Clear();
}
public RemotePathMapping Get(int id)
{
return _remotePathMappingRepository.Get(id);
}
public RemotePathMapping Update(RemotePathMapping mapping)
{
var existing = All().Where(v => v.Id != mapping.Id).ToList();
ValidateMapping(existing, mapping);
var result = _remotePathMappingRepository.Update(mapping);
_cache.Clear();
return result;
}
private void ValidateMapping(List<RemotePathMapping> existing, RemotePathMapping mapping)
{
if (mapping.Host.IsNullOrWhiteSpace())
{
throw new ArgumentException("Invalid Host");
}
if (mapping.RemotePath.IsNullOrWhiteSpace())
{
throw new ArgumentException("Invalid RemotePath");
}
if (mapping.LocalPath.IsNullOrWhiteSpace() || !Path.IsPathRooted(mapping.LocalPath))
{
throw new ArgumentException("Invalid LocalPath");
}
if (!_diskProvider.FolderExists(mapping.LocalPath))
{
throw new DirectoryNotFoundException("Can't add mount point directory that doesn't exist.");
}
if (existing.Exists(r => r.Host == mapping.Host && r.RemotePath == mapping.RemotePath))
{
throw new InvalidOperationException("RemotePath already mounted.");
}
}
public String RemapRemoteToLocal(String host, String remotePath)
{
if (remotePath.IsNullOrWhiteSpace())
{
return remotePath;
}
var cleanRemotePath = CleanPath(remotePath);
foreach (var mapping in All())
{
if (host == mapping.Host && cleanRemotePath.StartsWith(mapping.RemotePath))
{
var localPath = mapping.LocalPath + cleanRemotePath.Substring(mapping.RemotePath.Length);
localPath = localPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
if (!remotePath.EndsWith("/") && !remotePath.EndsWith("\\"))
{
localPath = localPath.TrimEnd('/', '\\');
}
return localPath;
}
}
return remotePath;
}
public String RemapLocalToRemote(String host, String localPath)
{
if (localPath.IsNullOrWhiteSpace())
{
return localPath;
}
var cleanLocalPath = CleanPath(localPath);
foreach (var mapping in All())
{
if (host != mapping.Host) continue;
if (cleanLocalPath.StartsWith(mapping.LocalPath))
{
var remotePath = mapping.RemotePath + cleanLocalPath.Substring(mapping.LocalPath.Length);
remotePath = remotePath.Replace(Path.DirectorySeparatorChar, mapping.RemotePath.Contains('\\') ? '\\' : '/');
if (!localPath.EndsWith("/") && !localPath.EndsWith("\\"))
{
remotePath = remotePath.TrimEnd('/', '\\');
}
return remotePath;
}
}
return localPath;
}
// TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings.
public void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath)
{
_logger.Debug("Migrating local category path for Host {0}/{1} to {2}", host, remotePath, localPath);
var existingMappings = All().Where(v => v.Host == host).ToList();
remotePath = CleanPath(remotePath);
localPath = CleanPath(localPath);
if (!existingMappings.Any(v => v.LocalPath == localPath && v.RemotePath == remotePath))
{
Add(new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath });
}
var downloadClient = _downloadClientRepository.Get(downloadClientId);
downloadClient.Settings = newSettings;
_downloadClientRepository.Update(downloadClient);
}
private static String CleanPath(String path)
{
if (path.Contains('\\'))
{
return path.TrimEnd('\\', '/') + "\\";
}
else
{
return path.TrimEnd('\\', '/') + "/";
}
}
}
}

@ -4,9 +4,11 @@ define([
'marionette',
'Settings/DownloadClient/DownloadClientCollection',
'Settings/DownloadClient/DownloadClientCollectionView',
'Settings/DownloadClient/DownloadHandling/DownloadHandlingView',
'Settings/DownloadClient/DroneFactory/DroneFactoryView',
'Settings/DownloadClient/DownloadHandling/DownloadHandlingView'
], function (Marionette, DownloadClientCollection, CollectionView, DroneFactoryView, DownloadHandlingView) {
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView'
], function (Marionette, DownloadClientCollection, DownloadClientCollectionView, DownloadHandlingView, DroneFactoryView, RemotePathMappingCollection, RemotePathMappingCollectionView) {
return Marionette.Layout.extend({
template : 'Settings/DownloadClient/DownloadClientLayoutTemplate',
@ -14,18 +16,22 @@ define([
regions: {
downloadClients : '#x-download-clients-region',
downloadHandling : '#x-download-handling-region',
droneFactory : '#x-dronefactory-region'
droneFactory : '#x-dronefactory-region',
remotePathMappings : '#x-remotepath-mapping-region'
},
initialize: function () {
this.downloadClientsCollection = new DownloadClientCollection();
this.downloadClientsCollection.fetch();
this.remotePathMappingCollection = new RemotePathMappingCollection();
this.remotePathMappingCollection.fetch();
},
onShow: function () {
this.downloadClients.show(new CollectionView({ collection: this.downloadClientsCollection }));
this.downloadClients.show(new DownloadClientCollectionView({ collection: this.downloadClientsCollection }));
this.downloadHandling.show(new DownloadHandlingView({ model: this.model }));
this.droneFactory.show(new DroneFactoryView({ model: this.model }));
this.remotePathMappings.show(new RemotePathMappingCollectionView({ collection: this.remotePathMappingCollection }));
}
});
});

@ -1,6 +1,6 @@
<div id="x-download-clients-region"></div>
<div class="form-horizontal">
<div id="x-download-handling-region"></div>
<div id="x-dronefactory-region"></div>
<div id="x-remotepath-mapping-region"></div>
</div>

@ -0,0 +1,11 @@
'use strict';
define([
'backbone',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel'
], function (Backbone, RemotePathMappingModel) {
return Backbone.Collection.extend({
model : RemotePathMappingModel,
url : window.NzbDrone.ApiRoot + '/remotePathMapping'
});
});

@ -0,0 +1,28 @@
'use strict';
define([
'AppLayout',
'marionette',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel',
'bootstrap'
], function (AppLayout, Marionette, RemotePathMappingItemView, EditView, RemotePathMappingModel) {
return Marionette.CompositeView.extend({
template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate',
itemViewContainer : '.x-rows',
itemView : RemotePathMappingItemView,
events: {
'click .x-add' : '_addMapping'
},
_addMapping: function() {
var model = new RemotePathMappingModel();
model.collection = this.collection;
var view = new EditView({ model: model, targetCollection: this.collection});
AppLayout.modalRegion.show(view);
}
});
});

@ -0,0 +1,24 @@
<fieldset class="advanced-setting">
<legend>Remote Path Mappings</legend>
<div class="col-md-12">
<div id="remotepath-mapping-list">
<div class="remotepath-header x-header hidden-xs">
<div class="row">
<span class="col-sm-2">Host</span>
<span class="col-sm-5">Remote Path</span>
<span class="col-sm-4">Local Path</span>
</div>
</div>
<div class="rows x-rows">
</div>
<div class="remotepath-footer">
<div class="pull-right">
<span class="add-remotepath-mapping">
<i class="icon-nd-add x-add" title="Add new mapping" />
</span>
</div>
</div>
</div>
</div>
</fieldset>

@ -0,0 +1,23 @@
'use strict';
define([
'vent',
'marionette'
], function (vent, Marionette) {
return Marionette.ItemView.extend({
template: 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate',
events: {
'click .x-confirm-delete': '_delete'
},
_delete: function () {
this.model.destroy({
wait : true,
success: function () {
vent.trigger(vent.Commands.CloseModalCommand);
}
});
}
});
});

@ -0,0 +1,13 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Delete Mapping</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the mapping for '{{localPath}}'?</p>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-danger x-confirm-delete">delete</button>
</div>
</div>

@ -0,0 +1,51 @@
'use strict';
define([
'underscore',
'vent',
'AppLayout',
'marionette',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView',
'Commands/CommandController',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView',
'Mixins/AsEditModalView',
'Mixins/AutoComplete',
'bootstrap'
], function (_, vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, AsEditModalView) {
var view = Marionette.ItemView.extend({
template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate',
ui : {
path : '.x-path',
modalBody : '.modal-body'
},
_deleteView: DeleteView,
initialize : function (options) {
this.targetCollection = options.targetCollection;
},
onShow : function () {
//Hack to deal with modals not overflowing
if (this.ui.path.length > 0) {
this.ui.modalBody.addClass('modal-overflow');
}
this.ui.path.autoComplete('/directories');
},
_onAfterSave : function () {
this.targetCollection.add(this.model, { merge : true });
vent.trigger(vent.Commands.CloseModalCommand);
}
});
AsModelBoundView.call(view);
AsValidatedView.call(view);
AsEditModalView.call(view);
return view;
});

@ -0,0 +1,63 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit Mapping</h3>
{{else}}
<h3>Add Mapping</h3>
{{/if}}
</div>
<div class="modal-body remotepath-mapping-modal">
<div class="form-horizontal">
<div>
<p>Use this feature if you have a remotely running Download Client. NzbDrone will use the information provided to translate the paths provided by the Download Client API to something NzbDrone can access and import.</p>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Host</label>
<div class="col-sm-1 col-sm-push-3 help-inline">
<i class="icon-nd-form-info" title="Host you specified for the remote Download Client." />
</div>
<div class="col-sm-3 col-sm-pull-1">
<input type="text" name="host" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Remote Path</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-nd-form-info" title="Root path to the directory that the Download Client accesses." />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" name="remotePath" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Local Path</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-nd-form-info" title="Path that NzbDrone should use to access the same directory remotely." />
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" name="localPath" class="form-control x-path"/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn" data-dismiss="modal">cancel</button>
<div class="btn-group">
<button class="btn btn-primary x-save">save</button>
</div>
</div>
</div>

@ -0,0 +1,26 @@
'use strict';
define([
'AppLayout',
'marionette',
'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView'
], function (AppLayout, Marionette, EditView) {
return Marionette.ItemView.extend({
template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate',
className : 'row',
events: {
'click .x-edit' : '_editMapping'
},
initialize: function () {
this.listenTo(this.model, 'sync', this.render);
},
_editMapping: function() {
var view = new EditView({ model: this.model, targetCollection: this.model.collection});
AppLayout.modalRegion.show(view);
}
});
});

@ -0,0 +1,12 @@
 <span class="col-sm-2">
<div>{{host}}</div>
</span>
<span class="col-sm-5">
<div>{{remotePath}}</div>
</span>
<span class="col-sm-4">
<div>{{localPath}}</div>
</span>
<span class="col-sm-1">
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit Mapping"></i></div>
</span>

@ -0,0 +1,10 @@
'use strict';
define([
'jquery',
'backbone.deepmodel'
], function ($, DeepModel) {
return DeepModel.DeepModel.extend({
});
});

@ -30,4 +30,31 @@
li.add-thingy-item {
width: 33%;
}
}
.add-remotepath-mapping {
cursor: pointer;
font-size: 14px;
text-align: center;
display: inline-block;
padding: 2px 6px;
i {
cursor: pointer;
}
}
#remotepath-mapping-list {
.remotepath-header .row {
font-weight: bold;
line-height: 40px;
}
.rows .row {
line-height : 30px;
border-top : 1px solid #ddd;
vertical-align : middle;
padding : 5px;
}
}

@ -10,7 +10,7 @@ define(
return Marionette.CompositeView.extend({
template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate',
itemViewContainer: ".x-rows",
itemViewContainer: '.x-rows',
itemView: QualityDefinitionView
});

Loading…
Cancel
Save