From c90791b266386c3319184614efd44ed041f5edc2 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 18 Jan 2014 12:44:36 +0100 Subject: [PATCH 01/48] Quality Order can now be change on per Quality Profile. Quality Title used in Renaming can now be changed by the user. Both options require Advanced Settings to be enabled. --- .../MappingTests/ResourceMappingFixture.cs | 1 - .../EpisodeFiles/EpisodeFileResource.cs | 1 + src/NzbDrone.Api/History/HistoryResource.cs | 1 + src/NzbDrone.Api/Indexers/ReleaseResource.cs | 1 + src/NzbDrone.Api/NzbDrone.Api.csproj | 4 +- .../Qualities/QualityDefinitionModule.cs | 38 + .../Qualities/QualityDefinitionResource.cs | 18 + .../Qualities/QualityProfileModule.cs | 40 +- .../Qualities/QualityProfileResource.cs | 3 +- .../Qualities/QualityProfileSchemaModule.cs | 35 +- .../Qualities/QualitySizeModule.cs | 38 - .../Qualities/QualitySizeResource.cs | 13 - src/NzbDrone.Api/Queue/QueueResource.cs | 1 + .../Datastore/DatabaseRelationshipFixture.cs | 1 + .../AlterFixture.cs | 12 +- .../AcceptableSizeSpecificationFixture.cs | 38 +- .../CutoffSpecificationFixture.cs | 10 +- .../HistorySpecificationFixture.cs | 6 +- .../NotInQueueSpecificationFixture.cs | 4 +- .../QualityUpgradeSpecificationFixture.cs | 19 +- .../UpgradeDiskSpecificationFixture.cs | 2 +- .../DownloadApprovedFixture.cs | 4 + .../GetQualifiedReportsFixture.cs | 4 + .../Download/DownloadServiceFixture.cs | 1 - .../DownloadedEpisodesImportServiceFixture.cs | 14 + .../ImportDecisionMakerFixture.cs | 15 +- .../UpgradeSpecificationFixture.cs | 6 +- .../ImportApprovedEpisodesFixture.cs | 1 + .../NzbDrone.Core.Test.csproj | 6 +- .../OrganizerTests/GetNewFilenameFixture.cs | 5 + .../ParserTests/QualityParserFixture.cs | 5 +- ... => QualityDefinitionRepositoryFixture.cs} | 16 +- .../QualityDefinitionServiceFixture.cs | 79 + .../Qualities/QualityFixture.cs | 92 +- .../Qualities/QualityModelComparerFixture.cs | 117 + .../QualityProfileRepositoryFixture.cs | 1 - .../Qualities/QualitySizeServiceFixture.cs | 39 - .../TvTests/QualityModelFixture.cs | 125 - src/NzbDrone.Core/Blacklisting/Blacklist.cs | 1 + .../Converters/QualityIntConverter.cs | 4 +- .../Converters/QualityListConverter.cs | 52 + .../Converters/QualityModelConverter.cs | 55 + .../036_update_with_quality_converters.cs | 97 + .../037_add_configurable_qualities.cs | 63 + .../Migration/Framework/MigrationExtension.cs | 6 + src/NzbDrone.Core/Datastore/TableMapping.cs | 4 +- .../QualityUpgradableSpecification.cs | 15 +- .../AcceptableSizeSpecification.cs | 12 +- .../Specifications/NotInQueueSpecification.cs | 4 +- .../RssSync/HistorySpecification.cs | 2 +- .../UpgradeDiskSpecification.cs | 2 +- .../Download/DownloadApprovedReports.cs | 16 +- .../Download/DownloadFailedEvent.cs | 1 + src/NzbDrone.Core/History/History.cs | 1 + .../History/HistoryRepository.cs | 1 + src/NzbDrone.Core/History/HistoryService.cs | 1 + .../DownloadedEpisodesImportService.cs | 1 + src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 9 +- .../EpisodeImport/ImportDecisionMaker.cs | 3 +- .../Specifications/UpgradeSpecification.cs | 5 +- .../Notifications/NotificationService.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 13 +- .../Organizer/FileNameBuilder.cs | 22 +- .../Parser/Model/LocalEpisode.cs | 5 +- .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Qualities/Quality.cs | 183 +- .../Qualities/QualityDefinition.cs | 33 + .../Qualities/QualityDefinitionRepository.cs | 33 + .../Qualities/QualityDefinitionService.cs | 101 + src/NzbDrone.Core/Qualities/QualityModel.cs | 73 + .../Qualities/QualityModelComparer.cs | 67 + src/NzbDrone.Core/Qualities/QualitySize.cs | 18 - .../Qualities/QualitySizeRepository.cs | 33 - .../Qualities/QualitySizeService.cs | 64 - src/NzbDrone.Core/Queue/Queue.cs | 1 + src/NzbDrone.Core/Tv/QualityModel.cs | 120 - src/UI/JsLibraries/backbone.collectionview.js | 1072 +++++ src/UI/JsLibraries/jquery-ui.js | 4233 +++++++++++++++++ src/UI/Quality/QualityDefinitionCollection.js | 11 + ...SizeModel.js => QualityDefinitionModel.js} | 6 +- src/UI/Quality/QualitySizeCollection.js | 11 - .../QualityDefinitionCollectionTemplate.html | 16 + .../QualityDefinitionCollectionView.js | 17 + .../Definition/QualityDefinitionTemplate.html | 31 + .../Definition/QualityDefinitionView.js | 86 + .../Profile/EditQualityProfileItemView.js | 9 + .../EditQualityProfileItemViewTemplate.html | 5 + .../Profile/EditQualityProfileTemplate.html | 24 +- .../Quality/Profile/EditQualityProfileView.js | 108 +- src/UI/Settings/Quality/QualityLayout.js | 16 +- .../Quality/QualityLayoutTemplate.html | 2 +- .../Size/QualitySizeCollectionTemplate.html | 4 - .../Quality/Size/QualitySizeCollectionView.js | 9 - .../Quality/Size/QualitySizeTemplate.html | 20 - .../Settings/Quality/Size/QualitySizeView.js | 61 - src/UI/Settings/Quality/quality.less | 147 +- src/UI/Settings/SettingsLayout.js | 10 +- src/UI/app.js | 62 +- 99 files changed, 6904 insertions(+), 996 deletions(-) create mode 100644 src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs create mode 100644 src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs delete mode 100644 src/NzbDrone.Api/Qualities/QualitySizeModule.cs delete mode 100644 src/NzbDrone.Api/Qualities/QualitySizeResource.cs rename src/NzbDrone.Core.Test/Qualities/{QualitySizeRepositoryFixture.cs => QualityDefinitionRepositoryFixture.cs} (57%) create mode 100644 src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs delete mode 100644 src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinition.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityDefinitionService.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityModel.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityModelComparer.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySize.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySizeRepository.cs delete mode 100644 src/NzbDrone.Core/Qualities/QualitySizeService.cs delete mode 100644 src/NzbDrone.Core/Tv/QualityModel.cs create mode 100644 src/UI/JsLibraries/backbone.collectionview.js create mode 100644 src/UI/JsLibraries/jquery-ui.js create mode 100644 src/UI/Quality/QualityDefinitionCollection.js rename src/UI/Quality/{QualitySizeModel.js => QualityDefinitionModel.js} (71%) delete mode 100644 src/UI/Quality/QualitySizeCollection.js create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html create mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionView.js create mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js create mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeCollectionView.js delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeTemplate.html delete mode 100644 src/UI/Settings/Quality/Size/QualitySizeView.js diff --git a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs index e03e02c35..d31edcac6 100644 --- a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs +++ b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs @@ -107,7 +107,6 @@ namespace NzbDrone.Api.Test.MappingTests [Test] public void should_map_qualityprofile() { - var profileResource = new QualityProfileResource { Allowed = Builder.CreateListOfSize(1).Build().ToList(), diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index f3bd37368..311253f3f 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.EpisodeFiles diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index fe572a25c..e95330c52 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Api.Episodes; using NzbDrone.Api.REST; using NzbDrone.Api.Series; using NzbDrone.Core.History; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.History diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index fda9ef67a..03955b5f8 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.Indexers diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 00f5f96e3..f6425304a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -166,8 +166,8 @@ - - + + diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs new file mode 100644 index 000000000..d675d54bc --- /dev/null +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using NzbDrone.Core.Qualities; +using NzbDrone.Api.Mapping; + +namespace NzbDrone.Api.Qualities +{ + public class QualityDefinitionModule : NzbDroneRestModule + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceAll = GetAll; + + GetResourceById = GetById; + + UpdateResource = Update; + } + + private void Update(QualityDefinitionResource resource) + { + var model = resource.InjectTo(); + _qualityDefinitionService.Update(model); + } + + private QualityDefinitionResource GetById(int id) + { + return _qualityDefinitionService.Get((Quality)id).InjectTo(); + } + + private List GetAll() + { + return ToListResource(_qualityDefinitionService.All); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs new file mode 100644 index 000000000..8750a6c33 --- /dev/null +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.Qualities +{ + public class QualityDefinitionResource : RestResource + { + public Quality Quality { get; set; } + + public String Title { get; set; } + + public Int32 Weight { get; set; } + + public Int32 MinSize { get; set; } + public Int32 MaxSize { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs index a08fdb12c..250bddd08 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs @@ -9,11 +9,14 @@ namespace NzbDrone.Api.Qualities public class QualityProfileModule : NzbDroneRestModule { private readonly IQualityProfileService _qualityProfileService; + private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityProfileModule(IQualityProfileService qualityProfileService) + public QualityProfileModule(IQualityProfileService qualityProfileService, + IQualityDefinitionService qualityDefinitionService) : base("/qualityprofiles") { _qualityProfileService = qualityProfileService; + _qualityDefinitionService = qualityDefinitionService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).NotNull(); @@ -44,38 +47,47 @@ namespace NzbDrone.Api.Qualities private void Update(QualityProfileResource resource) { - var model = resource.InjectTo(); + var model = _qualityProfileService.Get(resource.Id); + model.Name = resource.Name; + model.Cutoff = (Quality)resource.Cutoff.Id; + model.Allowed = resource.Allowed.Select(p => (Quality)p.Id).ToList(); _qualityProfileService.Update(model); } private QualityProfileResource GetById(int id) { - return QualityToResource(_qualityProfileService.Get(id)); + return MapToResource(_qualityProfileService.Get(id)); } private List GetAll() { - var allProfiles = _qualityProfileService.All(); - - - var profiles = allProfiles.Select(QualityToResource).ToList(); + var profiles = _qualityProfileService.All().Select(MapToResource).ToList(); return profiles; } - private static QualityProfileResource QualityToResource(QualityProfile profile) + private QualityProfileResource MapToResource(QualityProfile profile) { return new QualityProfileResource { - Cutoff = profile.Cutoff.InjectTo(), - Available = Quality.All() - .Where(c => !profile.Allowed.Any(q => c.Id == q.Id)) - .InjectTo>(), - - Allowed = profile.Allowed.InjectTo>(), + Cutoff = MapToResource(_qualityDefinitionService.Get(profile.Cutoff)), + Available = _qualityDefinitionService.All() + .Where(c => !profile.Allowed.Any(q => c.Quality == q)) + .Select(MapToResource).ToList(), + Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(MapToResource).ToList(), Name = profile.Name, Id = profile.Id }; } + + private QualityResource MapToResource(QualityDefinition config) + { + return new QualityResource + { + Id = config.Quality.Id, + Name = config.Quality.Name, + Weight = config.Weight + }; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs index 25e6a9108..73ba94268 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs @@ -14,7 +14,8 @@ namespace NzbDrone.Api.Qualities public class QualityResource : RestResource { - public Int32 Weight { get; set; } public String Name { get; set; } + + public Int32 Weight { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs index 06d941a2a..e4e23ca02 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs @@ -7,9 +7,13 @@ namespace NzbDrone.Api.Qualities { public class QualityProfileSchemaModule : NzbDroneRestModule { - public QualityProfileSchemaModule() + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) : base("/qualityprofiles/schema") { + _qualityDefinitionService = qualityDefinitionService; + GetResourceAll = GetAll; } @@ -19,21 +23,30 @@ namespace NzbDrone.Api.Qualities profile.Cutoff = Quality.Unknown; profile.Allowed = new List(); - return new List{ QualityToResource(profile)}; + return new List { QualityToResource(profile) }; } - private static QualityProfileResource QualityToResource(QualityProfile profile) + private QualityProfileResource QualityToResource(QualityProfile profile) { return new QualityProfileResource - { - Available = Quality.All() - .Where(c => !profile.Allowed.Any(q => c.Id == q.Id)) - .InjectTo>(), + { + Cutoff = QualityToResource(_qualityDefinitionService.Get(profile.Cutoff)), + Available = _qualityDefinitionService.All().Select(QualityToResource).ToList(), + Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(QualityToResource).ToList(), + Name = profile.Name, + Id = profile.Id + }; + } - Allowed = profile.Allowed.InjectTo>(), - Name = profile.Name, - Id = profile.Id - }; + + private QualityResource QualityToResource(QualityDefinition config) + { + return new QualityResource + { + Id = config.Quality.Id, + Name = config.Quality.Name, + Weight = config.Weight + }; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualitySizeModule.cs b/src/NzbDrone.Api/Qualities/QualitySizeModule.cs deleted file mode 100644 index 206bc9c51..000000000 --- a/src/NzbDrone.Api/Qualities/QualitySizeModule.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Qualities; -using NzbDrone.Api.Mapping; - -namespace NzbDrone.Api.Qualities -{ - public class QualitySizeModule : NzbDroneRestModule - { - private readonly IQualitySizeService _qualityTypeProvider; - - public QualitySizeModule(IQualitySizeService qualityTypeProvider) - { - _qualityTypeProvider = qualityTypeProvider; - - GetResourceAll = GetAll; - - GetResourceById = GetById; - - UpdateResource = Update; - } - - private void Update(QualitySizeResource resource) - { - var model = resource.InjectTo(); - _qualityTypeProvider.Update(model); - } - - private QualitySizeResource GetById(int id) - { - return _qualityTypeProvider.Get(id).InjectTo(); - } - - private List GetAll() - { - return ToListResource(_qualityTypeProvider.All); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualitySizeResource.cs b/src/NzbDrone.Api/Qualities/QualitySizeResource.cs deleted file mode 100644 index 2d428f575..000000000 --- a/src/NzbDrone.Api/Qualities/QualitySizeResource.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using NzbDrone.Api.REST; - -namespace NzbDrone.Api.Qualities -{ - public class QualitySizeResource : RestResource - { - public Int32 QualityId { get; set; } - public String Name { get; set; } - public Int32 MinSize { get; set; } - public Int32 MaxSize { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 94a7f07fc..0adfe1e79 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Api.Queue diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 75707c4e5..e852662e4 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Test.Datastore public void one_to_one() { var episodeFile = Builder.CreateNew() + .With(c => c.Quality = new QualityModel()) .BuildNew(); Db.Insert(episodeFile); diff --git a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs index aa07e1028..d93d0f262 100644 --- a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs @@ -81,25 +81,25 @@ namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests [Test] public void should_read_existing_indexes() { - var indexes = _subject.GetIndexes("QualitySizes"); + var indexes = _subject.GetIndexes("QualityDefinitions"); indexes.Should().NotBeEmpty(); indexes.Should().OnlyContain(c => c != null); indexes.Should().OnlyContain(c => !string.IsNullOrWhiteSpace(c.Column)); - indexes.Should().OnlyContain(c => c.Table == "QualitySizes"); + indexes.Should().OnlyContain(c => c.Table == "QualityDefinitions"); indexes.Should().OnlyContain(c => c.Unique); } [Test] public void should_add_indexes_when_creating_new_table() { - var columns = _subject.GetColumns("QualitySizes"); - var indexes = _subject.GetIndexes("QualitySizes"); + var columns = _subject.GetColumns("QualityDefinitions"); + var indexes = _subject.GetIndexes("QualityDefinitions"); - _subject.CreateTable("QualityB", columns.Values, indexes); + _subject.CreateTable("QualityDefinitionsB", columns.Values, indexes); - var newIndexes = _subject.GetIndexes("QualityB"); + var newIndexes = _subject.GetIndexes("QualityDefinitionsB"); newIndexes.Should().HaveSameCount(indexes); newIndexes.Select(c=>c.Column).Should().BeEquivalentTo(indexes.Select(c=>c.Column)); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index 5f511e3dc..b586622ea 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteEpisode parseResultSingle; private Series series30minutes; private Series series60minutes; - private QualitySize qualityType; + private QualityDefinition qualityType; [SetUp] public void Setup() @@ -47,10 +47,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(c => c.Runtime = 60) .Build(); - qualityType = Builder.CreateNew() + qualityType = Builder.CreateNew() .With(q => q.MinSize = 0) .With(q => q.MaxSize = 10) - .With(q => q.QualityId = 1) + .With(q => q.Quality = Quality.SDTV) .Build(); } @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series30minutes; parseResultMulti.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series60minutes; parseResultMulti.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -173,7 +173,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series30minutes; parseResultMulti.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -192,7 +192,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultMulti.Series = series60minutes; parseResultMulti.Release.Size = 10.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -211,7 +211,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 184572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -230,7 +230,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 368572800; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series30minutes; parseResultSingle.Release.Size = 1.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -270,7 +270,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Series = series60minutes; parseResultSingle.Release.Size = 10.Gigabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -292,7 +292,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Release.Size = 18457280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -314,7 +314,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests parseResultSingle.Release.Size = 36857280000; qualityType.MaxSize = 0; - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -338,7 +338,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests qualityType.MaxSize = (int)600.Megabytes(); - Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(qualityType); Mocker.GetMock().Setup( s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny())) @@ -374,7 +374,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(parseResult, null).Should().BeFalse(); - Mocker.GetMock().Verify(c=>c.Get(It.IsAny()),Times.Never()); + Mocker.GetMock().Verify(c => c.Get(It.IsAny()), Times.Never()); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index 7ff36a03b..be1f420da 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -13,35 +13,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.DVD, true)).Should().BeTrue(); } [Test] public void should_return_false_if_current_episode_is_equal_to_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true)).Should().BeFalse(); } [Test] public void should_return_false_if_current_episode_is_greater_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } [Test] public void should_return_true_when_new_episode_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, false), new QualityModel(Quality.HDTV720p, true)).Should().BeTrue(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true), new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 0fe7b7ba0..36d7db629 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; _fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode @@ -62,8 +62,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _upgradableQuality = new QualityModel(Quality.SDTV, false); _notupgradableQuality = new QualityModel(Quality.HDTV1080p, true); - - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(3)).Returns(null); @@ -132,7 +130,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p }; + _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, false); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, false); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index 1ca3e9c41..59361c70f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -26,7 +26,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - _series = Builder.CreateNew().Build(); + _series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); _episode = Builder.CreateNew() .With(e => e.SeriesId = _series.Id) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index cc631b871..d3385c23b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Linq; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; using NzbDrone.Core.Qualities; @@ -23,6 +24,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new object[] { Quality.SDTV, false, Quality.SDTV, true, Quality.SDTV, true }, new object[] { Quality.WEBDL1080p, false, Quality.WEBDL1080p, false, Quality.WEBDL1080p, false } }; + + [SetUp] + public void Setup() + { + + } private void GivenAutoDownloadPropers(bool autoDownloadPropers) { @@ -36,7 +43,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - Subject.IsUpgradable(new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) + var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + + Subject.IsUpgradable(qualityProfile, new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) .Should().Be(expected); } @@ -45,8 +54,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - Subject.IsUpgradable(new QualityModel(Quality.DVD, true), - new QualityModel(Quality.DVD, false)).Should().BeFalse(); + var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + + Subject.IsUpgradable(qualityProfile, new QualityModel(Quality.DVD, true), new QualityModel(Quality.DVD, false)) + .Should().BeFalse(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index eb85c757d..9135f0a75 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index f13c81ccb..65ad262a1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -37,6 +37,10 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release = new ReleaseInfo(); remoteEpisode.Release.PublishDate = DateTime.UtcNow; + remoteEpisode.Series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + return remoteEpisode; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs index 370142d75..0036a8bcb 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs @@ -37,6 +37,10 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-Age); remoteEpisode.Release.Size = size; + remoteEpisode.Series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + return remoteEpisode; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index cb3a0313c..f1a64dd7b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -87,7 +87,6 @@ namespace NzbDrone.Core.Test.Download { Mocker.GetMock().Setup(c => c.IsConfigured).Returns(false); - Subject.DownloadReport(_parseResult); Mocker.GetMock().Verify(c => c.DownloadNzb(It.IsAny()), Times.Never()); diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 90208c4f5..8f33db4f0 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -80,6 +80,20 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(true); Subject.Execute(new DownloadedEpisodesScanCommand()); + + VerifyNoImport(); + } + + [Test] + public void should_skip_if_no_series_found() + { + Mocker.GetMock().Setup(c => c.GetSeries("foldername")).Returns((Series)null); + + Subject.Execute(new DownloadedEpisodesScanCommand()); + + Mocker.GetMock() + .Verify(c => c.GetImportDecisions(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); VerifyNoImport(); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index edcfa1a14..0e2bc1053 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using FizzWare.NBuilder; namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport { @@ -63,7 +64,10 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _fail3.Setup(c => c.RejectionReason).Returns("_fail3"); _videoFiles = new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; - _series = new Series(); + _series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .Build(); + _quality = new QualityModel(Quality.DVD); _localEpisode = new LocalEpisode { @@ -80,7 +84,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Mocker.GetMock() .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) .Returns(_videoFiles); - } private void GivenSpecifications(params Mock[] mocks) @@ -162,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny())) .Returns(_videoFiles); - Subject.GetImportDecisions(_videoFiles, new Series(), false); + Subject.GetImportDecisions(_videoFiles, _series, false); Mocker.GetMock() .Verify(c => c.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(_videoFiles.Count)); @@ -176,7 +179,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, null); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, null); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -187,7 +190,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = QualityParser.ParseQuality(_videoFiles.Single()); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, new QualityModel(Quality.SDTV)); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, new QualityModel(Quality.SDTV)); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } @@ -198,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport GivenSpecifications(_pass1, _pass2, _pass3); var expectedQuality = new QualityModel(Quality.Bluray1080p); - var result = Subject.GetImportDecisions(_videoFiles, new Series(), false, expectedQuality); + var result = Subject.GetImportDecisions(_videoFiles, _series, false, expectedQuality); result.Single().LocalEpisode.Quality.Should().Be(expectedQuality); } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index a0d0f7aef..f189f049b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -22,13 +22,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications public void Setup() { _series = Builder.CreateNew() - .With(s => s.SeriesType = SeriesTypes.Standard) + .With(s => s.SeriesType = SeriesTypes.Standard) + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _localEpisode = new LocalEpisode { Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", - Quality = new QualityModel(Quality.HDTV720p, false) + Quality = new QualityModel(Quality.HDTV720p, false), + Series = _series }; } diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 7946b53aa..55789f9b8 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions = new List(); var series = Builder.CreateNew() + .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); var episodes = Builder.CreateListOfSize(5) diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 326789038..3e406aef7 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -182,7 +182,7 @@ - + @@ -217,14 +217,14 @@ - + - + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index b67e571ec..338e3059c 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -50,6 +51,10 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "DRONE" }; + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); } private void GivenProper() diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index b716883cf..a3a7c4a58 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; @@ -150,9 +151,9 @@ namespace NzbDrone.Core.Test.ParserTests } [Test, TestCaseSource("SelfQualityParserCases")] - public void parsing_our_own_quality_enum(Quality quality) + public void parsing_our_own_quality_enum_name(Quality quality) { - var fileName = String.Format("My series S01E01 [{0}]", quality); + var fileName = String.Format("My series S01E01 [{0}]", quality.Name); var result = Parser.QualityParser.ParseQuality(fileName); result.Quality.Should().Be(quality); } diff --git a/src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs similarity index 57% rename from src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs rename to src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs index 02454aa66..6156d9ab0 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualitySizeRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionRepositoryFixture.cs @@ -4,25 +4,27 @@ using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using FluentAssertions; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Qualities { [TestFixture] - - public class QualitySizeRepositoryFixture : DbTest + public class QualityDefinitionRepositoryFixture : DbTest { [SetUp] public void Setup() { - Mocker.SetConstant(Subject); - Mocker.Resolve().Handle(new ApplicationStartedEvent()); + foreach (var qualityDefault in Quality.DefaultQualityDefinitions) + { + qualityDefault.Id = 0; + Storage.Insert(qualityDefault); + } } - [Test] - public void should_get_quality_size_by_id() + public void should_get_qualitydefinition_by_id() { - var size = Subject.GetByQualityId(Quality.Bluray1080p.Id); + var size = Subject.GetByQualityId((int)Quality.Bluray1080p); size.Should().NotBeNull(); } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs new file mode 100644 index 000000000..90f25b5f7 --- /dev/null +++ b/src/NzbDrone.Core.Test/Qualities/QualityDefinitionServiceFixture.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityDefinitionServiceFixture : CoreTest + { + [Test] + public void init_should_add_all_definitions() + { + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All.Count)); + } + + [Test] + public void init_should_insert_any_missing_definitions() + { + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(new List + { + new QualityDefinition(Quality.SDTV) { Weight = 1, MinSize = 0, MaxSize = 100, Id = 20 } + }); + + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All.Count - 1)); + } + + [Test] + public void init_should_insert_missing_definitions_preserving_weight() + { + // User moved HDTV1080p to a higher weight. + var currentQualities = new List + { + new QualityDefinition(Quality.SDTV) { Id = 5, Title = "SDTV", Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { Id = 2, Title = "720p WEB-DL", Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { Id = 4, Title = "1080p HDTV", Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { Id = 8, Title = "1080p WEB-DL", Weight = 4, MinSize=0, MaxSize=100 }, + }; + + // Expected to insert Bluray720p above HDTV1080p. + // Expected to insert Bluray1080p above WEBDL1080p. + var addBluray1080p = new List + { + new QualityDefinition(Quality.SDTV) { Title = "SDTV", Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { Title = "1080p HDTV", Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { Title = "720p WEB-DL", Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray720p) { Title = "720p BluRay", Weight = 4, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { Title = "1080p WEB-DL", Weight = 5, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray1080p) { Title = "1080p BluRay", Weight = 6, MinSize=0, MaxSize=100 } + }; + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(currentQualities); + + Subject.InsertMissingDefinitions(addBluray1080p); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(p => p.Quality == Quality.Bluray720p && p.Weight == 4)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.Update(It.Is(p => p.Quality == Quality.WEBDL1080p && p.Weight == 5)), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(p => p.Quality == Quality.Bluray1080p && p.Weight == 6)), Times.Once()); + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index a3fabfec0..bffb0f5be 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -42,81 +44,21 @@ namespace NzbDrone.Core.Test.Qualities i.Should().Be(expected); } - - [Test] - public void Icomparer_greater_test() + public static List GetDefaultQualities() { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_lesser() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - first.Should().BeLessThan(second); - } - - [Test] - public void equal_operand() - { - var first = Quality.Bluray1080p; - var second = Quality.Bluray1080p; - - (first == second).Should().BeTrue(); - (first >= second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void equal_operand_false() - { - var first = Quality.Bluray1080p; - var second = Quality.Unknown; - - (first == second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand() - { - var first = Quality.Bluray1080p; - var second = Quality.Bluray1080p; - - (first != second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand_false() - { - var first = Quality.Bluray1080p; - var second = Quality.Unknown; - - (first != second).Should().BeTrue(); - } - - [Test] - public void greater_operand() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - (first < second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void lesser_operand() - { - var first = Quality.DVD; - var second = Quality.Bluray1080p; - - (second > first).Should().BeTrue(); - (second >= first).Should().BeTrue(); + return new List + { + Quality.SDTV, + Quality.WEBDL480p, + Quality.DVD, + Quality.HDTV720p, + Quality.HDTV1080p, + Quality.RAWHD, + Quality.WEBDL720p, + Quality.Bluray720p, + Quality.WEBDL1080p, + Quality.Bluray1080p + }; } } } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs new file mode 100644 index 000000000..416962658 --- /dev/null +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -0,0 +1,117 @@ +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Qualities +{ + [TestFixture] + public class QualityModelComparerFixture : CoreTest + { + public QualityModelComparer Subject { get; set; } + + private void GivenDefaultQualityProfile() + { + Subject = new QualityModelComparer(new QualityProfile { Allowed = QualityFixture.GetDefaultQualities() }); + } + + private void GivenCustomQualityProfile() + { + Subject = new QualityModelComparer(new QualityProfile { Allowed = new List { Quality.Bluray720p, Quality.DVD } }); + } + + [Test] + public void Icomparer_greater_test() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(second, first); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_greater_proper() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.Bluray1080p, false); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(second, first); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_lesser() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeLessThan(0); + } + + [Test] + public void Icomparer_lesser_proper() + { + GivenDefaultQualityProfile(); + + var first = new QualityModel(Quality.DVD, false); + var second = new QualityModel(Quality.DVD, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeLessThan(0); + } + + [Test] + public void Icomparer_greater_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.DVD, true); + var second = new QualityModel(Quality.Bluray720p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_missing_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.Bluray720p, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().BeGreaterThan(0); + } + + [Test] + public void Icomparer_missing_both_custom_order() + { + GivenCustomQualityProfile(); + + var first = new QualityModel(Quality.SDTV, true); + var second = new QualityModel(Quality.Bluray1080p, true); + + var compare = Subject.Compare(first, second); + + compare.Should().Be(0); + } + } +} diff --git a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs index f82879953..f13c59f3d 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs @@ -7,7 +7,6 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Qualities { [TestFixture] - public class QualityProfileRepositoryFixture : DbTest { [Test] diff --git a/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs deleted file mode 100644 index 9c0b0b626..000000000 --- a/src/NzbDrone.Core.Test/Qualities/QualitySizeServiceFixture.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Qualities -{ - [TestFixture] - - public class QualitySizeServiceFixture : CoreTest - { - [Test] - public void Init_should_add_all_sizes() - { - Subject.Handle(new ApplicationStartedEvent()); - - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All().Count)); - } - - [Test] - public void Init_should_insert_any_missing_sizes() - { - Mocker.GetMock() - .Setup(s => s.All()) - .Returns(new List - { - new QualitySize { QualityId = 1, Name = "SDTV", MinSize = 0, MaxSize = 100 } - }); - - Subject.Handle(new ApplicationStartedEvent()); - - Mocker.GetMock() - .Verify(v => v.Insert(It.IsAny()), Times.Exactly(Quality.All().Count - 1)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs b/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs deleted file mode 100644 index 322ef2e79..000000000 --- a/src/NzbDrone.Core.Test/TvTests/QualityModelFixture.cs +++ /dev/null @@ -1,125 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.TvTests -{ - [TestFixture] - - public class QualityModelFixture : CoreTest - { - [Test] - public void Icomparer_greater_test() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_greater_proper() - { - var first = new QualityModel(Quality.Bluray1080p, false); - var second = new QualityModel(Quality.Bluray1080p, true); - - second.Should().BeGreaterThan(first); - } - - [Test] - public void Icomparer_lesser() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - first.Should().BeLessThan(second); - } - - [Test] - public void Icomparer_lesser_proper() - { - var first = new QualityModel(Quality.DVD, false); - var second = new QualityModel(Quality.DVD, true); - - first.Should().BeLessThan(second); - } - - [Test] - public void equal_operand() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first == second).Should().BeTrue(); - (first >= second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void equal_operand_false() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Unknown, true); - - (first == second).Should().BeFalse(); - } - - [Test] - public void equal_operand_false_proper() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, false); - - (first == second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first != second).Should().BeFalse(); - } - - [Test] - public void not_equal_operand_false() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Unknown, true); - - (first != second).Should().BeTrue(); - } - - [Test] - public void not_equal_operand_false_proper() - { - var first = new QualityModel(Quality.Bluray1080p, true); - var second = new QualityModel(Quality.Bluray1080p, false); - - (first != second).Should().BeTrue(); - } - - [Test] - public void greater_operand() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (first < second).Should().BeTrue(); - (first <= second).Should().BeTrue(); - } - - [Test] - public void lesser_operand() - { - var first = new QualityModel(Quality.DVD, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - (second > first).Should().BeTrue(); - (second >= first).Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 94cc5ffed..c68f55ff3 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Blacklisting diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs index 6dc9d6c24..ea5dfd59f 100644 --- a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs @@ -2,6 +2,8 @@ using Marr.Data.Converters; using Marr.Data.Mapping; using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Datastore.Converters { @@ -26,7 +28,7 @@ namespace NzbDrone.Core.Datastore.Converters public object ToDB(object clrValue) { - if(clrValue == null) return 0; + if(clrValue == DBNull.Value) return 0; if(clrValue as Quality == null) { diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs new file mode 100644 index 000000000..c0c58d8f5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs @@ -0,0 +1,52 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class QualityListConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return DBNull.Value; + } + + var val = Convert.ToString(context.DbValue); + + var qualityList = Json.Deserialize>(val).ConvertAll(Quality.FindById); + + return qualityList; + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) return null; + + var qualityList = clrValue as List; + + if (qualityList == null) + { + throw new InvalidOperationException("Can only store a list of qualities in this database column."); + } + + var intList = qualityList.ConvertAll(v => v.Id); + + return intList.ToJson(); + } + + public Type DbType + { + get { return typeof(string); } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs new file mode 100644 index 000000000..8e2917dcc --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs @@ -0,0 +1,55 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class QualityModelConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return new QualityModel(); + } + + var val = Convert.ToString(context.DbValue); + + var jsonObject = Json.Deserialize>(val); + + return new QualityModel((Quality)Convert.ToInt32(jsonObject["id"]), Convert.ToBoolean(jsonObject["proper"])); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) + clrValue = new QualityModel(); + + var qualityModel = clrValue as QualityModel; + + if (qualityModel == null) + { + throw new InvalidOperationException("Can only store a QualityModel in this database column."); + } + + var jsonObject = new Dictionary(); + jsonObject["id"] = (int)qualityModel.Quality; + jsonObject["proper"] = qualityModel.Proper; + + return jsonObject.ToJson(); + } + + public Type DbType + { + get { return typeof(string); } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs new file mode 100644 index 000000000..c29490f66 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -0,0 +1,97 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; +using System.Linq; +using System; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(36)] + public class update_with_quality_converters : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertQualityProfiles); + + Execute.WithConnection(ConvertQualityModels); + } + + private void ConvertQualityProfiles(IDbConnection conn, IDbTransaction tran) + { + var qualityListConverter = new NzbDrone.Core.Datastore.Converters.QualityListConverter(); + + // Convert 'Allowed' column in QualityProfiles from Json List to Json List (int = Quality) + using (IDbCommand qualityProfileCmd = conn.CreateCommand()) + { + qualityProfileCmd.Transaction = tran; + qualityProfileCmd.CommandText = @"SELECT Id, Allowed FROM QualityProfiles"; + using (IDataReader qualityProfileReader = qualityProfileCmd.ExecuteReader()) + { + while (qualityProfileReader.Read()) + { + var id = qualityProfileReader.GetInt32(0); + var allowedJson = qualityProfileReader.GetString(1); + + var allowed = Json.Deserialize>(allowedJson); + + var allowedNewJson = qualityListConverter.ToDB(allowed); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE QualityProfiles SET Allowed = ? WHERE Id = ?"; + updateCmd.AddParameter(allowedNewJson); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + + private void ConvertQualityModels(IDbConnection conn, IDbTransaction tran) + { + // Converts the QualityModel JSON objects to their new format (only storing the QualityId instead of the entire object) + ConvertQualityModel(conn, tran, "Blacklist"); + ConvertQualityModel(conn, tran, "EpisodeFiles"); + ConvertQualityModel(conn, tran, "History"); + } + + private void ConvertQualityModel(IDbConnection conn, IDbTransaction tran, string tableName) + { + var qualityModelConverter = new NzbDrone.Core.Datastore.Converters.QualityModelConverter(); + + using (IDbCommand qualityModelCmd = conn.CreateCommand()) + { + qualityModelCmd.Transaction = tran; + qualityModelCmd.CommandText = @"SELECT Id, Quality FROM " + tableName; + using (IDataReader qualityModelReader = qualityModelCmd.ExecuteReader()) + { + while (qualityModelReader.Read()) + { + var id = qualityModelReader.GetInt32(0); + var qualityJson = qualityModelReader.GetString(1); + + var quality = Json.Deserialize(qualityJson); + + var qualityNewJson = qualityModelConverter.ToDB(quality); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Id = ?"; + updateCmd.AddParameter(qualityNewJson); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs new file mode 100644 index 000000000..1020dca2c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs @@ -0,0 +1,63 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; +using System.Linq; +using System; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(37)] + public class add_configurable_qualities : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("QualityDefinitions") + .WithColumn("Quality").AsInt32().Unique() + .WithColumn("Title").AsString().Unique() + .WithColumn("Weight").AsInt32().Unique() + .WithColumn("MinSize").AsInt32() + .WithColumn("MaxSize").AsInt32(); + + Execute.WithConnection(ConvertQualities); + + Delete.Table("QualitySizes"); + } + + private void ConvertQualities(IDbConnection conn, IDbTransaction tran) + { + // Convert QualitySizes to a more generic QualityDefinitions table. + using (IDbCommand qualitySizeCmd = conn.CreateCommand()) + { + qualitySizeCmd.Transaction = tran; + qualitySizeCmd.CommandText = @"SELECT QualityId, MinSize, MaxSize FROM QualitySizes"; + using (IDataReader qualitySizeReader = qualitySizeCmd.ExecuteReader()) + { + while (qualitySizeReader.Read()) + { + var qualityId = qualitySizeReader.GetInt32(0); + var minSize = qualitySizeReader.GetInt32(1); + var maxSize = qualitySizeReader.GetInt32(2); + + var defaultConfig = Quality.DefaultQualityDefinitions.Single(p => (int)p.Quality == qualityId); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "INSERT INTO QualityDefinitions (Quality, Title, Weight, MinSize, MaxSize) VALUES (?, ?, ?, ?, ?)"; + updateCmd.AddParameter(qualityId); + updateCmd.AddParameter(defaultConfig.Title); + updateCmd.AddParameter(defaultConfig.Weight); + updateCmd.AddParameter(minSize); + updateCmd.AddParameter(maxSize); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs index 565630165..807e73e2a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationExtension.cs @@ -10,5 +10,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity(); } + public static void AddParameter(this System.Data.IDbCommand command, object value) + { + var parameter = command.CreateParameter(); + parameter.Value = value; + command.Parameters.Add(parameter); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f7ce5cd43..f164e925d 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("QualityProfiles"); - Mapper.Entity().RegisterModel("QualitySizes"); + Mapper.Entity().RegisterModel("QualityDefinitions"); Mapper.Entity().RegisterModel("Logs"); @@ -81,6 +81,8 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(Boolean), new BooleanIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new QualityListConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new QualityModelConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); } diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs index 8bec31bc1..3330be7ab 100644 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.DecisionEngine { public interface IQualityUpgradableSpecification { - bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null); + bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null); bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality); } @@ -20,11 +20,12 @@ namespace NzbDrone.Core.DecisionEngine _logger = logger; } - public bool IsUpgradable(QualityModel currentQuality, QualityModel newQuality = null) + public bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) { if (newQuality != null) { - if (currentQuality >= newQuality) + int compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); + if (compare <= 0) { _logger.Trace("existing item has better or equal quality. skipping"); return false; @@ -41,7 +42,9 @@ namespace NzbDrone.Core.DecisionEngine public bool CutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null) { - if (currentQuality.Quality >= profile.Cutoff) + int compare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); + + if (compare >= 0) { if (newQuality != null && IsProperUpgrade(currentQuality, newQuality)) { @@ -57,7 +60,9 @@ namespace NzbDrone.Core.DecisionEngine public bool IsProperUpgrade(QualityModel currentQuality, QualityModel newQuality) { - if (currentQuality.Quality == newQuality.Quality && newQuality > currentQuality) + int compare = newQuality.Proper.CompareTo(currentQuality.Proper); + + if (currentQuality.Quality == newQuality.Quality && compare > 0) { _logger.Trace("New quality is a proper for existing quality"); return true; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 1d6f5af7f..e63edaa8c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -9,13 +9,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class AcceptableSizeSpecification : IDecisionEngineSpecification { - private readonly IQualitySizeService _qualityTypeProvider; + private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public AcceptableSizeSpecification(IQualitySizeService qualityTypeProvider, IEpisodeService episodeService, Logger logger) + public AcceptableSizeSpecification(IQualityDefinitionService qualityDefinitionService, IEpisodeService episodeService, Logger logger) { - _qualityTypeProvider = qualityTypeProvider; + _qualityDefinitionService = qualityDefinitionService; _episodeService = episodeService; _logger = logger; } @@ -44,15 +44,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return false; } - var qualityType = _qualityTypeProvider.Get(quality.Id); + var qualityDefinition = _qualityDefinitionService.Get(quality); - if (qualityType.MaxSize == 0) + if (qualityDefinition.MaxSize == 0) { _logger.Trace("Max size is 0 (unlimited) - skipping check."); return true; } - var maxSize = qualityType.MaxSize.Megabytes(); + var maxSize = qualityDefinition.MaxSize.Megabytes(); //Multiply maxSize by Series.Runtime maxSize = maxSize * subject.Series.Runtime * subject.Episodes.Count; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index c0ccf0ecf..3827f26a7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -4,6 +4,8 @@ using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -44,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable queue) { var matchingSeries = queue.Where(q => q.Series.Id == newEpisode.Series.Id); - var matchingSeriesAndQuality = matchingSeries.Where(q => q.ParsedEpisodeInfo.Quality >= newEpisode.ParsedEpisodeInfo.Quality); + var matchingSeriesAndQuality = matchingSeries.Where(q => new QualityModelComparer(q.Series.QualityProfile).Compare(q.ParsedEpisodeInfo.Quality, newEpisode.ParsedEpisodeInfo.Quality) >= 0); return matchingSeriesAndQuality.Any(q => q.Episodes.Select(e => e.Id).Intersect(newEpisode.Episodes.Select(e => e.Id)).Any()); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index e6d6ef366..a4c3b3745 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -63,7 +63,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (bestQualityInHistory != null) { _logger.Trace("Comparing history quality with report. History is {0}", bestQualityInHistory); - if (!_qualityUpgradableSpecification.IsUpgradable(bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, bestQualityInHistory, subject.ParsedEpisodeInfo.Quality)) return false; } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 004f20134..8d2c5f631 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Trace("Comparing file quality with report. Existing file is {0}", file.Quality); - if (!_qualityUpgradableSpecification.IsUpgradable(file.Quality, subject.ParsedEpisodeInfo.Quality)) + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { return false; } diff --git a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs b/src/NzbDrone.Core/Download/DownloadApprovedReports.cs index 46f54d1aa..1added8cc 100644 --- a/src/NzbDrone.Core/Download/DownloadApprovedReports.cs +++ b/src/NzbDrone.Core/Download/DownloadApprovedReports.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download { @@ -16,7 +18,7 @@ namespace NzbDrone.Core.Download private readonly IDownloadService _downloadService; private readonly Logger _logger; - public DownloadApprovedReports(IDownloadService downloadService, Logger logger) + public DownloadApprovedReports(IDownloadService downloadService, Logger logger) { _downloadService = downloadService; _logger = logger; @@ -57,11 +59,13 @@ namespace NzbDrone.Core.Download public List GetQualifiedReports(IEnumerable decisions) { return decisions.Where(c => c.Approved && c.RemoteEpisode.Episodes.Any()) - .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality) - .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) - .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / c.RemoteEpisode.Episodes.Count) - .ThenBy(c => c.RemoteEpisode.Release.Age) - .ToList(); + .GroupBy(c => c.RemoteEpisode.Series.Id, (i,s) => s + .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.QualityProfile)) + .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault()) + .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / c.RemoteEpisode.Episodes.Count) + .ThenBy(c => c.RemoteEpisode.Release.Age)) + .SelectMany(c => c) + .ToList(); } } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 188ab24f9..6950081c2 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Common.Messaging; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 94b345e59..4208039e4 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 59d54c745..f860afc70 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 4c8758618..07e32b824 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.History diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 174693c7c..cf82620de 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 7fac2d1ae..12dccb7b0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index c9fe15e58..a18b435c9 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -7,6 +7,8 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -40,8 +42,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public List Import(List decisions, bool newDownload = false) { var qualifiedImports = decisions.Where(c => c.Approved) - .OrderByDescending(c => c.LocalEpisode.Quality) - .ThenByDescending(c => c.LocalEpisode.Size); + .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s + .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) + .ThenByDescending(c => c.LocalEpisode.Size)) + .SelectMany(c => c) + .ToList(); var imported = new List(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 56e49d2c2..8b7c620bb 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -60,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (parsedEpisode != null) { - if (quality != null && quality > parsedEpisode.Quality) + if (quality != null && new QualityModelComparer(parsedEpisode.Series.QualityProfile).Compare(quality, parsedEpisode.Quality) > 0) { _logger.Trace("Using quality from folder: {0}", quality); parsedEpisode.Quality = quality; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 58b5ac75d..c637ac5cd 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -1,6 +1,8 @@ using System.Linq; using NLog; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { @@ -17,7 +19,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications public bool IsSatisfiedBy(LocalEpisode localEpisode) { - if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && e.EpisodeFile.Value.Quality > localEpisode.Quality)) + var qualityComparer = new QualityModelComparer(localEpisode.Series.QualityProfile); + if (localEpisode.Episodes.Any(e => e.EpisodeFileId != 0 && qualityComparer.Compare(e.EpisodeFile.Value.Quality, localEpisode.Quality) > 0)) { _logger.Trace("This file isn't an upgrade for all episodes. Skipping {0}", localEpisode.Path); return false; diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index a948fa830..768c3fda3 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -5,6 +5,7 @@ using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Notifications diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5dbf4fd48..b93b226a2 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -145,6 +145,8 @@ + + @@ -191,6 +193,8 @@ + + @@ -456,12 +460,13 @@ + - + @@ -489,7 +494,7 @@ - + @@ -575,7 +580,7 @@ Code - + Code @@ -597,7 +602,7 @@ - + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 72a4ab75c..9a35534be 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Organizer @@ -22,6 +23,7 @@ namespace NzbDrone.Core.Organizer public class FileNameBuilder : IBuildFileNames { private readonly INamingConfigService _namingConfigService; + private readonly IQualityDefinitionService _qualityDefinitionService; private readonly ICached _patternCache; private readonly Logger _logger; @@ -43,10 +45,12 @@ namespace NzbDrone.Core.Organizer RegexOptions.Compiled | RegexOptions.IgnoreCase); public FileNameBuilder(INamingConfigService namingConfigService, + IQualityDefinitionService qualityDefinitionService, ICacheManger cacheManger, Logger logger) { _namingConfigService = namingConfigService; + _qualityDefinitionService = qualityDefinitionService; _patternCache = cacheManger.GetCache(GetType()); _logger = logger; } @@ -87,12 +91,10 @@ namespace NzbDrone.Core.Organizer sortedEpisodes.First().Title }; - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance) - { - {"{Series Title}", series.Title}, - {"Original Title", episodeFile.SceneName} - }; + var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); + tokenValues.Add("{Series Title}", series.Title); + tokenValues.Add("{Original Title}", episodeFile.SceneName); tokenValues.Add("{Release Group}", episodeFile.ReleaseGroup); if (series.SeriesType == SeriesTypes.Daily) @@ -146,7 +148,7 @@ namespace NzbDrone.Core.Organizer } tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); - tokenValues.Add("{Quality Title}", episodeFile.Quality.ToString()); + tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); return CleanFilename(ReplaceTokens(pattern, tokenValues).Trim()); @@ -341,6 +343,14 @@ namespace NzbDrone.Core.Organizer return String.Join(" + ", episodeTitles.Select(Parser.Parser.CleanupEpisodeTitle).Distinct()); } + + private string GetQualityTitle(QualityModel quality) + { + if (quality.Proper) + return _qualityDefinitionService.Get(quality.Quality).Title + " Proper"; + else + return _qualityDefinitionService.Get(quality.Quality).Title; + } } public enum MultiEpisodeStyle diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 06f620a96..447055328 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -1,7 +1,8 @@ using System; -using System.Collections.Generic; -using NzbDrone.Core.Tv; using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser.Model { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 7ae94f647..fffc79c96 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser.Model diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 5d62d3b04..656e522da 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -2,65 +2,24 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Converters; + namespace NzbDrone.Core.Qualities { - public class Quality : IComparable, IEmbeddedDocument + public class Quality : IEmbeddedDocument, IEquatable { public int Id { get; set; } public string Name { get; set; } - public int Weight { get; set; } - public int CompareTo(Quality other) + public Quality() { - if (other.Weight > Weight) - return -1; - - if (other.Weight < Weight) - return 1; - - if (other.Weight == Weight) - return 0; - - return 0; } - public static bool operator !=(Quality x, Quality y) + private Quality(int id, string name) { - return !(x == y); - } - - public static bool operator ==(Quality x, Quality y) - { - var xObj = (Object)x; - var yObj = (object)y; - - if (xObj == null || yObj == null) - { - return xObj == yObj; - } - - return x.CompareTo(y) == 0; - } - - public static bool operator >(Quality x, Quality y) - { - return x.CompareTo(y) > 0; - } - - public static bool operator <(Quality x, Quality y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator <=(Quality x, Quality y) - { - return x.CompareTo(y) <= 0; - } - - public static bool operator >=(Quality x, Quality y) - { - return x.CompareTo(y) >= 0; + Id = id; + Name = name; } public override string ToString() @@ -70,110 +29,96 @@ namespace NzbDrone.Core.Qualities public override int GetHashCode() { - unchecked // Overflow is fine, just wrap - { - int hash = 17; - hash = hash * 23 + Weight.GetHashCode(); - return hash; - } + return Id.GetHashCode(); } public bool Equals(Quality other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Equals(other.Weight, Weight); + return Id.Equals(other.Id); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof(Quality)) return false; - return Equals((Quality)obj); + + return Equals(obj as Quality); } - public static Quality Unknown + public static bool operator ==(Quality left, Quality right) { - get { return new Quality { Id = 0, Name = "Unknown", Weight = 0 }; } + return Equals(left, right); } - public static Quality SDTV + public static bool operator !=(Quality left, Quality right) { - get { return new Quality { Id = 1, Name = "SDTV", Weight = 1 }; } + return !Equals(left, right); } - public static Quality WEBDL480p + public static Quality Unknown { get { return new Quality(0, "Unknown"); } } + public static Quality SDTV { get { return new Quality(1, "SDTV"); } } + public static Quality DVD { get { return new Quality(2, "DVD"); } } + public static Quality WEBDL1080p { get { return new Quality(3, "WEBDL-1080p"); } } + public static Quality HDTV720p { get { return new Quality(4, "HDTV-720p"); } } + public static Quality WEBDL720p { get { return new Quality(5, "WEBDL-720p"); } } + public static Quality Bluray720p { get { return new Quality(6, "Bluray-720p"); } } + public static Quality Bluray1080p { get { return new Quality(7, "Bluray-1080p"); } } + public static Quality WEBDL480p { get { return new Quality(8, "WEBDL-480p"); } } + public static Quality HDTV1080p { get { return new Quality(9, "HDTV-1080p"); } } + public static Quality RAWHD { get { return new Quality(10, "Raw-HD"); } } + public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p"); } } + + public static List All { - get { return new Quality { Id = 8, Name = "WEBDL-480p", Weight = 2 }; } + get + { + return new List + { + SDTV, + DVD, + WEBDL1080p, + HDTV720p, + WEBDL720p, + Bluray720p, + Bluray1080p, + WEBDL480p, + HDTV1080p, + RAWHD + }; + } } - public static Quality DVD + public static HashSet DefaultQualityDefinitions { - get { return new Quality { Id = 2, Name = "DVD", Weight = 3 }; } - } - - public static Quality HDTV720p - { - get { return new Quality { Id = 4, Name = "HDTV-720p", Weight = 4 }; } - } - - public static Quality HDTV1080p - { - get { return new Quality { Id = 9, Name = "HDTV-1080p", Weight = 5 }; } - } - - public static Quality RAWHD - { - get { return new Quality { Id = 10, Name = "Raw-HD", Weight = 6 }; } - } - - public static Quality WEBDL720p - { - get { return new Quality { Id = 5, Name = "WEBDL-720p", Weight = 7 }; } - } - - public static Quality Bluray720p - { - get { return new Quality { Id = 6, Name = "Bluray720p", Weight = 8 }; } - } - - public static Quality WEBDL1080p - { - get { return new Quality { Id = 3, Name = "WEBDL-1080p", Weight = 9 }; } - } - - public static Quality Bluray1080p - { - get { return new Quality { Id = 7, Name = "Bluray1080p", Weight = 10 }; } - } - - public static List All() - { - return new List - { - SDTV, - WEBDL480p, - DVD, - HDTV720p, - HDTV1080p, - RAWHD, - WEBDL720p, - WEBDL1080p, - Bluray720p, - Bluray1080p - }; + get + { + return new HashSet + { + new QualityDefinition(Quality.SDTV) { /*Title = "SDTV", */ Weight = 1, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL480p) { /*Title = "WEB-DL", */ Weight = 2, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.DVD) { /*Title = "DVD", */ Weight = 3, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV720p) { /*Title = "720p HDTV", */ Weight = 4, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.HDTV1080p) { /*Title = "1080p HDTV", */ Weight = 5, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.RAWHD) { /*Title = "RawHD", */ Weight = 6, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL720p) { /*Title = "720p WEB-DL", */ Weight = 7, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray720p) { /*Title = "720p BluRay", */ Weight = 8, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.WEBDL1080p) { /*Title = "1080p WEB-DL",*/ Weight = 9, MinSize=0, MaxSize=100 }, + new QualityDefinition(Quality.Bluray1080p) { /*Title = "1080p BluRay",*/ Weight = 10, MinSize=0, MaxSize=100 } + }; + } } public static Quality FindById(int id) { if (id == 0) return Unknown; - var quality = All().SingleOrDefault(q => q.Id == id); + Quality quality = All.FirstOrDefault(v => v.Id == id); if (quality == null) throw new ArgumentException("ID does not match a known quality", "id"); - + return quality; } diff --git a/src/NzbDrone.Core/Qualities/QualityDefinition.cs b/src/NzbDrone.Core/Qualities/QualityDefinition.cs new file mode 100644 index 000000000..14cc1f008 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinition.cs @@ -0,0 +1,33 @@ +using NzbDrone.Core.Datastore; + + +namespace NzbDrone.Core.Qualities +{ + public class QualityDefinition : ModelBase + { + public Quality Quality { get; set; } + + public string Title { get; set; } + + public int Weight { get; set; } + + public int MinSize { get; set; } + public int MaxSize { get; set; } + + public QualityDefinition() + { + + } + + public QualityDefinition(Quality quality) + { + Quality = quality; + Title = quality.Name; + } + + public override string ToString() + { + return Quality.Name; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs new file mode 100644 index 000000000..f73fb2de8 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.Qualities +{ + public interface IQualityDefinitionRepository : IBasicRepository + { + QualityDefinition GetByQualityId(int qualityId); + } + + public class QualityDefinitionRepository : BasicRepository, IQualityDefinitionRepository + { + public QualityDefinitionRepository(IDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public QualityDefinition GetByQualityId(int qualityId) + { + try + { + return Query.Single(q => (int)q.Quality == qualityId); + } + catch (InvalidOperationException e) + { + throw new ModelNotFoundException(typeof(QualityDefinition), qualityId); + } + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs new file mode 100644 index 000000000..9895461ec --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using System; + +namespace NzbDrone.Core.Qualities +{ + public interface IQualityDefinitionService + { + void Update(QualityDefinition qualityDefinition); + List All(); + QualityDefinition Get(Quality quality); + } + + public class QualityDefinitionService : IQualityDefinitionService, IHandle + { + private readonly IQualityDefinitionRepository _qualityDefinitionRepository; + private readonly Logger _logger; + + public QualityDefinitionService(IQualityDefinitionRepository qualityDefinitionRepository, Logger logger) + { + _qualityDefinitionRepository = qualityDefinitionRepository; + _logger = logger; + } + + public void Update(QualityDefinition qualityDefinition) + { + _qualityDefinitionRepository.Update(qualityDefinition); + } + + public List All() + { + return _qualityDefinitionRepository.All().ToList(); + } + + public QualityDefinition Get(Quality quality) + { + if (quality == Quality.Unknown) + return new QualityDefinition(Quality.Unknown); + + return _qualityDefinitionRepository.GetByQualityId((int)quality); + } + + public void InsertMissingDefinitions(List allDefinitions) + { + allDefinitions.OrderBy(v => v.Weight).ToList(); + var existingDefinitions = _qualityDefinitionRepository.All().OrderBy(v => v.Weight).ToList(); + + // Try insert each item intelligently to merge the lists preserving the Weight the user set. + for (int i = 0; i < allDefinitions.Count;i++) + { + // Skip if this definition isn't missing. + if (existingDefinitions.Any(v => v.Quality == allDefinitions[i].Quality)) + continue; + + int targetIndexMinimum = 0; + for (int j = 0; j < i; j++) + targetIndexMinimum = Math.Max(targetIndexMinimum, existingDefinitions.FindIndex(v => v.Quality == allDefinitions[j].Quality) + 1); + + int targetIndexMaximum = existingDefinitions.Count; + for (int j = i + 1; j < allDefinitions.Count; j++) + { + var index = existingDefinitions.FindIndex(v => v.Quality == allDefinitions[j].Quality); + if (index != -1) + targetIndexMaximum = Math.Min(targetIndexMaximum, index); + } + + // Rounded down average sounds reasonable. + int targetIndex = (targetIndexMinimum + targetIndexMaximum) / 2; + + existingDefinitions.Insert(targetIndex, allDefinitions[i]); + } + + // Update all Weights. + List insertList = new List(); + List updateList = new List(); + for (int i = 0; i < existingDefinitions.Count; i++) + { + if (existingDefinitions[i].Id == 0) + { + existingDefinitions[i].Weight = i + 1; + _qualityDefinitionRepository.Insert(existingDefinitions[i]); + } + else if (existingDefinitions[i].Weight != i + 1) + { + existingDefinitions[i].Weight = i + 1; + _qualityDefinitionRepository.Update(existingDefinitions[i]); + } + } + } + + public void Handle(ApplicationStartedEvent message) + { + _logger.Debug("Setting up default quality config"); + + InsertMissingDefinitions(Quality.DefaultQualityDefinitions.ToList()); + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs new file mode 100644 index 000000000..ef4dc9bd0 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -0,0 +1,73 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Qualities +{ + public class QualityModel : IEmbeddedDocument, IEquatable + { + public Quality Quality { get; set; } + + public Boolean Proper { get; set; } + + public QualityModel() + : this(Quality.Unknown) + { + + } + + public QualityModel(Quality quality, Boolean proper = false) + { + Quality = quality; + Proper = proper; + } + + + public override string ToString() + { + string result = Quality.ToString(); + if (Proper) + { + result += " Proper"; + } + + return result; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hash = 17; + hash = hash * 23 + Proper.GetHashCode(); + hash = hash * 23 + Quality.GetHashCode(); + return hash; + } + } + + public bool Equals(QualityModel other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return other.Quality.Equals(Quality) && other.Proper.Equals(Proper); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + return Equals(obj as QualityModel); + } + + public static bool operator ==(QualityModel left, QualityModel right) + { + return Equals(left, right); + } + + public static bool operator !=(QualityModel left, QualityModel right) + { + return !Equals(left, right); + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs new file mode 100644 index 000000000..2859059af --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.EnsureThat; + +namespace NzbDrone.Core.Qualities +{ + public class QualityModelComparer : IComparer, IComparer + { + private readonly QualityProfile _qualityProfile; + + public QualityModelComparer(QualityProfile qualityProfile) + { + Ensure.That(qualityProfile, () => qualityProfile).IsNotNull(); + Ensure.That(qualityProfile.Allowed, () => qualityProfile.Allowed).HasItems(); + + _qualityProfile = qualityProfile; + } + + public int Compare(Quality left, Quality right) + { + int leftIndex = _qualityProfile.Allowed.IndexOf(left); + int rightIndex = _qualityProfile.Allowed.IndexOf(right); + + return leftIndex.CompareTo(rightIndex); + } + + public int Compare(QualityModel left, QualityModel right) + { + int result = Compare(left.Quality, right.Quality); + + if (result == 0) + result = left.Proper.CompareTo(right.Proper); + + return result; + } + /* + public string GetName(Quality quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality); + + return qualityDefinition.Name; + } + + public string GetName(QualityModel quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); + + if (quality.Proper) + return qualityDefinition.Name + " Proper"; + else + return qualityDefinition.Name; + } + + public string GetSceneName(QualityModel quality) + { + QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); + + if (quality.Proper) + return qualityDefinition.SceneName + " PROPER"; + else + return qualityDefinition.SceneName; + }*/ + } +} diff --git a/src/NzbDrone.Core/Qualities/QualitySize.cs b/src/NzbDrone.Core/Qualities/QualitySize.cs deleted file mode 100644 index 39f7f4e29..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySize.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NzbDrone.Core.Datastore; - - -namespace NzbDrone.Core.Qualities -{ - public class QualitySize : ModelBase - { - public int QualityId { get; set; } - public string Name { get; set; } - public int MinSize { get; set; } - public int MaxSize { get; set; } - - public override string ToString() - { - return Name; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs b/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs deleted file mode 100644 index f96f84be3..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySizeRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - - -namespace NzbDrone.Core.Qualities -{ - public interface IQualitySizeRepository : IBasicRepository - { - QualitySize GetByQualityId(int qualityId); - } - - public class QualitySizeRepository : BasicRepository, IQualitySizeRepository - { - public QualitySizeRepository(IDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - - public QualitySize GetByQualityId(int qualityId) - { - try - { - return Query.Single(q => q.QualityId == qualityId); - } - catch (InvalidOperationException e) - { - throw new ModelNotFoundException(typeof(QualitySize), qualityId); - } - } - } -} diff --git a/src/NzbDrone.Core/Qualities/QualitySizeService.cs b/src/NzbDrone.Core/Qualities/QualitySizeService.cs deleted file mode 100644 index 0671a6c10..000000000 --- a/src/NzbDrone.Core/Qualities/QualitySizeService.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Qualities -{ - public interface IQualitySizeService - { - void Update(QualitySize qualitySize); - List All(); - QualitySize Get(int qualityId); - } - - public class QualitySizeService : IQualitySizeService, IHandle - { - private readonly IQualitySizeRepository _qualitySizeRepository; - private readonly Logger _logger; - - public QualitySizeService(IQualitySizeRepository qualitySizeRepository, Logger logger) - { - _qualitySizeRepository = qualitySizeRepository; - _logger = logger; - } - - public virtual void Update(QualitySize qualitySize) - { - _qualitySizeRepository.Update(qualitySize); - } - - - public virtual List All() - { - return _qualitySizeRepository.All().ToList(); - } - - public virtual QualitySize Get(int qualityId) - { - return _qualitySizeRepository.GetByQualityId(qualityId); - } - - public void Handle(ApplicationStartedEvent message) - { - var existing = All(); - - _logger.Debug("Setting up default quality sizes"); - - foreach (var quality in Quality.All()) - { - if (!existing.Any(s => s.QualityId == quality.Id)) - { - _qualitySizeRepository.Insert(new QualitySize - { - QualityId = quality.Id, - Name = quality.Name, - MinSize = 0, - MaxSize = 100 - }); - } - } - } - } -} diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index e1ceb3ff8..733ff2301 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -1,5 +1,6 @@ using System; using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Queue diff --git a/src/NzbDrone.Core/Tv/QualityModel.cs b/src/NzbDrone.Core/Tv/QualityModel.cs deleted file mode 100644 index eaa7f7884..000000000 --- a/src/NzbDrone.Core/Tv/QualityModel.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Tv -{ - public class QualityModel : IComparable, IEmbeddedDocument - { - public Quality Quality { get; set; } - - public Boolean Proper { get; set; } - - public QualityModel() - : this(Quality.Unknown) - { - - } - - public QualityModel(Quality quality, Boolean proper = false) - { - Quality = quality; - Proper = proper; - } - - public int CompareTo(QualityModel other) - { - if (other.Quality > Quality) - return -1; - - if (other.Quality < Quality) - return 1; - - if (other.Quality == Quality && other.Proper == Proper) - return 0; - - if (Proper && !other.Proper) - return 1; - - if (!Proper && other.Proper) - return -1; - - return 0; - } - - public static bool operator !=(QualityModel x, QualityModel y) - { - return !(x == y); - } - - public static bool operator ==(QualityModel x, QualityModel y) - { - var xObj = (Object)x; - var yObj = (object)y; - - if (xObj == null || yObj == null) - { - return xObj == yObj; - } - - return x.CompareTo(y) == 0; - } - - public static bool operator >(QualityModel x, QualityModel y) - { - return x.CompareTo(y) > 0; - } - - public static bool operator <(QualityModel x, QualityModel y) - { - return x.CompareTo(y) < 0; - } - - public static bool operator <=(QualityModel x, QualityModel y) - { - return x.CompareTo(y) <= 0; - } - - public static bool operator >=(QualityModel x, QualityModel y) - { - return x.CompareTo(y) >= 0; - } - - public override string ToString() - { - string result = Quality.ToString(); - if (Proper) - { - result += " Proper"; - } - - return result; - } - - public override int GetHashCode() - { - unchecked // Overflow is fine, just wrap - { - int hash = 17; - hash = hash * 23 + Proper.GetHashCode(); - hash = hash * 23 + Quality.GetHashCode(); - return hash; - } - } - - public bool Equals(QualityModel other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Equals(other.Quality, Quality) && other.Proper.Equals(Proper); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof(QualityModel)) return false; - return Equals((QualityModel)obj); - } - } -} diff --git a/src/UI/JsLibraries/backbone.collectionview.js b/src/UI/JsLibraries/backbone.collectionview.js new file mode 100644 index 000000000..170871ec4 --- /dev/null +++ b/src/UI/JsLibraries/backbone.collectionview.js @@ -0,0 +1,1072 @@ +/*! +* Backbone.CollectionView, v0.8.1 +* Copyright (c)2013 Rotunda Software, LLC. +* Distributed under MIT license +* http://github.com/rotundasoftware/backbone-collection-view +*/ + + +(function() { + var mDefaultModelViewConstructor = Backbone.View; + + var kDefaultReferenceBy = "model"; + + var kAllowedOptions = [ + "collection", "modelView", "modelViewOptions", "itemTemplate", "emptyListCaption", + "selectable", "clickToSelect", "selectableModelsFilter", "visibleModelsFilter", + "selectMultiple", "clickToToggle", "processKeyEvents", "sortable", "sortableOptions", "sortableModelsFilter", "itemTemplateFunction", "detachedRendering" + ]; + + var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ]; + + var kStylesForEmptyListCaption = { + "background" : "transparent", + "border" : "none", + "box-shadow" : "none" + }; + + Backbone.CollectionView = Backbone.View.extend( { + + tagName : "ul", + + events : { + "mousedown li, td" : "_listItem_onMousedown", + "dblclick li, td" : "_listItem_onDoubleClick", + "click" : "_listBackground_onClick", + "click ul.collection-list, table.collection-list" : "_listBackground_onClick", + "keydown" : "_onKeydown" + }, + + // only used if Backbone.Courier is available + spawnMessages : { + "focus" : "focus" + }, + + //only used if Backbone.Courier is available + passMessages : { "*" : "." }, + + initialize : function( options ) { + var _this = this; + + this._hasBeenRendered = false; + + // default options + options = _.extend( {}, { + collection : null, + modelView : this.modelView || null, + modelViewOptions : {}, + itemTemplate : null, + itemTemplateFunction : null, + selectable : true, + clickToSelect : true, + selectableModelsFilter : null, + visibleModelsFilter : null, + sortableModelsFilter : null, + selectMultiple : false, + clickToToggle : false, + processKeyEvents : true, + sortable : false, + sortableOptions : null, + detachedRendering : false, + emptyListCaption : null + }, options ); + + // add each of the white-listed options to the CollectionView object itself + _.each( kAllowedOptions, function( option ) { + _this[ option ] = options[option]; + } ); + + if( ! this.collection ) this.collection = new Backbone.Collection(); + + if( this._isBackboneCourierAvailable() ) { + Backbone.Courier.add( this ); + } + + this.$el.data( "view", this ); // needed for connected sortable lists + this.$el.addClass( "collection-list" ); + if( this.processKeyEvents ) + this.$el.attr( "tabindex", 0 ); // so we get keyboard events + + this.selectedItems = []; + + this._updateItemTemplate(); + + if( this.collection ) + this._registerCollectionEvents(); + + this.viewManager = new ChildViewContainer(); + + //this.listenTo( this.collection, "change", function() { this.render(); this.spawn( "change" ); } ); // don't want changes to models bubbling up and triggering the list's render() function + + // note we do NOT call render here anymore, because if we inherit from this class we will likely call this + // function using __super__ before the rest of the initialization logic for the decedent class. however, we may + // override the render() function in that decedent class as well, and that will certainly expect all the initialization + // to be done already. so we have to make sure to not jump the gun and start rending at this point. + // this.render(); + }, + + setOption : function( name, value ) { + + var _this = this; + + if( name === "collection" ) { + this._setCollection( value ); + } + else { + if( _.contains( kAllowedOptions, name ) ) { + + switch( name ) { + case "selectMultiple" : + this[ name ] = value; + if( !value && this.selectedItems.length > 1 ) + this.setSelectedModel( _.first( this.selectedItems ), { by : "cid" } ); + break; + case "selectable" : + if( !value && this.selectedItems.length > 0 ) + this.setSelectedModels( [] ); + this[ name ] = value; + break; + case "selectableModelsFilter" : + this[ name ] = value; + if( value && _.isFunction( value ) ) + this._validateSelection(); + break; + case "itemTemplate" : + this[ name ] = value; + this._updateItemTemplate(); + break; + case "processKeyEvents" : + this[ name ] = value; + if( value ) this.$el.attr( "tabindex", 0 ); // so we get keyboard events + break; + case "modelView" : + this[ name ] = value; + //need to remove all old view instances + this.viewManager.each( function( view ) { + _this.viewManager.remove( view ); + // destroy the View itself + view.remove(); + } ); + break; + default : + this[ name ] = value; + } + + if( _.contains( kOptionsRequiringRerendering, name ) ) this.render(); + } + else throw name + " is not an allowed option"; + } + }, + + getSelectedModel : function( options ) { + return _.first( this.getSelectedModels( options ) ); + }, + + getSelectedModels : function ( options ) { + var _this = this; + + options = _.extend( {}, { + by : kDefaultReferenceBy + }, options ); + + var referenceBy = options.by; + var items = []; + + switch( referenceBy ) { + case "id" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.collection.get( item ).id ); + } ); + break; + case "cid" : + items = items.concat( this.selectedItems ); + break; + case "offset" : + var curLineNumber = 0; + + var itemElements; + if( this._isRenderedAsTable() ) + itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); + else if( this._isRenderedAsList() ) + itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); + + itemElements.each( function() { + var thisItemEl = $( this ); + if( thisItemEl.is( ".selected" ) ) + items.push( curLineNumber ); + curLineNumber++; + } ); + break; + case "model" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.collection.get( item ) ); + } ); + break; + case "view" : + _.each( this.selectedItems, function ( item ) { + items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) ); + } ); + break; + } + + return items; + + }, + + setSelectedModels : function( newSelectedItems, options ) { + if( ! this.selectable ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors + if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value"; + + options = _.extend( {}, { + silent : false, + by : kDefaultReferenceBy + }, options ); + + var referenceBy = options.by; + var newSelectedCids = []; + + switch( referenceBy ) { + case "cid" : + newSelectedCids = newSelectedItems; + break; + case "id" : + this.collection.each( function( thisModel ) { + if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid ); + } ); + break; + case "model" : + newSelectedCids = _.pluck( newSelectedItems, "cid" ); + break; + case "view" : + _.each( newSelectedItems, function( item ) { + newSelectedCids.push( item.model.cid ); + } ); + break; + case "offset" : + var curLineNumber = 0; + var selectedItems = []; + + var itemElements; + if( this._isRenderedAsTable() ) + itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); + else if( this._isRenderedAsList() ) + itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); + + itemElements.each( function() { + var thisItemEl = $( this ); + if( _.contains( newSelectedItems, curLineNumber ) ) + newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) ); + curLineNumber++; + } ); + break; + } + + var oldSelectedModels = this.getSelectedModels(); + var oldSelectedCids = _.clone( this.selectedItems ); + + this.selectedItems = this._convertStringsToInts( newSelectedCids ); + this._validateSelection(); + + var newSelectedModels = this.getSelectedModels(); + + if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) ) + { + this._addSelectedClassToSelectedItems( oldSelectedCids ); + + if( ! options.silent ) + { + this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "selectionChanged", { + selectedModels : newSelectedModels, + oldSelectedModels : oldSelectedModels + } ); + } + } + + this.updateDependentControls(); + } + }, + + setSelectedModel : function( newSelectedItem, options ) { + if( ! newSelectedItem && newSelectedItem !== 0 ) + this.setSelectedModels( [], options ); + else + this.setSelectedModels( [ newSelectedItem ], options ); + }, + + render : function(){ + var _this = this; + + this._hasBeenRendered = true; + + if( this.selectable ) this._saveSelection(); + + var modelViewContainerEl; + + // If collection view element is a table and it has a tbody + // within it, render the model views inside of the tbody + if( this._isRenderedAsTable() ) { + var tbodyChild = this.$el.find( "> tbody" ); + if( tbodyChild.length > 0 ) + modelViewContainerEl = tbodyChild; + } + + if( _.isUndefined( modelViewContainerEl ) ) + modelViewContainerEl = this.$el; + + var oldViewManager = this.viewManager; + this.viewManager = new ChildViewContainer(); + + // detach each of our subviews that we have already created to represent models + // in the collection. We are going to re-use the ones that represent models that + // are still here, instead of creating new ones, so that we don't loose state + // information in the views. + oldViewManager.each( function( thisModelView ) { + // to boost performance, only detach those views that will be sticking around. + // we won't need the other ones later, so no need to detach them individually. + if( _this.collection.get( thisModelView.model.cid ) ) + thisModelView.$el.detach(); + else + thisModelView.remove(); + } ); + + modelViewContainerEl.empty(); + var fragmentContainer; + + if( this.detachedRendering ) + fragmentContainer = document.createDocumentFragment(); + + this.collection.each( function( thisModel ) { + var thisModelView; + + thisModelView = oldViewManager.findByModelCid( thisModel.cid ); + if( _.isUndefined( thisModelView ) ) { + // if the model view was not already created on previous render, + // then create and initialize it now. + + var modelViewOptions = this._getModelViewOptions( thisModel ); + thisModelView = this._createNewModelView( thisModel, modelViewOptions ); + + thisModelView.collectionListView = _this; + } + + var thisModelViewWrapped = this._wrapModelView( thisModelView ); + if( this.detachedRendering ) + fragmentContainer.appendChild( thisModelViewWrapped[0] ); + else + modelViewContainerEl.append( thisModelViewWrapped ); + + // we have to render the modelView after it has been put in context, as opposed to in the + // initialize function of the modelView, because some rendering might be dependent on + // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself, + // it must be in full context in the DOM tree or else the stretch will not behave as intended. + var renderResult = thisModelView.render(); + + // return false from the view's render function to hide this item + if( renderResult === false ) { + thisModelViewWrapped.hide(); + thisModelViewWrapped.addClass( "not-visible" ); + } + + if( _.isFunction( this.visibleModelsFilter ) ) { + if( ! this.visibleModelsFilter( thisModel ) ) { + if( thisModelViewWrapped.children().length === 1 ) + thisModelViewWrapped.hide(); + else thisModelView.$el.hide(); + + thisModelViewWrapped.addClass( "not-visible" ); + } + } + + this.viewManager.add( thisModelView ); + }, this ); + + if( this.detachedRendering ) + modelViewContainerEl.append( fragmentContainer ); + + if( this.sortable ) + { + var sortableOptions = _.extend( { + axis: "y", + distance: 10, + forcePlaceholderSize : true, + start : _.bind( this._sortStart, this ), + change : _.bind( this._sortChange, this ), + stop : _.bind( this._sortStop, this ), + receive : _.bind( this._receive, this ), + over : _.bind( this._over, this ) + }, _.result( this, "sortableOptions" ) ); + + if( _this._isRenderedAsTable() ) { + sortableOptions.items = "> tbody > tr:not(.not-sortable)"; + } + else if( _this._isRenderedAsList() ) { + sortableOptions.items = "> li:not(.not-sortable)"; + } + + this.$el = this.$el.sortable( sortableOptions ); + } + + if( this.emptyListCaption ) { + var visibleView = this.viewManager.find( function( view ) { + return ! view.$el.hasClass( "not-visible" ); + } ); + + if( _.isUndefined( visibleView ) ) { + var emptyListString; + + if( _.isFunction( this.emptyListCaption ) ) + emptyListString = this.emptyListCaption(); + else + emptyListString = this.emptyListCaption; + + var $emptyCaptionEl; + var $varEl = $( "" + emptyListString + "" ); + + //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td) + if( this._isRenderedAsList() ) + $emptyListCaptionEl = $varEl.wrapAll( "
  • " ).parent().css( kStylesForEmptyListCaption ); + else + $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().parent().css( kStylesForEmptyListCaption ); + + this.$el.append( $emptyListCaptionEl ); + + } + } + + this.trigger( "render" ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "render" ); + + if( this.selectable ) { + this._restoreSelection(); + this.updateDependentControls(); + } + + if( _.isFunction( this.onAfterRender ) ) + this.onAfterRender(); + }, + + updateDependentControls : function() { + this.trigger( "updateDependentControls", this.getSelectedModels() ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "updateDependentControls", { + selectedModels : this.getSelectedModels() + } ); + } + }, + + // Override `Backbone.View.remove` to also destroy all Views in `viewManager` + remove : function() { + this.viewManager.each( function( view ) { + view.remove(); + } ); + + Backbone.View.prototype.remove.apply( this, arguments ); + }, + + _validateSelectionAndRender : function() { + this._validateSelection(); + this.render(); + }, + + _registerCollectionEvents : function() { + this.listenTo( this.collection, "add", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "add" ); + } ); + + this.listenTo( this.collection, "remove", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "remove" ); + } ); + + this.listenTo( this.collection, "reset", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "reset" ); + } ); + + // It should be up to the model to rerender itself when it changes. + // this.listenTo( this.collection, "change", function( model ) { + // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render(); + // if( this._isBackboneCourierAvailable() ) + // this.spawn( "change", { model : model } ); + // } ); + + this.listenTo( this.collection, "sort", function() { + if( this._hasBeenRendered ) this.render(); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sort" ); + } ); + }, + + _getClickedItemId : function( theEvent ) { + var clickedItemId = null; + + // important to use currentTarget as opposed to target, since we could be bubbling + // an event that took place within another collectionList + var clickedItemEl = $( theEvent.currentTarget ); + if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return; + + // determine which list item was clicked. If we clicked in the blank area + // underneath all the elements, we want to know that too, since in this + // case we will want to deselect all elements. so check to see if the clicked + // DOM element is the list itself to find that out. + var clickedItem = clickedItemEl.closest( "[data-model-cid]" ); + if( clickedItem.length > 0 ) + { + clickedItemId = clickedItem.attr( "data-model-cid" ); + if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 ); + } + + return clickedItemId; + }, + + _setCollection : function( newCollection ) { + if( newCollection !== this.collection ) + { + this.stopListening( this.collection ); + this.collection = newCollection; + this._registerCollectionEvents(); + } + + if( this._hasBeenRendered ) this.render(); + }, + + _updateItemTemplate : function() { + var itemTemplateHtml; + if( this.itemTemplate ) + { + if( $( this.itemTemplate ).length === 0 ) + throw "Could not find item template from selector: " + this.itemTemplate; + + itemTemplateHtml = $( this.itemTemplate ).html(); + } + else + itemTemplateHtml = this.$( ".item-template" ).html(); + + if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml ); + + }, + + _validateSelection : function() { + // note can't use the collection's proxy to underscore because "cid" ais not an attribute, + // but an element of the model object itself. + var modelReferenceIds = _.pluck( this.collection.models, "cid" ); + this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems ); + + if( _.isFunction( this.selectableModelsFilter ) ) + { + this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) { + return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) ); + }, this ); + } + }, + + _saveSelection : function() { + // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called. + if( ! this.selectable ) throw "Attempt to save selection on non-selectable list"; + this.savedSelection = { + items : this.selectedItems, + offset : this.getSelectedModel( { by : "offset" } ) + }; + }, + + _restoreSelection : function() { + if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!"; + + // reset selectedItems to empty so that we "redraw" all "selected" classes + // when we set our new selection. We do this because it is likely that our + // contents have been refreshed, and we have thus lost all old "selected" classes. + this.setSelectedModels( [], { silent : true } ); + + if( this.savedSelection.items.length > 0 ) + { + // first try to restore the old selected items using their reference ids. + this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } ); + + // all the items with the saved reference ids have been removed from the list. + // ok. try to restore the selection based on the offset that used to be selected. + // this is the expected behavior after a item is deleted from a list (i.e. select + // the line that immediately follows the deleted line). + if( this.selectedItems.length === 0 ) + this.setSelectedModel( this.savedSelection.offset, { by : "offset" } ); + + // Trigger a selection changed if the previously selected items were not all found + if (this.selectedItems.length !== this.savedSelection.items.length) + { + this.trigger( "selectionChanged", this.getSelectedModels(), [] ); + if( this._isBackboneCourierAvailable() ) { + this.spawn( "selectionChanged", { + selectedModels : this.getSelectedModels(), + oldSelectedModels : [] + } ); + } + } + } + + delete this.savedSelection; + }, + + _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) { + if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = []; + + // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we + // only have to add / remove the "selected" class from those items that "selected" state has changed. + + var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass; + itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems ); + + _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) { + this.$el.find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" ); + }, this ); + + var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems; + itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass ); + + _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) { + this.$el.find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" ); + }, this ); + }, + + _reorderCollectionBasedOnHTML : function() { + var _this = this; + + this.$el.children().each( function() { + var thisModelCid = $( this ).attr( "data-model-cid" ); + + if( thisModelCid ) + { + // remove the current model and then add it back (at the end of the collection). + // When we are done looping through all models, they will be in the correct order. + var thisModel = _this.collection.get( thisModelCid ); + if( thisModel ) + { + _this.collection.remove( thisModel, { silent : true } ); + _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } ); + } + } + } ); + + this.collection.trigger( "reorder" ); + + if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" ); + + if( this.collection.comparator ) this.collection.sort(); + + }, + + _getModelViewConstructor : function( thisModel ) { + return this.modelView || mDefaultModelViewConstructor; + }, + + _getModelViewOptions : function( thisModel ) { + return _.extend( { model : thisModel }, this.modelViewOptions ); + }, + + _createNewModelView : function( model, modelViewOptions ) { + var modelViewConstructor = this._getModelViewConstructor( model ); + if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model"; + + return new ( modelViewConstructor )( modelViewOptions ); + }, + + _wrapModelView : function( modelView ) { + var _this = this; + + // we use items client ids as opposed to real ids, since we may not have a representation + // of these models on the server + var wrappedModelView; + + if( this._isRenderedAsTable() ) { + // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid + wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); + } + else if( this._isRenderedAsList() ) { + // if we are rendering the collection in a list, we need wrap each item in an
  • (if its not already an
  • ) + // and set the data-model-cid + if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) { + wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); + } else { + wrappedModelView = modelView.$el.wrapAll( "
  • " ).parent(); + } + } + + if( _.isFunction( this.sortableModelsFilter ) ) + if( ! this.sortableModelsFilter.call( _this, modelView.model ) ) + wrappedModelView.addClass( "not-sortable" ); + + if( _.isFunction( this.selectableModelsFilter ) ) + if( ! this.selectableModelsFilter.call( _this, modelView.model ) ) + wrappedModelView.addClass( "not-selectable" ); + + return wrappedModelView; + }, + + _convertStringsToInts : function( theArray ) { + return _.map( theArray, function( thisEl ) { + if( ! _.isString( thisEl ) ) return thisEl; + var thisElAsNumber = parseInt( thisEl, 10 ); + return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl ); + } ); + }, + + _containSameElements : function( arrayA, arrayB ) { + if( arrayA.length != arrayB.length ) return false; + var intersectionSize = _.intersection( arrayA, arrayB ).length; + return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length + }, + + _isRenderedAsTable : function() { + return this.$el.prop('tagName').toLowerCase() === 'table'; + }, + + + _isRenderedAsList : function() { + return ! this._isRenderedAsTable(); + }, + + _charCodes : { + upArrow : 38, + downArrow : 40 + }, + + _isBackboneCourierAvailable : function() { + return !_.isUndefined( Backbone.Courier ); + }, + + _sortStart : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + this.trigger( "sortStart", modelBeingSorted ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } ); + }, + + _sortChange : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + this.trigger( "sortChange", modelBeingSorted ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } ); + }, + + _sortStop : function( event, ui ) { + var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); + var modelViewContainerEl = (this._isRenderedAsTable()) ? this.$el.find( "> tbody" ) : this.$el; + var newIndex = modelViewContainerEl.children().index( ui.item ); + + if( newIndex == -1 ) { + // the element was removed from this list. can happen if this sortable is connected + // to another sortable, and the item was dropped into the other sortable. + this.collection.remove( modelBeingSorted ); + } + + this._reorderCollectionBasedOnHTML(); + this.updateDependentControls(); + this.trigger( "sortStop", modelBeingSorted, newIndex ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } ); + }, + + _receive : function( event, ui ) { + var senderListEl = ui.sender; + var senderCollectionListView = senderListEl.data( "view" ); + if( ! senderCollectionListView || ! senderCollectionListView.collection ) return; + + var newIndex = this.$el.children().index( ui.item ); + var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) ); + this.collection.add( modelReceived, { at : newIndex } ); + modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value. + this.setSelectedModel( modelReceived ); + }, + + _over : function( event, ui ) { + // when an item is being dragged into the sortable, + // hide the empty list caption if it exists + this.$el.find( ".empty-list-caption" ).hide(); + }, + + _onKeydown : function( event ) { + if( ! this.processKeyEvents ) return true; + + var trap = false; + + if( this.getSelectedModels( { by : "offset" } ).length == 1 ) + { + // need to trap down and up arrows or else the browser + // will end up scrolling a autoscroll div. + + var currentOffset = this.getSelectedModel( { by : "offset" } ); + if( event.which === this._charCodes.upArrow && currentOffset !== 0 ) + { + this.setSelectedModel( currentOffset - 1, { by : "offset" } ); + trap = true; + } + else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 ) + { + this.setSelectedModel( currentOffset + 1, { by : "offset" } ); + trap = true; + } + } + + return ! trap; + }, + + _listItem_onMousedown : function( theEvent ) { + if( ! this.selectable || ! this.clickToSelect ) return; + + var clickedItemId = this._getClickedItemId( theEvent ); + + if( clickedItemId ) + { + // Exit if an unselectable item was clicked + if( _.isFunction( this.selectableModelsFilter ) && + ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) ) + { + return; + } + + // a selectable list item was clicked + if( this.selectMultiple && theEvent.shiftKey ) + { + var firstSelectedItemIndex = -1; + + if( this.selectedItems.length > 0 ) + { + this.collection.find( function( thisItemModel ) { + firstSelectedItemIndex++; + + // exit when we find our first selected element + return _.contains( this.selectedItems, thisItemModel.cid ); + }, this ); + } + + var clickedItemIndex = -1; + this.collection.find( function( thisItemModel ) { + clickedItemIndex++; + + // exit when we find the clicked element + return thisItemModel.cid == clickedItemId; + }, this ); + + var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex; + var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex ); + var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex ); + + var newSelectedItems = []; + for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ ) + newSelectedItems.push( this.collection.at( thisIndex ).cid ); + this.setSelectedModels( newSelectedItems, { by : "cid" } ); + + // shift clicking will usually highlight selectable text, which we do not want. + // this is a cross browser (hopefully) snippet that deselects all text selection. + if( document.selection && document.selection.empty ) + document.selection.empty(); + else if(window.getSelection) { + var sel = window.getSelection(); + if( sel && sel.removeAllRanges ) + sel.removeAllRanges(); + } + } + else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) ) + { + if( _.contains( this.selectedItems, clickedItemId ) ) + this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } ); + else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } ); + } + else + this.setSelectedModels( [ clickedItemId ], { by : "cid" } ); + } + else + // the blank area of the list was clicked + this.setSelectedModels( [] ); + + }, + + _listItem_onDoubleClick : function( theEvent ) { + var clickedItemId = this._getClickedItemId( theEvent ); + + if( clickedItemId ) + { + var clickedModel = this.collection.get( clickedItemId ); + this.trigger( "doubleClick", clickedModel ); + if( this._isBackboneCourierAvailable() ) + this.spawn( "doubleClick", { clickedModel : clickedModel } ); + } + }, + + _listBackground_onClick : function( theEvent ) { + if( ! this.selectable ) return; + if( ! $( theEvent.target ).is( ".collection-list" ) ) return; + + this.setSelectedModels( [] ); + } + + }, { + setDefaultModelViewConstructor : function( theConstructor ) { + mDefaultModelViewConstructor = theConstructor; + } + }); + + + // Backbone.BabySitter + // ------------------- + // v0.0.6 + // + // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. + // Distributed under MIT license + // + // http://github.com/babysitterjs/backbone.babysitter + + // Backbone.ChildViewContainer + // --------------------------- + // + // Provide a container to store, retrieve and + // shut down child views. + + ChildViewContainer = (function(Backbone, _){ + + // Container Constructor + // --------------------- + + var Container = function(views){ + this._views = {}; + this._indexByModel = {}; + this._indexByCustom = {}; + this._updateLength(); + + _.each(views, this.add, this); + }; + + // Container Methods + // ----------------- + + _.extend(Container.prototype, { + + // Add a view to this container. Stores the view + // by `cid` and makes it searchable by the model + // cid (and model itself). Optionally specify + // a custom key to store an retrieve the view. + add: function(view, customIndex){ + var viewCid = view.cid; + + // store the view + this._views[viewCid] = view; + + // index it by model + if (view.model){ + this._indexByModel[view.model.cid] = viewCid; + } + + // index by custom + if (customIndex){ + this._indexByCustom[customIndex] = viewCid; + } + + this._updateLength(); + }, + + // Find a view by the model that was attached to + // it. Uses the model's `cid` to find it. + findByModel: function(model){ + return this.findByModelCid(model.cid); + }, + + // Find a view by the `cid` of the model that was attached to + // it. Uses the model's `cid` to find the view `cid` and + // retrieve the view using it. + findByModelCid: function(modelCid){ + var viewCid = this._indexByModel[modelCid]; + return this.findByCid(viewCid); + }, + + // Find a view by a custom indexer. + findByCustom: function(index){ + var viewCid = this._indexByCustom[index]; + return this.findByCid(viewCid); + }, + + // Find by index. This is not guaranteed to be a + // stable index. + findByIndex: function(index){ + return _.values(this._views)[index]; + }, + + // retrieve a view by it's `cid` directly + findByCid: function(cid){ + return this._views[cid]; + }, + + // Remove a view + remove: function(view){ + var viewCid = view.cid; + + // delete model index + if (view.model){ + delete this._indexByModel[view.model.cid]; + } + + // delete custom index + _.any(this._indexByCustom, function(cid, key) { + if (cid === viewCid) { + delete this._indexByCustom[key]; + return true; + } + }, this); + + // remove the view from the container + delete this._views[viewCid]; + + // update the length + this._updateLength(); + }, + + // Call a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.call`. + call: function(method){ + this.apply(method, _.tail(arguments)); + }, + + // Apply a method on every view in the container, + // passing parameters to the call method one at a + // time, like `function.apply`. + apply: function(method, args){ + _.each(this._views, function(view){ + if (_.isFunction(view[method])){ + view[method].apply(view, args || []); + } + }); + }, + + // Update the `.length` attribute on this container + _updateLength: function(){ + this.length = _.size(this._views); + } + }); + + // Borrowing this code from Backbone.Collection: + // http://backbonejs.org/docs/backbone.html#section-106 + // + // Mix in methods from Underscore, for iteration, and other + // collection related features. + var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', + 'select', 'reject', 'every', 'all', 'some', 'any', 'include', + 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', + 'last', 'without', 'isEmpty', 'pluck']; + + _.each(methods, function(method) { + Container.prototype[method] = function() { + var views = _.values(this._views); + var args = [views].concat(_.toArray(arguments)); + return _[method].apply(_, args); + }; + }); + + // return the public API + return Container; + })(Backbone, _); +})(); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery-ui.js b/src/UI/JsLibraries/jquery-ui.js new file mode 100644 index 000000000..fe44a9c84 --- /dev/null +++ b/src/UI/JsLibraries/jquery-ui.js @@ -0,0 +1,4233 @@ +/*! jQuery UI - v1.10.4 - 2014-01-22 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.sortable.js, jquery.ui.slider.js +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +(function( $, undefined ) { + +var uuid = 0, + runiqueId = /^ui-id-\d+$/; + +// $.ui might exist from components with no dependencies, e.g., $.ui.position +$.ui = $.ui || {}; + +$.extend( $.ui, { + version: "1.10.4", + + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + NUMPAD_ADD: 107, + NUMPAD_DECIMAL: 110, + NUMPAD_DIVIDE: 111, + NUMPAD_ENTER: 108, + NUMPAD_MULTIPLY: 106, + NUMPAD_SUBTRACT: 109, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } +}); + +// plugins +$.fn.extend({ + focus: (function( orig ) { + return function( delay, fn ) { + return typeof delay === "number" ? + this.each(function() { + var elem = this; + setTimeout(function() { + $( elem ).focus(); + if ( fn ) { + fn.call( elem ); + } + }, delay ); + }) : + orig.apply( this, arguments ); + }; + })( $.fn.focus ), + + scrollParent: function() { + var scrollParent; + if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { + scrollParent = this.parents().filter(function() { + return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } else { + scrollParent = this.parents().filter(function() { + return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); + }).eq(0); + } + + return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; + }, + + zIndex: function( zIndex ) { + if ( zIndex !== undefined ) { + return this.css( "zIndex", zIndex ); + } + + if ( this.length ) { + var elem = $( this[ 0 ] ), position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + //
    + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } + } + + return 0; + }, + + uniqueId: function() { + return this.each(function() { + if ( !this.id ) { + this.id = "ui-id-" + (++uuid); + } + }); + }, + + removeUniqueId: function() { + return this.each(function() { + if ( runiqueId.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + }); + } +}); + +// selectors +function focusable( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap=#" + mapName + "]" )[0]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible( element ); +} + +function visible( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; +} + +$.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, + + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } +}); + +// support: jQuery <1.8 +if ( !$( "" ).outerWidth( 1 ).jquery ) { + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + }); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each(function() { + $( this ).css( type, reduce( this, size ) + "px" ); + }); + }; + + $.fn[ "outer" + name] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each(function() { + $( this).css( type, reduce( this, size, true, margin ) + "px" ); + }); + }; + }); +} + +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) +if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { + $.fn.removeData = (function( removeData ) { + return function( key ) { + if ( arguments.length ) { + return removeData.call( this, $.camelCase( key ) ); + } else { + return removeData.call( this ); + } + }; + })( $.fn.removeData ); +} + + + + + +// deprecated +$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +$.support.selectstart = "onselectstart" in document.createElement( "div" ); +$.fn.extend({ + disableSelection: function() { + return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + + ".ui-disableSelection", function( event ) { + event.preventDefault(); + }); + }, + + enableSelection: function() { + return this.unbind( ".ui-disableSelection" ); + } +}); + +$.extend( $.ui, { + // $.ui.plugin is deprecated. Use $.widget() extensions instead. + plugin: { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args ) { + var i, + set = instance.plugins[ name ]; + if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } + }, + + // only used by resizable + hasScroll: function( el, a ) { + + //If overflow is hidden, the element might have extra content, but the user wants to hide it + if ( $( el ).css( "overflow" ) === "hidden") { + return false; + } + + var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", + has = false; + + if ( el[ scroll ] > 0 ) { + return true; + } + + // TODO: determine which cases actually cause this to happen + // if the element doesn't have the scroll set, see if it's possible to + // set the scroll + el[ scroll ] = 1; + has = ( el[ scroll ] > 0 ); + el[ scroll ] = 0; + return has; + } +}); + +})( jQuery ); +(function( $, undefined ) { + +var uuid = 0, + slice = Array.prototype.slice, + _cleanData = $.cleanData; +$.cleanData = function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + _cleanData( elems ); +}; + +$.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); +}; + +$.widget.extend = function( target ) { + var input = slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = slice.call( arguments, 1 ), + returnValue = this; + + // allow multiple hashes to be passed on init + options = !isMethodCall && args.length ? + $.widget.extend.apply( null, [ options ].concat(args) ) : + options; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} )._init(); + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
    ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + // 1.9 BC for #7810 + // TODO remove dual storage + .removeData( this.widgetName ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) + .attr( "aria-disabled", value ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + + return this; + }, + + enable: function() { + return this._setOption( "disabled", false ); + }, + disable: function() { + return this._setOption( "disabled", true ); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + // accept selectors, DOM elements + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^(\w+)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; +}); + +})( jQuery ); +(function( $, undefined ) { + +var mouseHandled = false; +$( document ).mouseup( function() { + mouseHandled = false; +}); + +$.widget("ui.mouse", { + version: "1.10.4", + options: { + cancel: "input,textarea,button,select,option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .bind("mousedown."+this.widgetName, function(event) { + return that._mouseDown(event); + }) + .bind("click."+this.widgetName, function(event) { + if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { + $.removeData(event.target, that.widgetName + ".preventClickEvent"); + event.stopImmediatePropagation(); + return false; + } + }); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.unbind("."+this.widgetName); + if ( this._mouseMoveDelegate ) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + } + }, + + _mouseDown: function(event) { + // don't let more than one widget handle mouseStart + if( mouseHandled ) { return; } + + // we may have missed mouseup (out of window) + (this._mouseStarted && this._mouseUp(event)); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = (event.which === 1), + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); + if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if (!this.mouseDelayMet) { + this._mouseDelayTimer = setTimeout(function() { + that.mouseDelayMet = true; + }, this.options.delay); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = (this._mouseStart(event) !== false); + if (!this._mouseStarted) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { + $.removeData(event.target, this.widgetName + ".preventClickEvent"); + } + + // these delegates are required to keep context + this._mouseMoveDelegate = function(event) { + return that._mouseMove(event); + }; + this._mouseUpDelegate = function(event) { + return that._mouseUp(event); + }; + $(document) + .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .bind("mouseup."+this.widgetName, this._mouseUpDelegate); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function(event) { + // IE mouseup check - mouseup happened when mouse was out of window + if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { + return this._mouseUp(event); + } + + if (this._mouseStarted) { + this._mouseDrag(event); + return event.preventDefault(); + } + + if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { + this._mouseStarted = + (this._mouseStart(this._mouseDownEvent, event) !== false); + (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); + } + + return !this._mouseStarted; + }, + + _mouseUp: function(event) { + $(document) + .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) + .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); + + if (this._mouseStarted) { + this._mouseStarted = false; + + if (event.target === this._mouseDownEvent.target) { + $.data(event.target, this.widgetName + ".preventClickEvent", true); + } + + this._mouseStop(event); + } + + return false; + }, + + _mouseDistanceMet: function(event) { + return (Math.max( + Math.abs(this._mouseDownEvent.pageX - event.pageX), + Math.abs(this._mouseDownEvent.pageY - event.pageY) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function(/* event */) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function(/* event */) {}, + _mouseDrag: function(/* event */) {}, + _mouseStop: function(/* event */) {}, + _mouseCapture: function(/* event */) { return true; } +}); + +})(jQuery); +(function( $, undefined ) { + +$.widget("ui.draggable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "drag", + options: { + addClasses: true, + appendTo: "parent", + axis: false, + connectToSortable: false, + containment: false, + cursor: "auto", + cursorAt: false, + grid: false, + handle: false, + helper: "original", + iframeFix: false, + opacity: false, + refreshPositions: false, + revert: false, + revertDuration: 500, + scope: "default", + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + snap: false, + snapMode: "both", + snapTolerance: 20, + stack: false, + zIndex: false, + + // callbacks + drag: null, + start: null, + stop: null + }, + _create: function() { + + if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { + this.element[0].style.position = "relative"; + } + if (this.options.addClasses){ + this.element.addClass("ui-draggable"); + } + if (this.options.disabled){ + this.element.addClass("ui-draggable-disabled"); + } + + this._mouseInit(); + + }, + + _destroy: function() { + this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); + this._mouseDestroy(); + }, + + _mouseCapture: function(event) { + + var o = this.options; + + // among others, prevent a drag on a resizable-handle + if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { + return false; + } + + //Quit if we're not on a valid handle + this.handle = this._getHandle(event); + if (!this.handle) { + return false; + } + + $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { + $("
    ") + .css({ + width: this.offsetWidth+"px", height: this.offsetHeight+"px", + position: "absolute", opacity: "0.001", zIndex: 1000 + }) + .css($(this).offset()) + .appendTo("body"); + }); + + return true; + + }, + + _mouseStart: function(event) { + + var o = this.options; + + //Create and append the visible helper + this.helper = this._createHelper(event); + + this.helper.addClass("ui-draggable-dragging"); + + //Cache the helper size + this._cacheHelperProportions(); + + //If ddmanager is used for droppables, set the global draggable + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Store the helper's css position + this.cssPosition = this.helper.css( "position" ); + this.scrollParent = this.helper.scrollParent(); + this.offsetParent = this.helper.offsetParent(); + this.offsetParentCssPosition = this.offsetParent.css( "position" ); + + //The element's absolute position on the page minus margins + this.offset = this.positionAbs = this.element.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + //Reset scroll cache + this.offset.scroll = false; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + //Generate the original position + this.originalPosition = this.position = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Set a containment if given in the options + this._setContainment(); + + //Trigger event + callbacks + if(this._trigger("start", event) === false) { + this._clear(); + return false; + } + + //Recache the helper size + this._cacheHelperProportions(); + + //Prepare the droppable offsets + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + + this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position + + //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) + if ( $.ui.ddmanager ) { + $.ui.ddmanager.dragStart(this, event); + } + + return true; + }, + + _mouseDrag: function(event, noPropagation) { + // reset any necessary cached properties (see #5009) + if ( this.offsetParentCssPosition === "fixed" ) { + this.offset.parent = this._getParentOffset(); + } + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + //Call plugins and callbacks and use the resulting position if something is returned + if (!noPropagation) { + var ui = this._uiHash(); + if(this._trigger("drag", event, ui) === false) { + this._mouseUp({}); + return false; + } + this.position = ui.position; + } + + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + return false; + }, + + _mouseStop: function(event) { + + //If we are using droppables, inform the manager about the drop + var that = this, + dropped = false; + if ($.ui.ddmanager && !this.options.dropBehaviour) { + dropped = $.ui.ddmanager.drop(this, event); + } + + //if a drop comes from outside (a sortable) + if(this.dropped) { + dropped = this.dropped; + this.dropped = false; + } + + //if the original element is no longer in the DOM don't bother to continue (see #8269) + if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { + return false; + } + + if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { + $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { + if(that._trigger("stop", event) !== false) { + that._clear(); + } + }); + } else { + if(this._trigger("stop", event) !== false) { + this._clear(); + } + } + + return false; + }, + + _mouseUp: function(event) { + //Remove frame helpers + $("div.ui-draggable-iframeFix").each(function() { + this.parentNode.removeChild(this); + }); + + //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) + if( $.ui.ddmanager ) { + $.ui.ddmanager.dragStop(this, event); + } + + return $.ui.mouse.prototype._mouseUp.call(this, event); + }, + + cancel: function() { + + if(this.helper.is(".ui-draggable-dragging")) { + this._mouseUp({}); + } else { + this._clear(); + } + + return this; + + }, + + _getHandle: function(event) { + return this.options.handle ? + !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : + true; + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); + + if(!helper.parents("body").length) { + helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); + } + + if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { + helper.css("position", "absolute"); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + //Get the offsetParent and cache its position + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + //This needs to be actually done for all browsers, since pageX/pageY includes this information + //Ugly IE fix + if((this.offsetParent[0] === document.body) || + (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.element.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.element.css("marginLeft"),10) || 0), + top: (parseInt(this.element.css("marginTop"),10) || 0), + right: (parseInt(this.element.css("marginRight"),10) || 0), + bottom: (parseInt(this.element.css("marginBottom"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var over, c, ce, + o = this.options; + + if ( !o.containment ) { + this.containment = null; + return; + } + + if ( o.containment === "window" ) { + this.containment = [ + $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, + $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, + $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, + $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment === "document") { + this.containment = [ + 0, + 0, + $( document ).width() - this.helperProportions.width - this.margins.left, + ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top + ]; + return; + } + + if ( o.containment.constructor === Array ) { + this.containment = o.containment; + return; + } + + if ( o.containment === "parent" ) { + o.containment = this.helper[ 0 ].parentNode; + } + + c = $( o.containment ); + ce = c[ 0 ]; + + if( !ce ) { + return; + } + + over = c.css( "overflow" ) !== "hidden"; + + this.containment = [ + ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), + ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom + ]; + this.relative_container = c; + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) + ) + }; + + }, + + _generatePosition: function(event) { + + var containment, co, top, left, + o = this.options, + scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, + pageX = event.pageX, + pageY = event.pageY; + + //Cache the scroll + if (!this.offset.scroll) { + this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + // If we are not dragging yet, we won't check for options + if ( this.originalPosition ) { + if ( this.containment ) { + if ( this.relative_container ){ + co = this.relative_container.offset(); + containment = [ + this.containment[ 0 ] + co.left, + this.containment[ 1 ] + co.top, + this.containment[ 2 ] + co.left, + this.containment[ 3 ] + co.top + ]; + } + else { + containment = this.containment; + } + + if(event.pageX - this.offset.click.left < containment[0]) { + pageX = containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < containment[1]) { + pageY = containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > containment[2]) { + pageX = containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > containment[3]) { + pageY = containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) + top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; + pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; + pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) + ) + }; + + }, + + _clear: function() { + this.helper.removeClass("ui-draggable-dragging"); + if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { + this.helper.remove(); + } + this.helper = null; + this.cancelHelperRemoval = false; + }, + + // From now on bulk stuff - mainly helpers + + _trigger: function(type, event, ui) { + ui = ui || this._uiHash(); + $.ui.plugin.call(this, type, [event, ui]); + //The absolute position has to be recalculated after plugins + if(type === "drag") { + this.positionAbs = this._convertPositionTo("absolute"); + } + return $.Widget.prototype._trigger.call(this, type, event, ui); + }, + + plugins: {}, + + _uiHash: function() { + return { + helper: this.helper, + position: this.position, + originalPosition: this.originalPosition, + offset: this.positionAbs + }; + } + +}); + +$.ui.plugin.add("draggable", "connectToSortable", { + start: function(event, ui) { + + var inst = $(this).data("ui-draggable"), o = inst.options, + uiSortable = $.extend({}, ui, { item: inst.element }); + inst.sortables = []; + $(o.connectToSortable).each(function() { + var sortable = $.data(this, "ui-sortable"); + if (sortable && !sortable.options.disabled) { + inst.sortables.push({ + instance: sortable, + shouldRevert: sortable.options.revert + }); + sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). + sortable._trigger("activate", event, uiSortable); + } + }); + + }, + stop: function(event, ui) { + + //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper + var inst = $(this).data("ui-draggable"), + uiSortable = $.extend({}, ui, { item: inst.element }); + + $.each(inst.sortables, function() { + if(this.instance.isOver) { + + this.instance.isOver = 0; + + inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance + this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) + + //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" + if(this.shouldRevert) { + this.instance.options.revert = this.shouldRevert; + } + + //Trigger the stop of the sortable + this.instance._mouseStop(event); + + this.instance.options.helper = this.instance.options._helper; + + //If the helper has been the original item, restore properties in the sortable + if(inst.options.helper === "original") { + this.instance.currentItem.css({ top: "auto", left: "auto" }); + } + + } else { + this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance + this.instance._trigger("deactivate", event, uiSortable); + } + + }); + + }, + drag: function(event, ui) { + + var inst = $(this).data("ui-draggable"), that = this; + + $.each(inst.sortables, function() { + + var innermostIntersecting = false, + thisSortable = this; + + //Copy over some variables to allow calling the sortable's native _intersectsWith + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + + if(this.instance._intersectsWith(this.instance.containerCache)) { + innermostIntersecting = true; + $.each(inst.sortables, function () { + this.instance.positionAbs = inst.positionAbs; + this.instance.helperProportions = inst.helperProportions; + this.instance.offset.click = inst.offset.click; + if (this !== thisSortable && + this.instance._intersectsWith(this.instance.containerCache) && + $.contains(thisSortable.instance.element[0], this.instance.element[0]) + ) { + innermostIntersecting = false; + } + return innermostIntersecting; + }); + } + + + if(innermostIntersecting) { + //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once + if(!this.instance.isOver) { + + this.instance.isOver = 1; + //Now we fake the start of dragging for the sortable instance, + //by cloning the list group item, appending it to the sortable and using it as inst.currentItem + //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) + this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); + this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it + this.instance.options.helper = function() { return ui.helper[0]; }; + + event.target = this.instance.currentItem[0]; + this.instance._mouseCapture(event, true); + this.instance._mouseStart(event, true, true); + + //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes + this.instance.offset.click.top = inst.offset.click.top; + this.instance.offset.click.left = inst.offset.click.left; + this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; + this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; + + inst._trigger("toSortable", event); + inst.dropped = this.instance.element; //draggable revert needs that + //hack so receive/update callbacks work (mostly) + inst.currentItem = inst.element; + this.instance.fromOutside = inst; + + } + + //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable + if(this.instance.currentItem) { + this.instance._mouseDrag(event); + } + + } else { + + //If it doesn't intersect with the sortable, and it intersected before, + //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval + if(this.instance.isOver) { + + this.instance.isOver = 0; + this.instance.cancelHelperRemoval = true; + + //Prevent reverting on this forced stop + this.instance.options.revert = false; + + // The out event needs to be triggered independently + this.instance._trigger("out", event, this.instance._uiHash(this.instance)); + + this.instance._mouseStop(event, true); + this.instance.options.helper = this.instance.options._helper; + + //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size + this.instance.currentItem.remove(); + if(this.instance.placeholder) { + this.instance.placeholder.remove(); + } + + inst._trigger("fromSortable", event); + inst.dropped = false; //draggable revert needs that + } + + } + + }); + + } +}); + +$.ui.plugin.add("draggable", "cursor", { + start: function() { + var t = $("body"), o = $(this).data("ui-draggable").options; + if (t.css("cursor")) { + o._cursor = t.css("cursor"); + } + t.css("cursor", o.cursor); + }, + stop: function() { + var o = $(this).data("ui-draggable").options; + if (o._cursor) { + $("body").css("cursor", o._cursor); + } + } +}); + +$.ui.plugin.add("draggable", "opacity", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("opacity")) { + o._opacity = t.css("opacity"); + } + t.css("opacity", o.opacity); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._opacity) { + $(ui.helper).css("opacity", o._opacity); + } + } +}); + +$.ui.plugin.add("draggable", "scroll", { + start: function() { + var i = $(this).data("ui-draggable"); + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + i.overflowOffset = i.scrollParent.offset(); + } + }, + drag: function( event ) { + + var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; + + if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { + + if(!o.axis || o.axis !== "x") { + if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { + i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; + } + } + + if(!o.axis || o.axis !== "y") { + if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { + i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; + } + } + + } else { + + if(!o.axis || o.axis !== "x") { + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + } + + if(!o.axis || o.axis !== "y") { + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(i, event); + } + + } +}); + +$.ui.plugin.add("draggable", "snap", { + start: function() { + + var i = $(this).data("ui-draggable"), + o = i.options; + + i.snapElements = []; + + $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { + var $t = $(this), + $o = $t.offset(); + if(this !== i.element[0]) { + i.snapElements.push({ + item: this, + width: $t.outerWidth(), height: $t.outerHeight(), + top: $o.top, left: $o.left + }); + } + }); + + }, + drag: function(event, ui) { + + var ts, bs, ls, rs, l, r, t, b, i, first, + inst = $(this).data("ui-draggable"), + o = inst.options, + d = o.snapTolerance, + x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, + y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; + + for (i = inst.snapElements.length - 1; i >= 0; i--){ + + l = inst.snapElements[i].left; + r = l + inst.snapElements[i].width; + t = inst.snapElements[i].top; + b = t + inst.snapElements[i].height; + + if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { + if(inst.snapElements[i].snapping) { + (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = false; + continue; + } + + if(o.snapMode !== "inner") { + ts = Math.abs(t - y2) <= d; + bs = Math.abs(b - y1) <= d; + ls = Math.abs(l - x2) <= d; + rs = Math.abs(r - x1) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; + } + } + + first = (ts || bs || ls || rs); + + if(o.snapMode !== "outer") { + ts = Math.abs(t - y1) <= d; + bs = Math.abs(b - y2) <= d; + ls = Math.abs(l - x1) <= d; + rs = Math.abs(r - x2) <= d; + if(ts) { + ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; + } + if(bs) { + ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; + } + if(ls) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; + } + if(rs) { + ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; + } + } + + if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { + (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); + } + inst.snapElements[i].snapping = (ts || bs || ls || rs || first); + + } + + } +}); + +$.ui.plugin.add("draggable", "stack", { + start: function() { + var min, + o = this.data("ui-draggable").options, + group = $.makeArray($(o.stack)).sort(function(a,b) { + return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); + }); + + if (!group.length) { return; } + + min = parseInt($(group[0]).css("zIndex"), 10) || 0; + $(group).each(function(i) { + $(this).css("zIndex", min + i); + }); + this.css("zIndex", (min + group.length)); + } +}); + +$.ui.plugin.add("draggable", "zIndex", { + start: function(event, ui) { + var t = $(ui.helper), o = $(this).data("ui-draggable").options; + if(t.css("zIndex")) { + o._zIndex = t.css("zIndex"); + } + t.css("zIndex", o.zIndex); + }, + stop: function(event, ui) { + var o = $(this).data("ui-draggable").options; + if(o._zIndex) { + $(ui.helper).css("zIndex", o._zIndex); + } + } +}); + +})(jQuery); +(function( $, undefined ) { + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +$.widget("ui.droppable", { + version: "1.10.4", + widgetEventPrefix: "drop", + options: { + accept: "*", + activeClass: false, + addClasses: true, + greedy: false, + hoverClass: false, + scope: "default", + tolerance: "intersect", + + // callbacks + activate: null, + deactivate: null, + drop: null, + out: null, + over: null + }, + _create: function() { + + var proportions, + o = this.options, + accept = o.accept; + + this.isover = false; + this.isout = true; + + this.accept = $.isFunction(accept) ? accept : function(d) { + return d.is(accept); + }; + + this.proportions = function( /* valueToWrite */ ) { + if ( arguments.length ) { + // Store the droppable's proportions + proportions = arguments[ 0 ]; + } else { + // Retrieve or derive the droppable's proportions + return proportions ? + proportions : + proportions = { + width: this.element[ 0 ].offsetWidth, + height: this.element[ 0 ].offsetHeight + }; + } + }; + + // Add the reference and positions to the manager + $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; + $.ui.ddmanager.droppables[o.scope].push(this); + + (o.addClasses && this.element.addClass("ui-droppable")); + + }, + + _destroy: function() { + var i = 0, + drop = $.ui.ddmanager.droppables[this.options.scope]; + + for ( ; i < drop.length; i++ ) { + if ( drop[i] === this ) { + drop.splice(i, 1); + } + } + + this.element.removeClass("ui-droppable ui-droppable-disabled"); + }, + + _setOption: function(key, value) { + + if(key === "accept") { + this.accept = $.isFunction(value) ? value : function(d) { + return d.is(value); + }; + } + $.Widget.prototype._setOption.apply(this, arguments); + }, + + _activate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.addClass(this.options.activeClass); + } + if(draggable){ + this._trigger("activate", event, this.ui(draggable)); + } + }, + + _deactivate: function(event) { + var draggable = $.ui.ddmanager.current; + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(draggable){ + this._trigger("deactivate", event, this.ui(draggable)); + } + }, + + _over: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.addClass(this.options.hoverClass); + } + this._trigger("over", event, this.ui(draggable)); + } + + }, + + _out: function(event) { + + var draggable = $.ui.ddmanager.current; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return; + } + + if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("out", event, this.ui(draggable)); + } + + }, + + _drop: function(event,custom) { + + var draggable = custom || $.ui.ddmanager.current, + childrenIntersection = false; + + // Bail if draggable and droppable are same element + if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { + return false; + } + + this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { + var inst = $.data(this, "ui-droppable"); + if( + inst.options.greedy && + !inst.options.disabled && + inst.options.scope === draggable.options.scope && + inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && + $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) + ) { childrenIntersection = true; return false; } + }); + if(childrenIntersection) { + return false; + } + + if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + if(this.options.activeClass) { + this.element.removeClass(this.options.activeClass); + } + if(this.options.hoverClass) { + this.element.removeClass(this.options.hoverClass); + } + this._trigger("drop", event, this.ui(draggable)); + return this.element; + } + + return false; + + }, + + ui: function(c) { + return { + draggable: (c.currentItem || c.element), + helper: c.helper, + position: c.position, + offset: c.positionAbs + }; + } + +}); + +$.ui.intersect = function(draggable, droppable, toleranceMode) { + + if (!droppable.offset) { + return false; + } + + var draggableLeft, draggableTop, + x1 = (draggable.positionAbs || draggable.position.absolute).left, + y1 = (draggable.positionAbs || draggable.position.absolute).top, + x2 = x1 + draggable.helperProportions.width, + y2 = y1 + draggable.helperProportions.height, + l = droppable.offset.left, + t = droppable.offset.top, + r = l + droppable.proportions().width, + b = t + droppable.proportions().height; + + switch (toleranceMode) { + case "fit": + return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); + case "intersect": + return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half + x2 - (draggable.helperProportions.width / 2) < r && // Left Half + t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half + y2 - (draggable.helperProportions.height / 2) < b ); // Top Half + case "pointer": + draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); + draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); + return isOverAxis( draggableTop, t, droppable.proportions().height ) && isOverAxis( draggableLeft, l, droppable.proportions().width ); + case "touch": + return ( + (y1 >= t && y1 <= b) || // Top edge touching + (y2 >= t && y2 <= b) || // Bottom edge touching + (y1 < t && y2 > b) // Surrounded vertically + ) && ( + (x1 >= l && x1 <= r) || // Left edge touching + (x2 >= l && x2 <= r) || // Right edge touching + (x1 < l && x2 > r) // Surrounded horizontally + ); + default: + return false; + } + +}; + +/* + This manager tracks offsets of draggables and droppables +*/ +$.ui.ddmanager = { + current: null, + droppables: { "default": [] }, + prepareOffsets: function(t, event) { + + var i, j, + m = $.ui.ddmanager.droppables[t.options.scope] || [], + type = event ? event.type : null, // workaround for #2317 + list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); + + droppablesLoop: for (i = 0; i < m.length; i++) { + + //No disabled and non-accepted + if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { + continue; + } + + // Filter out elements in the current dragged item + for (j=0; j < list.length; j++) { + if(list[j] === m[i].element[0]) { + m[i].proportions().height = 0; + continue droppablesLoop; + } + } + + m[i].visible = m[i].element.css("display") !== "none"; + if(!m[i].visible) { + continue; + } + + //Activate the droppable if used directly from draggables + if(type === "mousedown") { + m[i]._activate.call(m[i], event); + } + + m[ i ].offset = m[ i ].element.offset(); + m[ i ].proportions({ width: m[ i ].element[ 0 ].offsetWidth, height: m[ i ].element[ 0 ].offsetHeight }); + + } + + }, + drop: function(draggable, event) { + + var dropped = false; + // Create a copy of the droppables in case the list changes during the drop (#9116) + $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { + + if(!this.options) { + return; + } + if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { + dropped = this._drop.call(this, event) || dropped; + } + + if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { + this.isout = true; + this.isover = false; + this._deactivate.call(this, event); + } + + }); + return dropped; + + }, + dragStart: function( draggable, event ) { + //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) + draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + }); + }, + drag: function(draggable, event) { + + //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. + if(draggable.options.refreshPositions) { + $.ui.ddmanager.prepareOffsets(draggable, event); + } + + //Run through all droppables and check their positions based on specific tolerance options + $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { + + if(this.options.disabled || this.greedyChild || !this.visible) { + return; + } + + var parentInstance, scope, parent, + intersects = $.ui.intersect(draggable, this, this.options.tolerance), + c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); + if(!c) { + return; + } + + if (this.options.greedy) { + // find droppable parents with same scope + scope = this.options.scope; + parent = this.element.parents(":data(ui-droppable)").filter(function () { + return $.data(this, "ui-droppable").options.scope === scope; + }); + + if (parent.length) { + parentInstance = $.data(parent[0], "ui-droppable"); + parentInstance.greedyChild = (c === "isover"); + } + } + + // we just moved into a greedy child + if (parentInstance && c === "isover") { + parentInstance.isover = false; + parentInstance.isout = true; + parentInstance._out.call(parentInstance, event); + } + + this[c] = true; + this[c === "isout" ? "isover" : "isout"] = false; + this[c === "isover" ? "_over" : "_out"].call(this, event); + + // we just moved out of a greedy child + if (parentInstance && c === "isout") { + parentInstance.isout = false; + parentInstance.isover = true; + parentInstance._over.call(parentInstance, event); + } + }); + + }, + dragStop: function( draggable, event ) { + draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); + //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) + if( !draggable.options.refreshPositions ) { + $.ui.ddmanager.prepareOffsets( draggable, event ); + } + } +}; + +})(jQuery); +(function( $, undefined ) { + +function isOverAxis( x, reference, size ) { + return ( x > reference ) && ( x < ( reference + size ) ); +} + +function isFloating(item) { + return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); +} + +$.widget("ui.sortable", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "sort", + ready: false, + options: { + appendTo: "parent", + axis: false, + connectWith: false, + containment: false, + cursor: "auto", + cursorAt: false, + dropOnEmpty: true, + forcePlaceholderSize: false, + forceHelperSize: false, + grid: false, + handle: false, + helper: "original", + items: "> *", + opacity: false, + placeholder: false, + revert: false, + scroll: true, + scrollSensitivity: 20, + scrollSpeed: 20, + scope: "default", + tolerance: "intersect", + zIndex: 1000, + + // callbacks + activate: null, + beforeStop: null, + change: null, + deactivate: null, + out: null, + over: null, + receive: null, + remove: null, + sort: null, + start: null, + stop: null, + update: null + }, + _create: function() { + + var o = this.options; + this.containerCache = {}; + this.element.addClass("ui-sortable"); + + //Get the items + this.refresh(); + + //Let's determine if the items are being displayed horizontally + this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; + + //Let's determine the parent's offset + this.offset = this.element.offset(); + + //Initialize mouse events for interaction + this._mouseInit(); + + //We're ready to go + this.ready = true; + + }, + + _destroy: function() { + this.element + .removeClass("ui-sortable ui-sortable-disabled"); + this._mouseDestroy(); + + for ( var i = this.items.length - 1; i >= 0; i-- ) { + this.items[i].item.removeData(this.widgetName + "-item"); + } + + return this; + }, + + _setOption: function(key, value){ + if ( key === "disabled" ) { + this.options[ key ] = value; + + this.widget().toggleClass( "ui-sortable-disabled", !!value ); + } else { + // Don't call widget base _setOption for disable as it adds ui-state-disabled class + $.Widget.prototype._setOption.apply(this, arguments); + } + }, + + _mouseCapture: function(event, overrideHandle) { + var currentItem = null, + validHandle = false, + that = this; + + if (this.reverting) { + return false; + } + + if(this.options.disabled || this.options.type === "static") { + return false; + } + + //We have to refresh the items data once first + this._refreshItems(event); + + //Find out if the clicked node (or one of its parents) is a actual item in this.items + $(event.target).parents().each(function() { + if($.data(this, that.widgetName + "-item") === that) { + currentItem = $(this); + return false; + } + }); + if($.data(event.target, that.widgetName + "-item") === that) { + currentItem = $(event.target); + } + + if(!currentItem) { + return false; + } + if(this.options.handle && !overrideHandle) { + $(this.options.handle, currentItem).find("*").addBack().each(function() { + if(this === event.target) { + validHandle = true; + } + }); + if(!validHandle) { + return false; + } + } + + this.currentItem = currentItem; + this._removeCurrentsFromItems(); + return true; + + }, + + _mouseStart: function(event, overrideHandle, noActivation) { + + var i, body, + o = this.options; + + this.currentContainer = this; + + //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture + this.refreshPositions(); + + //Create and append the visible helper + this.helper = this._createHelper(event); + + //Cache the helper size + this._cacheHelperProportions(); + + /* + * - Position generation - + * This block generates everything position related - it's the core of draggables. + */ + + //Cache the margins of the original element + this._cacheMargins(); + + //Get the next scrolling parent + this.scrollParent = this.helper.scrollParent(); + + //The element's absolute position on the page minus margins + this.offset = this.currentItem.offset(); + this.offset = { + top: this.offset.top - this.margins.top, + left: this.offset.left - this.margins.left + }; + + $.extend(this.offset, { + click: { //Where the click happened, relative to the element + left: event.pageX - this.offset.left, + top: event.pageY - this.offset.top + }, + parent: this._getParentOffset(), + relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper + }); + + // Only after we got the offset, we can change the helper's position to absolute + // TODO: Still need to figure out a way to make relative sorting possible + this.helper.css("position", "absolute"); + this.cssPosition = this.helper.css("position"); + + //Generate the original position + this.originalPosition = this._generatePosition(event); + this.originalPageX = event.pageX; + this.originalPageY = event.pageY; + + //Adjust the mouse offset relative to the helper if "cursorAt" is supplied + (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); + + //Cache the former DOM position + this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; + + //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way + if(this.helper[0] !== this.currentItem[0]) { + this.currentItem.hide(); + } + + //Create the placeholder + this._createPlaceholder(); + + //Set a containment if given in the options + if(o.containment) { + this._setContainment(); + } + + if( o.cursor && o.cursor !== "auto" ) { // cursor option + body = this.document.find( "body" ); + + // support: IE + this.storedCursor = body.css( "cursor" ); + body.css( "cursor", o.cursor ); + + this.storedStylesheet = $( "" ).appendTo( body ); + } + + if(o.opacity) { // opacity option + if (this.helper.css("opacity")) { + this._storedOpacity = this.helper.css("opacity"); + } + this.helper.css("opacity", o.opacity); + } + + if(o.zIndex) { // zIndex option + if (this.helper.css("zIndex")) { + this._storedZIndex = this.helper.css("zIndex"); + } + this.helper.css("zIndex", o.zIndex); + } + + //Prepare scrolling + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + this.overflowOffset = this.scrollParent.offset(); + } + + //Call callbacks + this._trigger("start", event, this._uiHash()); + + //Recache the helper size + if(!this._preserveHelperProportions) { + this._cacheHelperProportions(); + } + + + //Post "activate" events to possible containers + if( !noActivation ) { + for ( i = this.containers.length - 1; i >= 0; i-- ) { + this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); + } + } + + //Prepare possible droppables + if($.ui.ddmanager) { + $.ui.ddmanager.current = this; + } + + if ($.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + + this.dragging = true; + + this.helper.addClass("ui-sortable-helper"); + this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position + return true; + + }, + + _mouseDrag: function(event) { + var i, item, itemElement, intersection, + o = this.options, + scrolled = false; + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + if (!this.lastPositionAbs) { + this.lastPositionAbs = this.positionAbs; + } + + //Do scrolling + if(this.options.scroll) { + if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { + + if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; + } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + } + + if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; + } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + } + + } else { + + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + } + + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + } + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { + $.ui.ddmanager.prepareOffsets(this, event); + } + } + + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo("absolute"); + + //Set the helper position + if(!this.options.axis || this.options.axis !== "y") { + this.helper[0].style.left = this.position.left+"px"; + } + if(!this.options.axis || this.options.axis !== "x") { + this.helper[0].style.top = this.position.top+"px"; + } + + //Rearrange + for (i = this.items.length - 1; i >= 0; i--) { + + //Cache variables and intersection, continue if no intersection + item = this.items[i]; + itemElement = item.item[0]; + intersection = this._intersectsWithPointer(item); + if (!intersection) { + continue; + } + + // Only put the placeholder inside the current Container, skip all + // items from other containers. This works because when moving + // an item from one container to another the + // currentContainer is switched before the placeholder is moved. + // + // Without this, moving items in "sub-sortables" can cause + // the placeholder to jitter beetween the outer and inner container. + if (item.instance !== this.currentContainer) { + continue; + } + + // cannot intersect with itself + // no useless actions that have been done before + // no action if the item moved is the parent of the item checked + if (itemElement !== this.currentItem[0] && + this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && + !$.contains(this.placeholder[0], itemElement) && + (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) + ) { + + this.direction = intersection === 1 ? "down" : "up"; + + if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { + this._rearrange(event, item); + } else { + break; + } + + this._trigger("change", event, this._uiHash()); + break; + } + } + + //Post events to containers + this._contactContainers(event); + + //Interconnect with droppables + if($.ui.ddmanager) { + $.ui.ddmanager.drag(this, event); + } + + //Call callbacks + this._trigger("sort", event, this._uiHash()); + + this.lastPositionAbs = this.positionAbs; + return false; + + }, + + _mouseStop: function(event, noPropagation) { + + if(!event) { + return; + } + + //If we are using droppables, inform the manager about the drop + if ($.ui.ddmanager && !this.options.dropBehaviour) { + $.ui.ddmanager.drop(this, event); + } + + if(this.options.revert) { + var that = this, + cur = this.placeholder.offset(), + axis = this.options.axis, + animation = {}; + + if ( !axis || axis === "x" ) { + animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); + } + if ( !axis || axis === "y" ) { + animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); + } + this.reverting = true; + $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { + that._clear(event); + }); + } else { + this._clear(event, noPropagation); + } + + return false; + + }, + + cancel: function() { + + if(this.dragging) { + + this._mouseUp({ target: null }); + + if(this.options.helper === "original") { + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + //Post deactivating events to containers + for (var i = this.containers.length - 1; i >= 0; i--){ + this.containers[i]._trigger("deactivate", null, this._uiHash(this)); + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", null, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + if (this.placeholder) { + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + if(this.placeholder[0].parentNode) { + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + } + if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { + this.helper.remove(); + } + + $.extend(this, { + helper: null, + dragging: false, + reverting: false, + _noFinalSort: null + }); + + if(this.domPosition.prev) { + $(this.domPosition.prev).after(this.currentItem); + } else { + $(this.domPosition.parent).prepend(this.currentItem); + } + } + + return this; + + }, + + serialize: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + str = []; + o = o || {}; + + $(items).each(function() { + var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); + if (res) { + str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); + } + }); + + if(!str.length && o.key) { + str.push(o.key + "="); + } + + return str.join("&"); + + }, + + toArray: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + ret = []; + + o = o || {}; + + items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); + return ret; + + }, + + /* Be careful with the following core functions */ + _intersectsWith: function(item) { + + var x1 = this.positionAbs.left, + x2 = x1 + this.helperProportions.width, + y1 = this.positionAbs.top, + y2 = y1 + this.helperProportions.height, + l = item.left, + r = l + item.width, + t = item.top, + b = t + item.height, + dyClick = this.offset.click.top, + dxClick = this.offset.click.left, + isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), + isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), + isOverElement = isOverElementHeight && isOverElementWidth; + + if ( this.options.tolerance === "pointer" || + this.options.forcePointerForContainers || + (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) + ) { + return isOverElement; + } else { + + return (l < x1 + (this.helperProportions.width / 2) && // Right Half + x2 - (this.helperProportions.width / 2) < r && // Left Half + t < y1 + (this.helperProportions.height / 2) && // Bottom Half + y2 - (this.helperProportions.height / 2) < b ); // Top Half + + } + }, + + _intersectsWithPointer: function(item) { + + var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), + isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), + isOverElement = isOverElementHeight && isOverElementWidth, + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (!isOverElement) { + return false; + } + + return this.floating ? + ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) + : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); + + }, + + _intersectsWithSides: function(item) { + + var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), + isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), + verticalDirection = this._getDragVerticalDirection(), + horizontalDirection = this._getDragHorizontalDirection(); + + if (this.floating && horizontalDirection) { + return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); + } else { + return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); + } + + }, + + _getDragVerticalDirection: function() { + var delta = this.positionAbs.top - this.lastPositionAbs.top; + return delta !== 0 && (delta > 0 ? "down" : "up"); + }, + + _getDragHorizontalDirection: function() { + var delta = this.positionAbs.left - this.lastPositionAbs.left; + return delta !== 0 && (delta > 0 ? "right" : "left"); + }, + + refresh: function(event) { + this._refreshItems(event); + this.refreshPositions(); + return this; + }, + + _connectWith: function() { + var options = this.options; + return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; + }, + + _getItemsAsjQuery: function(connected) { + + var i, j, cur, inst, + items = [], + queries = [], + connectWith = this._connectWith(); + + if(connectWith && connected) { + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for ( j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); + } + } + } + } + + queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); + + function addItems() { + items.push( this ); + } + for (i = queries.length - 1; i >= 0; i--){ + queries[i][0].each( addItems ); + } + + return $(items); + + }, + + _removeCurrentsFromItems: function() { + + var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); + + this.items = $.grep(this.items, function (item) { + for (var j=0; j < list.length; j++) { + if(list[j] === item.item[0]) { + return false; + } + } + return true; + }); + + }, + + _refreshItems: function(event) { + + this.items = []; + this.containers = [this]; + + var i, j, cur, inst, targetData, _queries, item, queriesLength, + items = this.items, + queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], + connectWith = this._connectWith(); + + if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down + for (i = connectWith.length - 1; i >= 0; i--){ + cur = $(connectWith[i]); + for (j = cur.length - 1; j >= 0; j--){ + inst = $.data(cur[j], this.widgetFullName); + if(inst && inst !== this && !inst.options.disabled) { + queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); + this.containers.push(inst); + } + } + } + } + + for (i = queries.length - 1; i >= 0; i--) { + targetData = queries[i][1]; + _queries = queries[i][0]; + + for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { + item = $(_queries[j]); + + item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) + + items.push({ + item: item, + instance: targetData, + width: 0, height: 0, + left: 0, top: 0 + }); + } + } + + }, + + refreshPositions: function(fast) { + + //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change + if(this.offsetParent && this.helper) { + this.offset.parent = this._getParentOffset(); + } + + var i, item, t, p; + + for (i = this.items.length - 1; i >= 0; i--){ + item = this.items[i]; + + //We ignore calculating positions of all connected containers when we're not over them + if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { + continue; + } + + t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; + + if (!fast) { + item.width = t.outerWidth(); + item.height = t.outerHeight(); + } + + p = t.offset(); + item.left = p.left; + item.top = p.top; + } + + if(this.options.custom && this.options.custom.refreshContainers) { + this.options.custom.refreshContainers.call(this); + } else { + for (i = this.containers.length - 1; i >= 0; i--){ + p = this.containers[i].element.offset(); + this.containers[i].containerCache.left = p.left; + this.containers[i].containerCache.top = p.top; + this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); + this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); + } + } + + return this; + }, + + _createPlaceholder: function(that) { + that = that || this; + var className, + o = that.options; + + if(!o.placeholder || o.placeholder.constructor === String) { + className = o.placeholder; + o.placeholder = { + element: function() { + + var nodeName = that.currentItem[0].nodeName.toLowerCase(), + element = $( "<" + nodeName + ">", that.document[0] ) + .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") + .removeClass("ui-sortable-helper"); + + if ( nodeName === "tr" ) { + that.currentItem.children().each(function() { + $( " ", that.document[0] ) + .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) + .appendTo( element ); + }); + } else if ( nodeName === "img" ) { + element.attr( "src", that.currentItem.attr( "src" ) ); + } + + if ( !className ) { + element.css( "visibility", "hidden" ); + } + + return element; + }, + update: function(container, p) { + + // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that + // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified + if(className && !o.forcePlaceholderSize) { + return; + } + + //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item + if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } + if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } + } + }; + } + + //Create the placeholder + that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); + + //Append it after the actual current item + that.currentItem.after(that.placeholder); + + //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) + o.placeholder.update(that, that.placeholder); + + }, + + _contactContainers: function(event) { + var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, + innermostContainer = null, + innermostIndex = null; + + // get innermost container that intersects with item + for (i = this.containers.length - 1; i >= 0; i--) { + + // never consider a container that's located within the item itself + if($.contains(this.currentItem[0], this.containers[i].element[0])) { + continue; + } + + if(this._intersectsWith(this.containers[i].containerCache)) { + + // if we've already found a container and it's more "inner" than this, then continue + if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { + continue; + } + + innermostContainer = this.containers[i]; + innermostIndex = i; + + } else { + // container doesn't intersect. trigger "out" event if necessary + if(this.containers[i].containerCache.over) { + this.containers[i]._trigger("out", event, this._uiHash(this)); + this.containers[i].containerCache.over = 0; + } + } + + } + + // if no intersecting containers found, return + if(!innermostContainer) { + return; + } + + // move the item into the container if it's not there already + if(this.containers.length === 1) { + if (!this.containers[innermostIndex].containerCache.over) { + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + } else { + + //When entering a new container, we will find the item with the least distance and append our item near it + dist = 10000; + itemWithLeastDistance = null; + floating = innermostContainer.floating || isFloating(this.currentItem); + posProperty = floating ? "left" : "top"; + sizeProperty = floating ? "width" : "height"; + base = this.positionAbs[posProperty] + this.offset.click[posProperty]; + for (j = this.items.length - 1; j >= 0; j--) { + if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { + continue; + } + if(this.items[j].item[0] === this.currentItem[0]) { + continue; + } + if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { + continue; + } + cur = this.items[j].item.offset()[posProperty]; + nearBottom = false; + if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ + nearBottom = true; + cur += this.items[j][sizeProperty]; + } + + if(Math.abs(cur - base) < dist) { + dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; + this.direction = nearBottom ? "up": "down"; + } + } + + //Check if dropOnEmpty is enabled + if(!itemWithLeastDistance && !this.options.dropOnEmpty) { + return; + } + + if(this.currentContainer === this.containers[innermostIndex]) { + return; + } + + itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); + this._trigger("change", event, this._uiHash()); + this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); + this.currentContainer = this.containers[innermostIndex]; + + //Update the placeholder + this.options.placeholder.update(this.currentContainer, this.placeholder); + + this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); + this.containers[innermostIndex].containerCache.over = 1; + } + + + }, + + _createHelper: function(event) { + + var o = this.options, + helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); + + //Add the helper to the DOM if that didn't happen already + if(!helper.parents("body").length) { + $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); + } + + if(helper[0] === this.currentItem[0]) { + this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; + } + + if(!helper[0].style.width || o.forceHelperSize) { + helper.width(this.currentItem.width()); + } + if(!helper[0].style.height || o.forceHelperSize) { + helper.height(this.currentItem.height()); + } + + return helper; + + }, + + _adjustOffsetFromHelper: function(obj) { + if (typeof obj === "string") { + obj = obj.split(" "); + } + if ($.isArray(obj)) { + obj = {left: +obj[0], top: +obj[1] || 0}; + } + if ("left" in obj) { + this.offset.click.left = obj.left + this.margins.left; + } + if ("right" in obj) { + this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; + } + if ("top" in obj) { + this.offset.click.top = obj.top + this.margins.top; + } + if ("bottom" in obj) { + this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; + } + }, + + _getParentOffset: function() { + + + //Get the offsetParent and cache its position + this.offsetParent = this.helper.offsetParent(); + var po = this.offsetParent.offset(); + + // This is a special case where we need to modify a offset calculated on start, since the following happened: + // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent + // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that + // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag + if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { + po.left += this.scrollParent.scrollLeft(); + po.top += this.scrollParent.scrollTop(); + } + + // This needs to be actually done for all browsers, since pageX/pageY includes this information + // with an ugly IE fix + if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { + po = { top: 0, left: 0 }; + } + + return { + top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), + left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) + }; + + }, + + _getRelativeOffset: function() { + + if(this.cssPosition === "relative") { + var p = this.currentItem.position(); + return { + top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), + left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() + }; + } else { + return { top: 0, left: 0 }; + } + + }, + + _cacheMargins: function() { + this.margins = { + left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), + top: (parseInt(this.currentItem.css("marginTop"),10) || 0) + }; + }, + + _cacheHelperProportions: function() { + this.helperProportions = { + width: this.helper.outerWidth(), + height: this.helper.outerHeight() + }; + }, + + _setContainment: function() { + + var ce, co, over, + o = this.options; + if(o.containment === "parent") { + o.containment = this.helper[0].parentNode; + } + if(o.containment === "document" || o.containment === "window") { + this.containment = [ + 0 - this.offset.relative.left - this.offset.parent.left, + 0 - this.offset.relative.top - this.offset.parent.top, + $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, + ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top + ]; + } + + if(!(/^(document|window|parent)$/).test(o.containment)) { + ce = $(o.containment)[0]; + co = $(o.containment).offset(); + over = ($(ce).css("overflow") !== "hidden"); + + this.containment = [ + co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, + co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, + co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, + co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top + ]; + } + + }, + + _convertPositionTo: function(d, pos) { + + if(!pos) { + pos = this.position; + } + var mod = d === "absolute" ? 1 : -1, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, + scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + return { + top: ( + pos.top + // The absolute mouse position + this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) + ), + left: ( + pos.left + // The absolute mouse position + this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) + ) + }; + + }, + + _generatePosition: function(event) { + + var top, left, + o = this.options, + pageX = event.pageX, + pageY = event.pageY, + scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); + + // This is another very weird special case that only happens for relative elements: + // 1. If the css position is relative + // 2. and the scroll parent is the document or similar to the offset parent + // we have to refresh the relative offset during the scroll so there are no jumps + if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { + this.offset.relative = this._getRelativeOffset(); + } + + /* + * - Position constraining - + * Constrain the position to a mix of grid, containment. + */ + + if(this.originalPosition) { //If we are not dragging yet, we won't check for options + + if(this.containment) { + if(event.pageX - this.offset.click.left < this.containment[0]) { + pageX = this.containment[0] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top < this.containment[1]) { + pageY = this.containment[1] + this.offset.click.top; + } + if(event.pageX - this.offset.click.left > this.containment[2]) { + pageX = this.containment[2] + this.offset.click.left; + } + if(event.pageY - this.offset.click.top > this.containment[3]) { + pageY = this.containment[3] + this.offset.click.top; + } + } + + if(o.grid) { + top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; + pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; + + left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; + pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; + } + + } + + return { + top: ( + pageY - // The absolute mouse position + this.offset.click.top - // Click offset (relative to the element) + this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.top + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) + ), + left: ( + pageX - // The absolute mouse position + this.offset.click.left - // Click offset (relative to the element) + this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent + this.offset.parent.left + // The offsetParent's offset without borders (offset + border) + ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) + ) + }; + + }, + + _rearrange: function(event, i, a, hardRefresh) { + + a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); + + //Various things done here to improve the performance: + // 1. we create a setTimeout, that calls refreshPositions + // 2. on the instance, we have a counter variable, that get's higher after every append + // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same + // 4. this lets only the last addition to the timeout stack through + this.counter = this.counter ? ++this.counter : 1; + var counter = this.counter; + + this._delay(function() { + if(counter === this.counter) { + this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove + } + }); + + }, + + _clear: function(event, noPropagation) { + + this.reverting = false; + // We delay all events that have to be triggered to after the point where the placeholder has been removed and + // everything else normalized again + var i, + delayedTriggers = []; + + // We first have to update the dom position of the actual currentItem + // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) + if(!this._noFinalSort && this.currentItem.parent().length) { + this.placeholder.before(this.currentItem); + } + this._noFinalSort = null; + + if(this.helper[0] === this.currentItem[0]) { + for(i in this._storedCSS) { + if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { + this._storedCSS[i] = ""; + } + } + this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); + } else { + this.currentItem.show(); + } + + if(this.fromOutside && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); + } + if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { + delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed + } + + // Check if the items Container has Changed and trigger appropriate + // events. + if (this !== this.currentContainer) { + if(!noPropagation) { + delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); + delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); + } + } + + + //Post events to containers + function delayEvent( type, instance, container ) { + return function( event ) { + container._trigger( type, event, instance._uiHash( instance ) ); + }; + } + for (i = this.containers.length - 1; i >= 0; i--){ + if (!noPropagation) { + delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); + } + if(this.containers[i].containerCache.over) { + delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); + this.containers[i].containerCache.over = 0; + } + } + + //Do what was originally in plugins + if ( this.storedCursor ) { + this.document.find( "body" ).css( "cursor", this.storedCursor ); + this.storedStylesheet.remove(); + } + if(this._storedOpacity) { + this.helper.css("opacity", this._storedOpacity); + } + if(this._storedZIndex) { + this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); + } + + this.dragging = false; + if(this.cancelHelperRemoval) { + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return false; + } + + if(!noPropagation) { + this._trigger("beforeStop", event, this._uiHash()); + } + + //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! + this.placeholder[0].parentNode.removeChild(this.placeholder[0]); + + if(this.helper[0] !== this.currentItem[0]) { + this.helper.remove(); + } + this.helper = null; + + if(!noPropagation) { + for (i=0; i < delayedTriggers.length; i++) { + delayedTriggers[i].call(this, event); + } //Trigger all delayed events + this._trigger("stop", event, this._uiHash()); + } + + this.fromOutside = false; + return true; + + }, + + _trigger: function() { + if ($.Widget.prototype._trigger.apply(this, arguments) === false) { + this.cancel(); + } + }, + + _uiHash: function(_inst) { + var inst = _inst || this; + return { + helper: inst.helper, + placeholder: inst.placeholder || $([]), + position: inst.position, + originalPosition: inst.originalPosition, + offset: inst.positionAbs, + item: inst.currentItem, + sender: _inst ? _inst.element : null + }; + } + +}); + +})(jQuery); +(function( $, undefined ) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) +var numPages = 5; + +$.widget( "ui.slider", $.ui.mouse, { + version: "1.10.4", + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null, + + // callbacks + change: null, + slide: null, + start: null, + stop: null + }, + + _create: function() { + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass( "ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all"); + + this._refresh(); + this._setOption( "disabled", this.options.disabled ); + + this._animateOff = false; + }, + + _refresh: function() { + this._createRange(); + this._createHandles(); + this._setupEvents(); + this._refreshValue(); + }, + + _createHandles: function() { + var i, handleCount, + options = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), + handle = "
    ", + handles = []; + + handleCount = ( options.values && options.values.length ) || 1; + + if ( existingHandles.length > handleCount ) { + existingHandles.slice( handleCount ).remove(); + existingHandles = existingHandles.slice( 0, handleCount ); + } + + for ( i = existingHandles.length; i < handleCount; i++ ) { + handles.push( handle ); + } + + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); + + this.handle = this.handles.eq( 0 ); + + this.handles.each(function( i ) { + $( this ).data( "ui-slider-handle-index", i ); + }); + }, + + _createRange: function() { + var options = this.options, + classes = ""; + + if ( options.range ) { + if ( options.range === true ) { + if ( !options.values ) { + options.values = [ this._valueMin(), this._valueMin() ]; + } else if ( options.values.length && options.values.length !== 2 ) { + options.values = [ options.values[0], options.values[0] ]; + } else if ( $.isArray( options.values ) ) { + options.values = options.values.slice(0); + } + } + + if ( !this.range || !this.range.length ) { + this.range = $( "
    " ) + .appendTo( this.element ); + + classes = "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header ui-corner-all"; + } else { + this.range.removeClass( "ui-slider-range-min ui-slider-range-max" ) + // Handle range switching from true to min/max + .css({ + "left": "", + "bottom": "" + }); + } + + this.range.addClass( classes + + ( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) ); + } else { + if ( this.range ) { + this.range.remove(); + } + this.range = null; + } + }, + + _setupEvents: function() { + var elements = this.handles.add( this.range ).filter( "a" ); + this._off( elements ); + this._on( elements, this._handleEvents ); + this._hoverable( elements ); + this._focusable( elements ); + }, + + _destroy: function() { + this.handles.remove(); + if ( this.range ) { + this.range.remove(); + } + + this.element + .removeClass( "ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" ); + + this._mouseDestroy(); + }, + + _mouseCapture: function( event ) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each(function( i ) { + var thisDistance = Math.abs( normValue - that.values(i) ); + if (( distance > thisDistance ) || + ( distance === thisDistance && + (i === that._lastChangedValue || that.values(i) === o.min ))) { + distance = thisDistance; + closestHandle = $( this ); + index = i; + } + }); + + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; + } + this._mouseSliding = true; + + this._handleIndex = index; + + closestHandle + .addClass( "ui-state-active" ) + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - + ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + + ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) + }; + + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function() { + return true; + }, + + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); + + this._slide( event, this._handleIndex, normValue ); + + return false; + }, + + _mouseStop: function( event ) { + this.handles.removeClass( "ui-state-active" ); + this._mouseSliding = false; + + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); + } + + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; + } + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue( valueMouse ); + }, + + _start: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + return this._trigger( "start", event, uiHash ); + }, + + _slide: function( event, index, newVal ) { + var otherVal, + newValues, + allowed; + + if ( this.options.values && this.options.values.length ) { + otherVal = this.values( index ? 0 : 1 ); + + if ( ( this.options.values.length === 2 && this.options.range === true ) && + ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) + ) { + newVal = otherVal; + } + + if ( newVal !== this.values( index ) ) { + newValues = this.values(); + newValues[ index ] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal, + values: newValues + } ); + otherVal = this.values( index ? 0 : 1 ); + if ( allowed !== false ) { + this.values( index, newVal ); + } + } + } else { + if ( newVal !== this.value() ) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal + } ); + if ( allowed !== false ) { + this.value( newVal ); + } + } + } + }, + + _stop: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "stop", event, uiHash ); + }, + + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + //store the last changed value index for reference when handles overlap + this._lastChangedValue = index; + + this._trigger( "change", event, uiHash ); + } + }, + + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; + } + + return this._value(); + }, + + values: function( index, newValue ) { + var vals, + newValues, + i; + + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } + + if ( arguments.length ) { + if ( $.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this.options.values && this.options.values.length ) { + return this._values( index ); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function( key, value ) { + var i, + valsLength = 0; + + if ( key === "range" && this.options.range === true ) { + if ( value === "min" ) { + this.options.value = this._values( 0 ); + this.options.values = null; + } else if ( value === "max" ) { + this.options.value = this._values( this.options.values.length-1 ); + this.options.values = null; + } + } + + if ( $.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + + switch ( key ) { + case "orientation": + this._detectOrientation(); + this.element + .removeClass( "ui-slider-horizontal ui-slider-vertical" ) + .addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for ( i = 0; i < valsLength; i += 1 ) { + this._change( null, i ); + } + this._animateOff = false; + break; + case "min": + case "max": + this._animateOff = true; + this._refreshValue(); + this._animateOff = false; + break; + case "range": + this._animateOff = true; + this._refresh(); + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; + + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); + + return val; + } else if ( this.options.values && this.options.values.length ) { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i+= 1) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); + } + + return vals; + } else { + return []; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if ( Math.abs(valModStep) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed(5) ); + }, + + _valueMin: function() { + return this.options.min; + }, + + _valueMax: function() { + return this.options.max; + }, + + _refreshValue: function() { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = ( !this._animateOff ) ? o.animate : false, + _set = {}; + + if ( this.options.values && this.options.values.length ) { + this.handles.each(function( i ) { + valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; + _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( that.options.range === true ) { + if ( that.orientation === "horizontal" ) { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } else { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + }, + + _handleEvents: { + keydown: function( event ) { + var allowed, curVal, newVal, step, + index = $( event.target ).data( "ui-slider-handle-index" ); + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !this._keySliding ) { + this._keySliding = true; + $( event.target ).addClass( "ui-state-active" ); + allowed = this._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } + + step = this.options.step; + if ( this.options.values && this.options.values.length ) { + curVal = newVal = this.values( index ); + } else { + curVal = newVal = this.value(); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === this._valueMax() ) { + return; + } + newVal = this._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === this._valueMin() ) { + return; + } + newVal = this._trimAlignValue( curVal - step ); + break; + } + + this._slide( event, index, newVal ); + }, + click: function( event ) { + event.preventDefault(); + }, + keyup: function( event ) { + var index = $( event.target ).data( "ui-slider-handle-index" ); + + if ( this._keySliding ) { + this._keySliding = false; + this._stop( event, index ); + this._change( event, index ); + $( event.target ).removeClass( "ui-state-active" ); + } + } + } + +}); + +}(jQuery)); diff --git a/src/UI/Quality/QualityDefinitionCollection.js b/src/UI/Quality/QualityDefinitionCollection.js new file mode 100644 index 000000000..8da78717b --- /dev/null +++ b/src/UI/Quality/QualityDefinitionCollection.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'backbone', + 'Quality/QualityDefinitionModel' + ], function (Backbone, QualityDefinitionModel) { + return Backbone.Collection.extend({ + model: QualityDefinitionModel, + url : window.NzbDrone.ApiRoot + '/qualitydefinition' + }); + }); diff --git a/src/UI/Quality/QualitySizeModel.js b/src/UI/Quality/QualityDefinitionModel.js similarity index 71% rename from src/UI/Quality/QualitySizeModel.js rename to src/UI/Quality/QualityDefinitionModel.js index d305969b3..e95c87a95 100644 --- a/src/UI/Quality/QualitySizeModel.js +++ b/src/UI/Quality/QualityDefinitionModel.js @@ -9,10 +9,10 @@ define( baseInitialize: ModelBase.prototype.initialize, initialize: function () { - var name = this.get('name'); + var name = this.get('quality').name; - this.successMessage = 'Saved ' + name + ' size settings'; - this.errorMessage = 'Couldn\'t save ' + name + ' size settings'; + this.successMessage = 'Saved ' + name + ' quality settings'; + this.errorMessage = 'Couldn\'t save ' + name + ' quality settings'; this.baseInitialize.call(this); } diff --git a/src/UI/Quality/QualitySizeCollection.js b/src/UI/Quality/QualitySizeCollection.js deleted file mode 100644 index 92c580647..000000000 --- a/src/UI/Quality/QualitySizeCollection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Quality/QualitySizeModel' - ], function (Backbone, QualitySizeModel) { - return Backbone.Collection.extend({ - model: QualitySizeModel, - url : window.NzbDrone.ApiRoot + '/qualitysize' - }); - }); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html new file mode 100644 index 000000000..bce6ecefa --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html @@ -0,0 +1,16 @@ +
    + Quality Definitions +
    +
    +
    +
    + Quality + Title + Size Limit +
    +
    +
    +
    +
    +
    +
    diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js new file mode 100644 index 000000000..48588bcbf --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js @@ -0,0 +1,17 @@ +'use strict'; + +define( + [ + 'marionette', + 'backgrid', + 'Settings/Quality/Definition/QualityDefinitionView' + ], function (Marionette, Backgrid, QualityDefinitionView) { + + return Marionette.CompositeView.extend({ + template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate', + + itemViewContainer: ".x-rows", + + itemView: QualityDefinitionView + }); + }); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html new file mode 100644 index 000000000..e61558bfc --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html @@ -0,0 +1,31 @@ + + {{quality.name}} + + + + + +
    +
    +
    + + + + +
    +
    + + + + +
    +
    +
    \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js new file mode 100644 index 000000000..9b0465fe9 --- /dev/null +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -0,0 +1,86 @@ +'use strict'; + +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'filesize', + 'jquery-ui' + ], function (Marionette, AsModelBoundView, fileSize) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Quality/Definition/QualityDefinitionTemplate', + className: 'row', + + ui: { + title : '.x-title', + sizeSlider : '.x-slider', + thirtyMinuteMinSize: '.x-min-thirty', + sixtyMinuteMinSize : '.x-min-sixty', + thirtyMinuteMaxSize: '.x-max-thirty', + sixtyMinuteMaxSize : '.x-max-sixty' + }, + + events: { + 'change .x-title': '_updateTitle', + 'slide .x-slider': '_updateSize' + }, + + initialize: function (options) { + this.qualityProfileCollection = options.qualityProfiles; + this.filesize = fileSize; + }, + + onRender: function () { + this.ui.sizeSlider.slider({ + range : true, + min : 0, + max : 200, + values : [ this.model.get('minSize'), this.model.get('maxSize') ], + }); + + this._changeSize(); + }, + + _updateTitle: function() { + this.model.set('title', this.ui.title.val()); + }, + + _updateSize: function (event, ui) { + this.model.set('minSize', ui.values[0]); + this.model.set('maxSize', ui.values[1]); + + this._changeSize(); + }, + + _changeSize: function () { + var minSize = this.model.get('minSize'); + var maxSize = this.model.get('maxSize'); + + { + var minBytes = minSize * 1024 * 1024; + var minThirty = fileSize(minBytes * 30, 1, false); + var minSixty = fileSize(minBytes * 60, 1, false); + + this.ui.thirtyMinuteMinSize.html(minThirty); + this.ui.sixtyMinuteMinSize.html(minSixty); + } + + { + var maxBytes = maxSize * 1024 * 1024; + var maxThirty = fileSize(maxBytes * 30, 1, false); + var maxSixty = fileSize(maxBytes * 60, 1, false); + + this.ui.thirtyMinuteMaxSize.html(maxThirty); + this.ui.sixtyMinuteMaxSize.html(maxSixty); + } + + /*if (parseInt(maxSize, 10) === 0) { + thirty = 'No Limit'; + sixty = 'No Limit'; + }*/ + } + }); + + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js new file mode 100644 index 000000000..e93fa6dee --- /dev/null +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'marionette' + ], function (Marionette) { + return Marionette.ItemView.extend({ + template : 'Settings/Quality/Profile/EditQualityProfileItemViewTemplate' + }); + }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html new file mode 100644 index 000000000..19d710178 --- /dev/null +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html @@ -0,0 +1,5 @@ + + + +{{name}} + diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html index f45c97dda..3a09e8922 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html @@ -29,25 +29,15 @@
    -
    +

    Available

    - +
      +
    -
    -
    -
    -

    Allowed

    - -
    -
    +
    +

    Allowed

    +
      +
    diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 503d820fc..703c6b88c 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -4,63 +4,113 @@ define( 'vent', 'marionette', 'backbone', + 'backbone.collectionview', + 'Settings/Quality/Profile/EditQualityProfileItemView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView', 'underscore' - ], function (vent, Marionette, Backbone, AsModelBoundView, AsValidatedView, _) { + ], function (vent, Marionette, Backbone, BackboneSortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, _) { var view = Marionette.ItemView.extend({ template: 'Settings/Quality/Profile/EditQualityProfileTemplate', ui: { - cutoff: '.x-cutoff' + available: '.x-available-list', + allowed : '.x-allowed-list', + cutoff : '.x-cutoff' }, - + events: { 'click .x-save' : '_saveQualityProfile', - 'dblclick .x-available-list': '_moveQuality', - 'dblclick .x-allowed-list' : '_moveQuality' + //'click .x-qualityitem' : '_moveQuality', }, initialize: function (options) { this.profileCollection = options.profileCollection; + + this.availableCollection = new Backbone.Collection(this.model.get('available')); + this.availableCollection.comparator = function (model) { return -model.get('weight'); }; + this.availableCollection.sort(); + + this.allowedCollection = new Backbone.Collection(this.model.get('allowed').reverse()); + }, + + onRender: function() { + var listViewAvailable = new BackboneSortableCollectionView( { + el : this.ui.available, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : false, + collection: this.availableCollection + }); + listViewAvailable.render(); + + var listViewAllowed = new BackboneSortableCollectionView( { + el : this.ui.allowed, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : true, + sortableOptions : { + handle: ".x-drag-handle" + }, + collection : this.allowedCollection + } ); + listViewAllowed.render(); + + this.listenTo(listViewAvailable, "doubleClick", this._moveQuality); + this.listenTo(listViewAllowed, "doubleClick", this._moveQuality); + this.listenTo(listViewAllowed, "sortStop", this._updateModel); }, _moveQuality: function (event) { var quality; - var qualityId = event.target.value; - var availableCollection = new Backbone.Collection(this.model.get('available')); - availableCollection.comparator = function (model) { - return model.get('weight'); - }; - - var allowedCollection = new Backbone.Collection(this.model.get('allowed')); - allowedCollection.comparator = function (model) { - return model.get('weight'); - }; - - if (availableCollection.get(qualityId)) { - quality = availableCollection.get(qualityId); - availableCollection.remove(quality); - allowedCollection.add(quality); + var qualityId = event.get('id'); + + if (this.availableCollection.get(qualityId)) { + quality = this.availableCollection.get(qualityId); + var idealIndex = 0; + var idealMismatches = 1000; + // Insert it at the best possible spot. + for (var i = 0; i <= this.allowedCollection.length; i++) { + var mismatches = 0; + for (var j = 0; j < i; j++) { + if (this.allowedCollection.at(j).get('weight') < quality.get('weight')) + mismatches++; + } + for (j = i; j < this.allowedCollection.length; j++) { + if (this.allowedCollection.at(j).get('weight') > quality.get('weight')) + mismatches++; + } + if (mismatches <= idealMismatches) { + idealIndex = i; + idealMismatches = mismatches; + } + } + + this.availableCollection.remove(quality); + this.allowedCollection.add(quality, {at: idealIndex}); } - else if (allowedCollection.get(qualityId)) { - quality = allowedCollection.get(qualityId); + else if (this.allowedCollection.get(qualityId)) { + quality = this.allowedCollection.get(qualityId); - allowedCollection.remove(quality); - availableCollection.add(quality); + this.allowedCollection.remove(quality); + this.availableCollection.add(quality); } else { throw 'couldnt find quality id ' + qualityId; } - - this.model.set('available', availableCollection.toJSON()); - this.model.set('allowed', allowedCollection.toJSON()); - + + this._updateModel(); + }, + + _updateModel: function() { + this.model.set('available', this.availableCollection.toJSON().reverse()); + this.model.set('allowed', this.allowedCollection.toJSON().reverse()); + this.render(); }, - + _saveQualityProfile: function () { var self = this; var cutoff = _.findWhere(this.model.get('allowed'), { id: parseInt(this.ui.cutoff.val(), 10)}); diff --git a/src/UI/Settings/Quality/QualityLayout.js b/src/UI/Settings/Quality/QualityLayout.js index 09d4aca44..c80486209 100644 --- a/src/UI/Settings/Quality/QualityLayout.js +++ b/src/UI/Settings/Quality/QualityLayout.js @@ -5,27 +5,27 @@ define( 'marionette', 'Quality/QualityProfileCollection', 'Settings/Quality/Profile/QualityProfileCollectionView', - 'Quality/QualitySizeCollection', - 'Settings/Quality/Size/QualitySizeCollectionView' - ], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualitySizeCollection, QualitySizeCollectionView) { + 'Quality/QualityDefinitionCollection', + 'Settings/Quality/Definition/QualityDefinitionCollectionView' + ], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualityDefinitionCollection, QualityDefinitionCollectionView) { return Marionette.Layout.extend({ template: 'Settings/Quality/QualityLayoutTemplate', regions: { - qualityProfile : '#quality-profile', - qualitySize : '#quality-size' + qualityProfile : '#quality-profile', + qualityDefinition : '#quality-definition' }, initialize: function (options) { this.settings = options.settings; QualityProfileCollection.fetch(); - this.qualitySizeCollection = new QualitySizeCollection(); - this.qualitySizeCollection.fetch(); + this.qualityDefinitionCollection = new QualityDefinitionCollection(); + this.qualityDefinitionCollection.fetch(); }, onShow: function () { this.qualityProfile.show(new QualityProfileCollectionView({collection: QualityProfileCollection})); - this.qualitySize.show(new QualitySizeCollectionView({collection: this.qualitySizeCollection})); + this.qualityDefinition.show(new QualityDefinitionCollectionView({collection: this.qualityDefinitionCollection})); } }); }); diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.html b/src/UI/Settings/Quality/QualityLayoutTemplate.html index 5954e5eb4..9bd521d37 100644 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.html +++ b/src/UI/Settings/Quality/QualityLayoutTemplate.html @@ -5,5 +5,5 @@
    -
    +
    diff --git a/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html b/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html deleted file mode 100644 index 6d406bdc0..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeCollectionTemplate.html +++ /dev/null @@ -1,4 +0,0 @@ -
    - Quality Size Limits -
      -
    diff --git a/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js b/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js deleted file mode 100644 index e27e01a41..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -define(['marionette', 'Settings/Quality/Size/QualitySizeView'], function (Marionette, QualitySizeView) { - return Marionette.CompositeView.extend({ - itemView : QualitySizeView, - itemViewContainer: '.quality-sizes', - template : 'Settings/Quality/Size/QualitySizeCollectionTemplate' - }); -}); diff --git a/src/UI/Settings/Quality/Size/QualitySizeTemplate.html b/src/UI/Settings/Quality/Size/QualitySizeTemplate.html deleted file mode 100644 index 657922e73..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeTemplate.html +++ /dev/null @@ -1,20 +0,0 @@ -
    -

    {{name}}

    -
    -
    -
    - - -
    -
    - - -
    -
    - -
    -
    \ No newline at end of file diff --git a/src/UI/Settings/Quality/Size/QualitySizeView.js b/src/UI/Settings/Quality/Size/QualitySizeView.js deleted file mode 100644 index 215304ba0..000000000 --- a/src/UI/Settings/Quality/Size/QualitySizeView.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'filesize', - 'jquery.knob' - ], function (Marionette, AsModelBoundView, fileSize) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Size/QualitySizeTemplate', - tagName : 'li', - - ui: { - knob : '.x-knob', - thirtyMinuteSize: '.x-size-thirty', - sixtyMinuteSize : '.x-size-sixty' - }, - - events: { - 'change .x-knob': '_changeMaxSize' - }, - - initialize: function (options) { - this.qualityProfileCollection = options.qualityProfiles; - this.filesize = fileSize; - }, - - onRender: function () { - this.ui.knob.knob({ - min : 0, - max : 200, - step : 1, - cursor : 25, - width : 150, - stopper : true, - displayInput: false - }); - - this._changeMaxSize(); - }, - - _changeMaxSize: function () { - var maxSize = this.model.get('maxSize'); - var bytes = maxSize * 1024 * 1024; - var thirty = fileSize(bytes * 30, 1, false); - var sixty = fileSize(bytes * 60, 1, false); - - if (parseInt(maxSize, 10) === 0) { - thirty = 'No Limit'; - sixty = 'No Limit'; - } - - this.ui.thirtyMinuteSize.html(thirty); - this.ui.sixtyMinuteSize.html(sixty); - } - }); - - return AsModelBoundView.call(view); - }); diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 95d9f3a4f..60820eeca 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -1,6 +1,7 @@ @import "../../Shared/Styles/card"; +@import "../../Content/Bootstrap/mixins"; -.quality-profiles, .quality-sizes { +.quality-profiles { li { display: inline-block; vertical-align: top; @@ -35,41 +36,121 @@ } } -.quality-size-item { +ul.x-available-list, ul.x-allowed-list { + min-height: 100px; - .card; - text-align: center; - - width: 200px; - height: 210px; - padding: 10px 15px; - - h3 { - margin-top: 0px; - } - - .size { - position: relative; - height: 100px; - margin: 10px; - text-align: center; - } - - .knob { - box-shadow: none; - } - - .size-value-wrapper { - position: absolute; - top: 50px; - width: 100%; - - div { - margin-top: 2px; + .user-select(none); + + margin: 0; + padding: 0; + list-style-type: none; + outline: none; + cursor: pointer; + + li { + margin: 2px; + padding: 2px; + line-height: 20px; + border: 1px solid #AAA; + border-radius: 4px; /* may need vendor varients */ + background: #FAFAFA; + + &:hover { + border-color: #888; + background: #EEE; + } + + .x-drag-handle, .x-moveleft-handle, .x-moveright-handle { + opacity: 0.0; + line-height: 20px; } } } -#quality-size { - overflow: hidden; +ul.x-available-list li { + .x-moveright-handle { + opacity: 0.2; + } + + .x-drag-handle { + display: none; + } + + &:hover .x-moveright-handle { + opacity: 1.0; + } } + +ul.x-allowed-list li { + .x-drag-handle, .x-moveleft-handle { + opacity: 0.2; + } + + .x-drag-handle:hover { + opacity: 1.0; + cursor: pointer; + } + + &:hover .x-moveleft-handle { + opacity: 1.0; + } +} + +#quality-definition-list { + + .x-header .row { + font-weight: bold; + line-height: 40px; + } + + .x-rows .row { + line-height: 30px; + border-top: 1px solid #ddd; + vertical-align: middle; + padding: 5px; + + input { + margin-bottom: 0px; + } + + .size-label-wrapper { + line-height: 20px; + } + + .label { + min-width: 70px; + text-align: center; + margin: 0px 1px; + padding: 1px 4px; + } + + .ui-slider { + position: relative; + text-align: left; + background-color: #f5f5f5; + border-radius: 3px; + border: 1px solid #ccc; + height: 8px; + + .ui-slider-range { + position: absolute; + display: block; + background-color: #ddd; + height: 100%; + } + + .ui-slider-handle { + position: absolute; + z-index: 2; + width: 6px; + height: 12px; + cursor: default; + background-color: #ccc; + border: 1px solid #aaa; + border-radius: 3px; + top: -3px; + } + } + } +} + diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 88d10205c..71948e76e 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -1,6 +1,7 @@ 'use strict'; define( [ + 'jquery', 'vent', 'marionette', 'backbone', @@ -17,7 +18,8 @@ define( 'Settings/General/GeneralView', 'Shared/LoadingView', 'Config' - ], function (vent, + ], function ($, + vent, Marionette, Backbone, SettingsModel, @@ -196,7 +198,7 @@ define( this.ui.advancedSettings.prop('checked', checked); if (checked) { - this.$el.addClass('show-advanced-settings'); + $('body').addClass('show-advanced-settings'); } }, @@ -205,11 +207,11 @@ define( Config.setValue('advancedSettings', checked); if (checked) { - this.$el.addClass('show-advanced-settings'); + $('body').addClass('show-advanced-settings'); } else { - this.$el.removeClass('show-advanced-settings'); + $('body').removeClass('show-advanced-settings'); } } }); diff --git a/src/UI/app.js b/src/UI/app.js index 736bd875a..d856cb62c 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -2,29 +2,31 @@ require.config({ paths: { - 'backbone' : 'JsLibraries/backbone', - 'moment' : 'JsLibraries/moment', - 'filesize' : 'JsLibraries/filesize', - 'handlebars' : 'JsLibraries/handlebars.runtime', - 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', - 'bootstrap' : 'JsLibraries/bootstrap', - 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', - 'backbone.pageable' : 'JsLibraries/backbone.pageable', - 'backbone.validation' : 'JsLibraries/backbone.validation', - 'backbone.modelbinder': 'JsLibraries/backbone.modelbinder', - 'backgrid' : 'JsLibraries/backbone.backgrid', - 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', - 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', - 'fullcalendar' : 'JsLibraries/fullcalendar', - 'backstrech' : 'JsLibraries/jquery.backstretch', - 'underscore' : 'JsLibraries/lodash.underscore', - 'marionette' : 'JsLibraries/backbone.marionette', - 'signalR' : 'JsLibraries/jquery.signalR', - 'jquery.knob' : 'JsLibraries/jquery.knob', - 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', - 'messenger' : 'JsLibraries/messenger', - 'jquery' : 'JsLibraries/jquery', - 'libs' : 'JsLibraries/', + 'backbone' : 'JsLibraries/backbone', + 'moment' : 'JsLibraries/moment', + 'filesize' : 'JsLibraries/filesize', + 'handlebars' : 'JsLibraries/handlebars.runtime', + 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', + 'bootstrap' : 'JsLibraries/bootstrap', + 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', + 'backbone.pageable' : 'JsLibraries/backbone.pageable', + 'backbone.validation' : 'JsLibraries/backbone.validation', + 'backbone.modelbinder' : 'JsLibraries/backbone.modelbinder', + 'backbone.collectionview' : 'JsLibraries/backbone.collectionview', + 'backgrid' : 'JsLibraries/backbone.backgrid', + 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', + 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', + 'fullcalendar' : 'JsLibraries/fullcalendar', + 'backstrech' : 'JsLibraries/jquery.backstretch', + 'underscore' : 'JsLibraries/lodash.underscore', + 'marionette' : 'JsLibraries/backbone.marionette', + 'signalR' : 'JsLibraries/jquery.signalR', + 'jquery-ui' : 'JsLibraries/jquery-ui', + 'jquery.knob' : 'JsLibraries/jquery.knob', + 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', + 'messenger' : 'JsLibraries/messenger', + 'jquery' : 'JsLibraries/jquery', + 'libs' : 'JsLibraries/', 'api': 'Require/require.api' }, @@ -105,6 +107,12 @@ require.config({ } }, + 'jquery-ui' : { + deps: + [ + 'jquery' + ] + }, 'jquery.knob' : { deps: [ @@ -143,6 +151,14 @@ require.config({ 'backbone' ] }, + 'backbone.collectionview': { + deps: + [ + 'backbone', + 'jquery-ui' + ], + exports: 'Backbone.CollectionView' + }, backgrid : { deps: [ From 8e59843d35e6ce88b16d4aba8c27306f98774dc6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 21:22:45 -0800 Subject: [PATCH 02/48] Fixed styles for quality profile editor, click chevron to move New: Quality in Profile can now be re-ordered (advanced setting) --- .../QualityDefinitionCollectionTemplate.html | 4 +- .../EditQualityProfileItemViewTemplate.html | 7 ++- .../Profile/EditQualityProfileTemplate.html | 4 +- .../Quality/Profile/EditQualityProfileView.js | 50 ++++++++++--------- .../Profile/QualitySortableCollectionView.js | 27 ++++++++++ src/UI/Settings/Quality/quality.less | 36 +++++++------ 6 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html index bce6ecefa..d3287100b 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.html @@ -2,14 +2,14 @@ Quality Definitions
    -
    +
    Quality Title Size Limit
    -
    +
    diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html index 19d710178..520c95246 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html @@ -1,5 +1,4 @@ - - - + {{name}} - + + diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html index 3a09e8922..e0c8a82fe 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html @@ -31,12 +31,12 @@

    Available

    -
      +

    Allowed

    -
      +
    diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 703c6b88c..225fcb844 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -4,7 +4,7 @@ define( 'vent', 'marionette', 'backbone', - 'backbone.collectionview', + 'Settings/Quality/Profile/QualitySortableCollectionView', 'Settings/Quality/Profile/EditQualityProfileItemView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView', @@ -21,8 +21,7 @@ define( }, events: { - 'click .x-save' : '_saveQualityProfile', - //'click .x-qualityitem' : '_moveQuality', + 'click .x-save': '_saveQualityProfile' }, initialize: function (options) { @@ -36,30 +35,35 @@ define( }, onRender: function() { - var listViewAvailable = new BackboneSortableCollectionView( { - el : this.ui.available, - modelView : EditQualityProfileItemView, - selectable: false, - sortable : false, - collection: this.availableCollection + var listViewAvailable = new BackboneSortableCollectionView({ + el : this.ui.available, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : false, + collection: this.availableCollection }); - listViewAvailable.render(); - var listViewAllowed = new BackboneSortableCollectionView( { - el : this.ui.allowed, - modelView : EditQualityProfileItemView, - selectable: false, - sortable : true, - sortableOptions : { - handle: ".x-drag-handle" - }, - collection : this.allowedCollection - } ); + var listViewAllowed = new BackboneSortableCollectionView({ + el : this.ui.allowed, + modelView : EditQualityProfileItemView, + selectable: false, + sortable : true, + sortableOptions : { + handle: '.x-drag-handle' + }, + collection : this.allowedCollection + }); + + listViewAvailable.render(); listViewAllowed.render(); - this.listenTo(listViewAvailable, "doubleClick", this._moveQuality); - this.listenTo(listViewAllowed, "doubleClick", this._moveQuality); - this.listenTo(listViewAllowed, "sortStop", this._updateModel); + this.listenTo(listViewAvailable, 'doubleClick', this._moveQuality); + this.listenTo(listViewAllowed, 'doubleClick', this._moveQuality); + + this.listenTo(listViewAvailable, 'moveClicked', this._moveQuality); + this.listenTo(listViewAllowed, 'moveClicked', this._moveQuality); + + this.listenTo(listViewAllowed, 'sortStop', this._updateModel); }, _moveQuality: function (event) { diff --git a/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js new file mode 100644 index 000000000..8af2c7638 --- /dev/null +++ b/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js @@ -0,0 +1,27 @@ +'use strict'; +define( + [ + 'backbone.collectionview' + ], function (BackboneSortableCollectionView) { + return BackboneSortableCollectionView.extend({ + + events : { + 'mousedown li, td' : '_listItem_onMousedown', + 'dblclick li, td' : '_listItem_onDoubleClick', + 'click' : '_listBackground_onClick', + 'click ul.collection-list, table.collection-list' : '_listBackground_onClick', + 'keydown' : '_onKeydown', + 'click .x-move' : '_onClickMove' + }, + + _onClickMove: function( theEvent ) { + var clickedItemId = this._getClickedItemId( theEvent ); + + if( clickedItemId ) + { + var clickedModel = this.collection.get( clickedItemId ); + this.trigger('moveClicked', clickedModel); + } + } + }); + }); diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 60820eeca..de383c735 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -36,16 +36,14 @@ } } -ul.x-available-list, ul.x-allowed-list { - min-height: 100px; - +ul.available-list, ul.allowed-list { .user-select(none); - + + min-height: 100px; margin: 0; padding: 0; list-style-type: none; outline: none; - cursor: pointer; li { margin: 2px; @@ -54,56 +52,62 @@ ul.x-available-list, ul.x-allowed-list { border: 1px solid #AAA; border-radius: 4px; /* may need vendor varients */ background: #FAFAFA; + cursor: default; &:hover { border-color: #888; background: #EEE; } - .x-drag-handle, .x-moveleft-handle, .x-moveright-handle { + .drag-handle, .move-left-handle, .move-right-handle { opacity: 0.0; line-height: 20px; + cursor: pointer; + } + + .move-handle { + cursor: pointer; } } } -ul.x-available-list li { - .x-moveright-handle { +ul.available-list li { + .move-right-handle { opacity: 0.2; } - .x-drag-handle { + .drag-handle { display: none; } - &:hover .x-moveright-handle { + &:hover .move-right-handle { opacity: 1.0; } } -ul.x-allowed-list li { - .x-drag-handle, .x-moveleft-handle { +ul.allowed-list li { + .drag-handle, .move-left-handle { opacity: 0.2; } - .x-drag-handle:hover { + .drag-handle:hover { opacity: 1.0; cursor: pointer; } - &:hover .x-moveleft-handle { + &:hover .move-left-handle { opacity: 1.0; } } #quality-definition-list { - .x-header .row { + .quality-header .row { font-weight: bold; line-height: 40px; } - .x-rows .row { + .rows .row { line-height: 30px; border-top: 1px solid #ddd; vertical-align: middle; From 37ff745521456df13945abb5a03d23cc1a04e4c4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 21:42:14 -0800 Subject: [PATCH 03/48] Renamed view template --- src/UI/Settings/Quality/Profile/EditQualityProfileView.js | 2 +- ...ProfileTemplate.html => EditQualityProfileViewTemplate.html} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/UI/Settings/Quality/Profile/{EditQualityProfileTemplate.html => EditQualityProfileViewTemplate.html} (96%) diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 225fcb844..8e2acad1e 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -12,7 +12,7 @@ define( ], function (vent, Marionette, Backbone, BackboneSortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, _) { var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/EditQualityProfileTemplate', + template: 'Settings/Quality/Profile/EditQualityProfileViewTemplate', ui: { available: '.x-available-list', diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html similarity index 96% rename from src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html rename to src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html index e0c8a82fe..830301855 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html @@ -19,7 +19,7 @@
    From 3bfdd1d9eb7e548d24679e3dc54a6f6777e9b6eb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 21:51:56 -0800 Subject: [PATCH 04/48] Fixed sorting of cutoff when allowed list changes --- src/UI/Handlebars/Helpers/EachReverse.js | 20 +++++++++++++++++++ .../backbone.marionette.templates.js | 1 + .../Quality/Profile/EditQualityProfileView.js | 5 ++++- .../EditQualityProfileViewTemplate.html | 4 ++-- 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/UI/Handlebars/Helpers/EachReverse.js diff --git a/src/UI/Handlebars/Helpers/EachReverse.js b/src/UI/Handlebars/Helpers/EachReverse.js new file mode 100644 index 000000000..e2e628117 --- /dev/null +++ b/src/UI/Handlebars/Helpers/EachReverse.js @@ -0,0 +1,20 @@ +'use strict'; +define( + [ + 'handlebars' + ], function (Handlebars) { + Handlebars.registerHelper('eachReverse', function (context) { + var options = arguments[arguments.length - 1]; + var ret = ''; + + if (context && context.length > 0) { + for (var i = context.length - 1; i >= 0; i--) { + ret += options.fn(context[i]); + } + } else { + ret = options.inverse(this); + } + + return ret; + }); + }); diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js index 2c589a5cb..6b0ed8ac5 100644 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ b/src/UI/Handlebars/backbone.marionette.templates.js @@ -10,6 +10,7 @@ define( 'Handlebars/Helpers/Series', 'Handlebars/Helpers/Quality', 'Handlebars/Helpers/System', + 'Handlebars/Helpers/EachReverse', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 8e2acad1e..139bb5de2 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -31,7 +31,10 @@ define( this.availableCollection.comparator = function (model) { return -model.get('weight'); }; this.availableCollection.sort(); - this.allowedCollection = new Backbone.Collection(this.model.get('allowed').reverse()); + this.allowedCollection = new Backbone.Collection(this.model.get('allowed')); + this.allowedCollection.comparator = function (model) { return -model.get('weight'); }; + this.allowedCollection.sort(); + this.allowedCollection.comparator = undefined; }, onRender: function() { diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html index 830301855..da9a83ab3 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html @@ -18,9 +18,9 @@
    From 6ead44ca1b4546eb643b25299e0e051b165c81a4 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 22:02:52 -0800 Subject: [PATCH 05/48] Added a tooltip to allowed --- src/UI/Content/form.less | 9 +++++++++ .../Quality/Profile/EditQualityProfileViewTemplate.html | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/UI/Content/form.less b/src/UI/Content/form.less index 3296620df..a4c5aaf9e 100644 --- a/src/UI/Content/form.less +++ b/src/UI/Content/form.less @@ -47,3 +47,12 @@ textarea.release-restrictions { .clickable; } } + +h3 { + .help-inline { + font-size: 16px; + padding-left: 0px; + margin-top: -5px; + text-transform: none; + } +} diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html index da9a83ab3..e905b9b4c 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html @@ -35,7 +35,12 @@
    -

    Allowed

    +

    + Allowed + + + +

    From 3e97106aa707d321e50293f2bd3ef48678e4969c Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Wed, 29 Jan 2014 01:53:59 +0100 Subject: [PATCH 06/48] Updated QualityProfile to contain a list of Items each with a 'Allowed' bool. --- .../MappingTests/ResourceMappingFixture.cs | 6 +- .../Qualities/QualityProfileModule.cs | 33 +----- .../Qualities/QualityProfileResource.cs | 13 +-- .../Qualities/QualityProfileSchemaModule.cs | 32 ++---- .../CutoffSpecificationFixture.cs | 10 +- .../HistorySpecificationFixture.cs | 4 +- .../NotInQueueSpecificationFixture.cs | 2 +- ...ityAllowedByProfileSpecificationFixture.cs | 4 +- .../QualityUpgradeSpecificationFixture.cs | 4 +- .../UpgradeDiskSpecificationFixture.cs | 2 +- .../DownloadApprovedFixture.cs | 2 +- .../GetQualifiedReportsFixture.cs | 2 +- .../ImportDecisionMakerFixture.cs | 2 +- .../UpgradeSpecificationFixture.cs | 2 +- .../ImportApprovedEpisodesFixture.cs | 2 +- .../Qualities/QualityFixture.cs | 14 ++- .../Qualities/QualityModelComparerFixture.cs | 30 +---- .../QualityProfileRepositoryFixture.cs | 12 +- .../QualityProfileRepositoryFixture.cs | 7 +- .../Converters/EmbeddedDocumentConverter.cs | 27 ++++- .../Converters/QualityIntConverter.cs | 21 +++- .../Converters/QualityListConverter.cs | 52 --------- .../Converters/QualityModelConverter.cs | 55 --------- .../036_update_with_quality_converters.cs | 15 ++- .../037_add_configurable_qualities.cs | 3 + src/NzbDrone.Core/Datastore/TableMapping.cs | 4 +- .../QualityAllowedByProfileSpecification.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 +- src/NzbDrone.Core/Qualities/Quality.cs | 2 +- .../Qualities/QualityModelComparer.cs | 33 +----- src/NzbDrone.Core/Qualities/QualityProfile.cs | 2 +- .../Qualities/QualityProfileItem.cs | 11 ++ .../Qualities/QualityProfileService.cs | 84 ++++++-------- src/UI/JsLibraries/backbone.collectionview.js | 2 +- .../Quality/Profile/AllowedLabeler.js | 15 +-- .../EditQualityProfileItemViewTemplate.html | 5 +- .../Quality/Profile/EditQualityProfileView.js | 104 ++++++------------ .../EditQualityProfileViewTemplate.html | 11 +- src/UI/Settings/Quality/quality.less | 62 +++++------ 39 files changed, 239 insertions(+), 457 deletions(-) delete mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs delete mode 100644 src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs create mode 100644 src/NzbDrone.Core/Qualities/QualityProfileItem.cs diff --git a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs index d31edcac6..1cff8601f 100644 --- a/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs +++ b/src/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs @@ -41,7 +41,8 @@ namespace NzbDrone.Api.Test.MappingTests [TestCase(typeof(ParsedEpisodeInfo), typeof(ReleaseResource))] [TestCase(typeof(DownloadDecision), typeof(ReleaseResource))] [TestCase(typeof(Core.History.History), typeof(HistoryResource))] - [TestCase(typeof(Quality), typeof(QualityResource))] + [TestCase(typeof(QualityProfile), typeof(QualityProfileResource))] + [TestCase(typeof(QualityProfileItem), typeof(QualityProfileItemResource))] [TestCase(typeof(Log), typeof(LogResource))] [TestCase(typeof(Command), typeof(CommandResource))] public void matching_fields(Type modelType, Type resourceType) @@ -109,7 +110,8 @@ namespace NzbDrone.Api.Test.MappingTests { var profileResource = new QualityProfileResource { - Allowed = Builder.CreateListOfSize(1).Build().ToList(), + Cutoff = Quality.WEBDL1080p, + Items = new List { new QualityProfileItemResource { Quality = Quality.WEBDL1080p, Allowed = true } } }; diff --git a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs index 250bddd08..395d67803 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Api.Qualities SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Allowed).NotEmpty(); + SharedValidator.RuleFor(c => c.Items).NotEmpty(); GetResourceAll = GetAll; @@ -48,46 +48,23 @@ namespace NzbDrone.Api.Qualities private void Update(QualityProfileResource resource) { var model = _qualityProfileService.Get(resource.Id); + model.Name = resource.Name; model.Cutoff = (Quality)resource.Cutoff.Id; - model.Allowed = resource.Allowed.Select(p => (Quality)p.Id).ToList(); + model.Items = resource.Items.InjectTo>(); _qualityProfileService.Update(model); } private QualityProfileResource GetById(int id) { - return MapToResource(_qualityProfileService.Get(id)); + return _qualityProfileService.Get(id).InjectTo(); } private List GetAll() { - var profiles = _qualityProfileService.All().Select(MapToResource).ToList(); + var profiles = _qualityProfileService.All().InjectTo>(); return profiles; } - - private QualityProfileResource MapToResource(QualityProfile profile) - { - return new QualityProfileResource - { - Cutoff = MapToResource(_qualityDefinitionService.Get(profile.Cutoff)), - Available = _qualityDefinitionService.All() - .Where(c => !profile.Allowed.Any(q => c.Quality == q)) - .Select(MapToResource).ToList(), - Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(MapToResource).ToList(), - Name = profile.Name, - Id = profile.Id - }; - } - - private QualityResource MapToResource(QualityDefinition config) - { - return new QualityResource - { - Id = config.Quality.Id, - Name = config.Quality.Name, - Weight = config.Weight - }; - } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs index 73ba94268..dbeff0fa0 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileResource.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileResource.cs @@ -1,21 +1,20 @@ using System; using System.Collections.Generic; using NzbDrone.Api.REST; +using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Qualities { public class QualityProfileResource : RestResource { public String Name { get; set; } - public QualityResource Cutoff { get; set; } - public List Available { get; set; } - public List Allowed { get; set; } + public Quality Cutoff { get; set; } + public List Items { get; set; } } - public class QualityResource : RestResource + public class QualityProfileItemResource : RestResource { - public String Name { get; set; } - - public Int32 Weight { get; set; } + public Quality Quality { get; set; } + public bool Allowed { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs index e4e23ca02..64caeefab 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileSchemaModule.cs @@ -19,34 +19,16 @@ namespace NzbDrone.Api.Qualities private List GetAll() { + var items = _qualityDefinitionService.All() + .OrderBy(v => v.Weight) + .Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = false }) + .ToList(); + var profile = new QualityProfile(); profile.Cutoff = Quality.Unknown; - profile.Allowed = new List(); + profile.Items = items; - return new List { QualityToResource(profile) }; - } - - private QualityProfileResource QualityToResource(QualityProfile profile) - { - return new QualityProfileResource - { - Cutoff = QualityToResource(_qualityDefinitionService.Get(profile.Cutoff)), - Available = _qualityDefinitionService.All().Select(QualityToResource).ToList(), - Allowed = profile.Allowed.Select(_qualityDefinitionService.Get).Select(QualityToResource).ToList(), - Name = profile.Name, - Id = profile.Id - }; - } - - - private QualityResource QualityToResource(QualityDefinition config) - { - return new QualityResource - { - Id = config.Quality.Id, - Name = config.Quality.Name, - Weight = config.Weight - }; + return new List { profile.InjectTo() }; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index be1f420da..861d39d32 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -13,35 +13,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.DVD, true)).Should().BeTrue(); } [Test] public void should_return_false_if_current_episode_is_equal_to_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true)).Should().BeFalse(); } [Test] public void should_return_false_if_current_episode_is_greater_than_cutoff() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } [Test] public void should_return_true_when_new_episode_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, false), new QualityModel(Quality.HDTV720p, true)).Should().BeTrue(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }, + Subject.CutoffNotMet(new QualityProfile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() }, new QualityModel(Quality.HDTV720p, true), new QualityModel(Quality.Bluray1080p, true)).Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 36d7db629..4013271c9 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; _fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing() { - _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + _fakeSeries.QualityProfile = new QualityProfile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, false); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, false); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index 59361c70f..baa24d5b4 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { _series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _episode = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index fb5ddde76..d8d46f3c0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_allow_if_quality_is_defined_in_profile(Quality qualityType) { remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType; - remoteEpisode.Series.QualityProfile.Value.Allowed = new List { Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p }; + remoteEpisode.Series.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); Subject.IsSatisfiedBy(remoteEpisode, null).Should().BeTrue(); } @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_not_allow_if_quality_is_not_defined_in_profile(Quality qualityType) { remoteEpisode.ParsedEpisodeInfo.Quality.Quality = qualityType; - remoteEpisode.Series.QualityProfile.Value.Allowed = new List { Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p }; + remoteEpisode.Series.QualityProfile.Value.Items = Qualities.QualityFixture.GetDefaultQualities(Quality.DVD, Quality.HDTV720p, Quality.Bluray1080p); Subject.IsSatisfiedBy(remoteEpisode, null).Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index d3385c23b..e91771776 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + var qualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }; Subject.IsUpgradable(qualityProfile, new QualityModel(current, currentProper), new QualityModel(newQuality, newProper)) .Should().Be(expected); @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - var qualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }; + var qualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }; Subject.IsUpgradable(qualityProfile, new QualityModel(Quality.DVD, true), new QualityModel(Quality.DVD, false)) .Should().BeFalse(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 9135f0a75..c9e03d256 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var doubleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = _secondFile, EpisodeFileId = 1 }, new Episode { EpisodeFile = null } }; var fakeSeries = Builder.CreateNew() - .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.QualityProfile = new QualityProfile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _parseResultMulti = new RemoteEpisode diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 65ad262a1..0563db94e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release.PublishDate = DateTime.UtcNow; remoteEpisode.Series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); return remoteEpisode; diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs index 0036a8bcb..4c8aafaa4 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/GetQualifiedReportsFixture.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests remoteEpisode.Release.Size = size; remoteEpisode.Series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); return remoteEpisode; diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs index 0e2bc1053..dcd8a12dc 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportDecisionMakerFixture.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _videoFiles = new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; _series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _quality = new QualityModel(Quality.DVD); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs index f189f049b..a0bff8424 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/UpgradeSpecificationFixture.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications { _series = Builder.CreateNew() .With(s => s.SeriesType = SeriesTypes.Standard) - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); _localEpisode = new LocalEpisode diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 55789f9b8..59aa51f1c 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions = new List(); var series = Builder.CreateNew() - .With(e => e.QualityProfile = new QualityProfile { Allowed = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() }) .Build(); var episodes = Builder.CreateListOfSize(5) diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index bffb0f5be..ddab8f19b 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -44,9 +44,9 @@ namespace NzbDrone.Core.Test.Qualities i.Should().Be(expected); } - public static List GetDefaultQualities() + public static List GetDefaultQualities(params Quality[] allowed) { - return new List + var qualities = new List { Quality.SDTV, Quality.WEBDL480p, @@ -59,6 +59,16 @@ namespace NzbDrone.Core.Test.Qualities Quality.WEBDL1080p, Quality.Bluray1080p }; + + if (allowed.Length == 0) + allowed = qualities.ToArray(); + + var items = qualities + .Except(allowed) + .Concat(allowed) + .Select(v => new QualityProfileItem { Quality = v, Allowed = allowed.Contains(v) }).ToList(); + + return items; } } } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index 416962658..187455abb 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.Qualities private void GivenDefaultQualityProfile() { - Subject = new QualityModelComparer(new QualityProfile { Allowed = QualityFixture.GetDefaultQualities() }); + Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities() }); } private void GivenCustomQualityProfile() { - Subject = new QualityModelComparer(new QualityProfile { Allowed = new List { Quality.Bluray720p, Quality.DVD } }); + Subject = new QualityModelComparer(new QualityProfile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) }); } [Test] @@ -87,31 +87,5 @@ namespace NzbDrone.Core.Test.Qualities compare.Should().BeGreaterThan(0); } - - [Test] - public void Icomparer_missing_custom_order() - { - GivenCustomQualityProfile(); - - var first = new QualityModel(Quality.Bluray720p, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - var compare = Subject.Compare(first, second); - - compare.Should().BeGreaterThan(0); - } - - [Test] - public void Icomparer_missing_both_custom_order() - { - GivenCustomQualityProfile(); - - var first = new QualityModel(Quality.SDTV, true); - var second = new QualityModel(Quality.Bluray1080p, true); - - var compare = Subject.Compare(first, second); - - compare.Should().Be(0); - } } } diff --git a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs index f13c59f3d..10b8ece92 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityProfileRepositoryFixture.cs @@ -14,13 +14,7 @@ namespace NzbDrone.Core.Test.Qualities { var profile = new QualityProfile { - Allowed = new List - { - Quality.Bluray1080p, - Quality.DVD, - Quality.HDTV720p - }, - + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), Cutoff = Quality.Bluray1080p, Name = "TestProfile" }; @@ -29,8 +23,8 @@ namespace NzbDrone.Core.Test.Qualities StoredModel.Name.Should().Be(profile.Name); StoredModel.Cutoff.Should().Be(profile.Cutoff); - - StoredModel.Allowed.Should().BeEquivalentTo(profile.Allowed); + + StoredModel.Items.Should().Equal(profile.Items, (a,b) => a.Quality == b.Quality && a.Allowed == b.Allowed); } diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs index 4829e3417..0685ccc0f 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/QualityProfileRepositoryFixture.cs @@ -17,12 +17,7 @@ namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests { var profile = new QualityProfile { - Allowed = new List - { - Quality.Bluray1080p, - Quality.DVD, - Quality.HDTV720p - }, + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p), Cutoff = Quality.Bluray1080p, Name = "TestProfile" diff --git a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs index e144d4cf4..e7591f3ee 100644 --- a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs @@ -2,11 +2,33 @@ using Marr.Data.Converters; using Marr.Data.Mapping; using NzbDrone.Common.Serializer; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Newtonsoft.Json.Converters; namespace NzbDrone.Core.Datastore.Converters { public class EmbeddedDocumentConverter : IConverter { + private readonly JsonSerializerSettings SerializerSetting; + + public EmbeddedDocumentConverter(params JsonConverter[] converters) + { + SerializerSetting = new JsonSerializerSettings + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Include, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); + SerializerSetting.Converters.Add(new VersionConverter()); + foreach (var converter in converters) + SerializerSetting.Converters.Add(converter); + } + public virtual object FromDB(ConverterContext context) { if (context.DbValue == DBNull.Value) @@ -20,8 +42,7 @@ namespace NzbDrone.Core.Datastore.Converters { return null; } - - return Json.Deserialize(stringValue, context.ColumnMap.FieldType); + return JsonConvert.DeserializeObject(stringValue, context.ColumnMap.FieldType, SerializerSetting); } public object FromDB(ColumnMap map, object dbValue) @@ -33,7 +54,7 @@ namespace NzbDrone.Core.Datastore.Converters { if (clrValue == null) return null; - return clrValue.ToJson(); + return JsonConvert.SerializeObject(clrValue, SerializerSetting); } public Type DbType diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs index ea5dfd59f..ad07994c5 100644 --- a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs @@ -4,10 +4,11 @@ using Marr.Data.Mapping; using NzbDrone.Core.Qualities; using System.Collections.Generic; using NzbDrone.Common.Serializer; +using Newtonsoft.Json; namespace NzbDrone.Core.Datastore.Converters { - public class QualityIntConverter : IConverter + public class QualityIntConverter : JsonConverter, IConverter { public object FromDB(ConverterContext context) { @@ -46,5 +47,23 @@ namespace NzbDrone.Core.Datastore.Converters return typeof(int); } } + + #region JsonConverter + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Quality); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var item = reader.Value; + return (Quality)Convert.ToInt32(item); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(ToDB(value)); + } + #endregion } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs deleted file mode 100644 index c0c58d8f5..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/QualityListConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using NzbDrone.Core.Qualities; -using System.Collections.Generic; -using NzbDrone.Common.Serializer; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class QualityListConverter : IConverter - { - public object FromDB(ConverterContext context) - { - if (context.DbValue == DBNull.Value) - { - return DBNull.Value; - } - - var val = Convert.ToString(context.DbValue); - - var qualityList = Json.Deserialize>(val).ConvertAll(Quality.FindById); - - return qualityList; - } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue == DBNull.Value) return null; - - var qualityList = clrValue as List; - - if (qualityList == null) - { - throw new InvalidOperationException("Can only store a list of qualities in this database column."); - } - - var intList = qualityList.ConvertAll(v => v.Id); - - return intList.ToJson(); - } - - public Type DbType - { - get { return typeof(string); } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs deleted file mode 100644 index 8e2917dcc..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/QualityModelConverter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using NzbDrone.Core.Qualities; -using System.Collections.Generic; -using NzbDrone.Common.Serializer; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class QualityModelConverter : IConverter - { - public object FromDB(ConverterContext context) - { - if (context.DbValue == DBNull.Value) - { - return new QualityModel(); - } - - var val = Convert.ToString(context.DbValue); - - var jsonObject = Json.Deserialize>(val); - - return new QualityModel((Quality)Convert.ToInt32(jsonObject["id"]), Convert.ToBoolean(jsonObject["proper"])); - } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue == DBNull.Value) - clrValue = new QualityModel(); - - var qualityModel = clrValue as QualityModel; - - if (qualityModel == null) - { - throw new InvalidOperationException("Can only store a QualityModel in this database column."); - } - - var jsonObject = new Dictionary(); - jsonObject["id"] = (int)qualityModel.Quality; - jsonObject["proper"] = qualityModel.Proper; - - return jsonObject.ToJson(); - } - - public Type DbType - { - get { return typeof(string); } - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index c29490f66..35f9dbbb9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -6,6 +6,7 @@ using System; using NzbDrone.Common.Serializer; using NzbDrone.Core.Qualities; using System.Collections.Generic; +using NzbDrone.Core.Datastore.Converters; namespace NzbDrone.Core.Datastore.Migration { @@ -14,6 +15,8 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { + Alter.Table("QualityProfiles").AddColumn("Items").AsString().Nullable(); + Execute.WithConnection(ConvertQualityProfiles); Execute.WithConnection(ConvertQualityModels); @@ -21,7 +24,7 @@ namespace NzbDrone.Core.Datastore.Migration private void ConvertQualityProfiles(IDbConnection conn, IDbTransaction tran) { - var qualityListConverter = new NzbDrone.Core.Datastore.Converters.QualityListConverter(); + var qualityProfileItemConverter = new EmbeddedDocumentConverter(new QualityIntConverter()); // Convert 'Allowed' column in QualityProfiles from Json List to Json List (int = Quality) using (IDbCommand qualityProfileCmd = conn.CreateCommand()) @@ -36,13 +39,15 @@ namespace NzbDrone.Core.Datastore.Migration var allowedJson = qualityProfileReader.GetString(1); var allowed = Json.Deserialize>(allowedJson); - - var allowedNewJson = qualityListConverter.ToDB(allowed); + + var items = Quality.DefaultQualityDefinitions.OrderBy(v => v.Weight).Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }).ToList(); + + var allowedNewJson = qualityProfileItemConverter.ToDB(items); using (IDbCommand updateCmd = conn.CreateCommand()) { updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE QualityProfiles SET Allowed = ? WHERE Id = ?"; + updateCmd.CommandText = "UPDATE QualityProfiles SET Items = ? WHERE Id = ?"; updateCmd.AddParameter(allowedNewJson); updateCmd.AddParameter(id); @@ -63,7 +68,7 @@ namespace NzbDrone.Core.Datastore.Migration private void ConvertQualityModel(IDbConnection conn, IDbTransaction tran, string tableName) { - var qualityModelConverter = new NzbDrone.Core.Datastore.Converters.QualityModelConverter(); + var qualityModelConverter = new EmbeddedDocumentConverter(new QualityIntConverter()); using (IDbCommand qualityModelCmd = conn.CreateCommand()) { diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs index 1020dca2c..c036ee4ba 100644 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs @@ -14,6 +14,9 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { + SqLiteAlter.DropColumns("QualityProfiles", new[] { "Allowed" }); + Alter.Column("Items").OnTable("QualityProfiles").AsString().NotNullable(); + Create.TableForModel("QualityDefinitions") .WithColumn("Quality").AsInt32().Unique() .WithColumn("Title").AsString().Unique() diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index f164e925d..f97514f16 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -81,8 +81,8 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(Boolean), new BooleanIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new QualityListConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new QualityModelConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new QualityIntConverter())); + MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 40c0a3ad0..b4bcc1f83 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { _logger.Trace("Checking if report meets quality requirements. {0}", subject.ParsedEpisodeInfo.Quality); - if (!subject.Series.QualityProfile.Value.Allowed.Contains(subject.ParsedEpisodeInfo.Quality.Quality)) + if (!subject.Series.QualityProfile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedEpisodeInfo.Quality.Quality)) { _logger.Trace("Quality {0} rejected by Series' quality profile", subject.ParsedEpisodeInfo.Quality); return false; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b93b226a2..635d2bfd3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -145,8 +145,6 @@ - - @@ -461,6 +459,7 @@ + diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 656e522da..673abc5f7 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -68,7 +68,7 @@ namespace NzbDrone.Core.Qualities public static Quality WEBDL480p { get { return new Quality(8, "WEBDL-480p"); } } public static Quality HDTV1080p { get { return new Quality(9, "HDTV-1080p"); } } public static Quality RAWHD { get { return new Quality(10, "Raw-HD"); } } - public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p"); } } + //public static Quality HDTV480p { get { return new Quality(11, "HDTV-480p"); } } public static List All { diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index 2859059af..bb66dcd7f 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -14,15 +14,15 @@ namespace NzbDrone.Core.Qualities public QualityModelComparer(QualityProfile qualityProfile) { Ensure.That(qualityProfile, () => qualityProfile).IsNotNull(); - Ensure.That(qualityProfile.Allowed, () => qualityProfile.Allowed).HasItems(); + Ensure.That(qualityProfile.Items, () => qualityProfile.Items).HasItems(); _qualityProfile = qualityProfile; } public int Compare(Quality left, Quality right) { - int leftIndex = _qualityProfile.Allowed.IndexOf(left); - int rightIndex = _qualityProfile.Allowed.IndexOf(right); + int leftIndex = _qualityProfile.Items.FindIndex(v => v.Quality == left); + int rightIndex = _qualityProfile.Items.FindIndex(v => v.Quality == right); return leftIndex.CompareTo(rightIndex); } @@ -36,32 +36,5 @@ namespace NzbDrone.Core.Qualities return result; } - /* - public string GetName(Quality quality) - { - QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality); - - return qualityDefinition.Name; - } - - public string GetName(QualityModel quality) - { - QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); - - if (quality.Proper) - return qualityDefinition.Name + " Proper"; - else - return qualityDefinition.Name; - } - - public string GetSceneName(QualityModel quality) - { - QualityDefinition qualityDefinition = _qualityDefinitionService.Get(quality.Quality); - - if (quality.Proper) - return qualityDefinition.SceneName + " PROPER"; - else - return qualityDefinition.SceneName; - }*/ } } diff --git a/src/NzbDrone.Core/Qualities/QualityProfile.cs b/src/NzbDrone.Core/Qualities/QualityProfile.cs index 892983ecd..b578d1962 100644 --- a/src/NzbDrone.Core/Qualities/QualityProfile.cs +++ b/src/NzbDrone.Core/Qualities/QualityProfile.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Qualities public class QualityProfile : ModelBase { public string Name { get; set; } - public List Allowed { get; set; } public Quality Cutoff { get; set; } + public List Items { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Qualities/QualityProfileItem.cs b/src/NzbDrone.Core/Qualities/QualityProfileItem.cs new file mode 100644 index 000000000..9d7d839d2 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityProfileItem.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Qualities +{ + public class QualityProfileItem : IEmbeddedDocument + { + public Quality Quality { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Qualities/QualityProfileService.cs index 82baaaa39..13b18afad 100644 --- a/src/NzbDrone.Core/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Qualities/QualityProfileService.cs @@ -60,68 +60,46 @@ namespace NzbDrone.Core.Qualities return _qualityProfileRepository.Get(id); } + private QualityProfile AddDefaultQualityProfile(string name, Quality cutoff, params Quality[] allowed) + { + var items = Quality.DefaultQualityDefinitions + .OrderBy(v => v.Weight) + .Select(v => new QualityProfileItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) + .ToList(); + + var qualityProfile = new QualityProfile { Name = name, Cutoff = cutoff, Items = items }; + + return Add(qualityProfile); + } + public void Handle(ApplicationStartedEvent message) { if (All().Any()) return; _logger.Info("Setting up default quality profiles"); - var sd = new QualityProfile - { - Name = "SD", - Allowed = new List - { - Quality.SDTV, - Quality.WEBDL480p, - Quality.DVD - }, - Cutoff = Quality.SDTV - }; + AddDefaultQualityProfile("SD", Quality.SDTV, + Quality.SDTV, + Quality.WEBDL480p, + Quality.DVD); - var hd720p = new QualityProfile - { - Name = "HD 720p", - Allowed = new List - { - Quality.HDTV720p, - Quality.WEBDL720p, - Quality.Bluray720p - }, - Cutoff = Quality.HDTV720p - }; + AddDefaultQualityProfile("HD-720p", Quality.HDTV720p, + Quality.HDTV720p, + Quality.WEBDL720p, + Quality.Bluray720p); + AddDefaultQualityProfile("HD-1080p", Quality.HDTV1080p, + Quality.HDTV1080p, + Quality.WEBDL1080p, + Quality.Bluray1080p); - var hd1080p = new QualityProfile - { - Name = "HD 1080p", - Allowed = new List - { - Quality.HDTV1080p, - Quality.WEBDL1080p, - Quality.Bluray1080p - }, - Cutoff = Quality.HDTV1080p - }; - - var hdAll = new QualityProfile - { - Name = "HD - All", - Allowed = new List - { - Quality.HDTV720p, - Quality.WEBDL720p, - Quality.Bluray720p, - Quality.HDTV1080p, - Quality.WEBDL1080p, - Quality.Bluray1080p - }, - Cutoff = Quality.HDTV720p - }; - - Add(sd); - Add(hd720p); - Add(hd1080p); - Add(hdAll); + AddDefaultQualityProfile("HD - All", Quality.HDTV720p, + Quality.HDTV720p, + Quality.HDTV1080p, + Quality.WEBDL720p, + Quality.WEBDL1080p, + Quality.Bluray720p, + Quality.Bluray1080p); } } } \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.collectionview.js b/src/UI/JsLibraries/backbone.collectionview.js index 170871ec4..29ec982ff 100644 --- a/src/UI/JsLibraries/backbone.collectionview.js +++ b/src/UI/JsLibraries/backbone.collectionview.js @@ -875,7 +875,7 @@ { if( _.contains( this.selectedItems, clickedItemId ) ) this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } ); - else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } ); + else this.setSelectedModels( _.union( this.selectedItems, [ clickedItemId ] ), { by : "cid" } ); } else this.setSelectedModels( [ clickedItemId ], { by : "cid" } ); diff --git a/src/UI/Settings/Quality/Profile/AllowedLabeler.js b/src/UI/Settings/Quality/Profile/AllowedLabeler.js index fed162e17..ab09626e9 100644 --- a/src/UI/Settings/Quality/Profile/AllowedLabeler.js +++ b/src/UI/Settings/Quality/Profile/AllowedLabeler.js @@ -7,13 +7,14 @@ define( Handlebars.registerHelper('allowedLabeler', function () { var ret = ''; var cutoff = this.cutoff; - _.each(this.allowed, function (allowed) { - if (allowed.id === cutoff.id) { - ret += '' + allowed.name + ' '; - } - - else { - ret += '' + allowed.name + ' '; + _.each(this.items, function (item) { + if (item.allowed) { + if (item.quality.id === cutoff.id) { + ret += '' + item.quality.name + ' '; + } + else { + ret += '' + item.quality.name + ' '; + } } }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html index 520c95246..f118c528b 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html @@ -1,4 +1,3 @@ - -{{name}} + +{{quality.name}} - diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 139bb5de2..49e571939 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -15,7 +15,6 @@ define( template: 'Settings/Quality/Profile/EditQualityProfileViewTemplate', ui: { - available: '.x-available-list', allowed : '.x-allowed-list', cutoff : '.x-cutoff' }, @@ -26,101 +25,62 @@ define( initialize: function (options) { this.profileCollection = options.profileCollection; - - this.availableCollection = new Backbone.Collection(this.model.get('available')); - this.availableCollection.comparator = function (model) { return -model.get('weight'); }; - this.availableCollection.sort(); - - this.allowedCollection = new Backbone.Collection(this.model.get('allowed')); - this.allowedCollection.comparator = function (model) { return -model.get('weight'); }; - this.allowedCollection.sort(); - this.allowedCollection.comparator = undefined; + + this.allowedCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); }, - onRender: function() { - var listViewAvailable = new BackboneSortableCollectionView({ - el : this.ui.available, - modelView : EditQualityProfileItemView, - selectable: false, - sortable : false, - collection: this.availableCollection - }); - - var listViewAllowed = new BackboneSortableCollectionView({ - el : this.ui.allowed, - modelView : EditQualityProfileItemView, - selectable: false, - sortable : true, + onRender: function() { + var MyCollectionView = BackboneSortableCollectionView.extend({ + events : { + // Backbone.CollectionView used mousedown for the click event, which interferes with the sortable. + "click li, td" : "_listItem_onMousedown", + "dblclick li, td" : "_listItem_onDoubleClick", + "click" : "_listBackground_onClick", + "click ul.collection-list, table.collection-list" : "_listBackground_onClick", + "keydown" : "_onKeydown" + } + }); + var listViewAllowed = new MyCollectionView({ + el : this.ui.allowed, + modelView : EditQualityProfileItemView, + selectable : true, + selectMultiple : true, + clickToSelect : true, + clickToToggle : true, + sortable : true, sortableOptions : { handle: '.x-drag-handle' }, collection : this.allowedCollection }); - listViewAvailable.render(); + listViewAllowed.setSelectedModels(this.allowedCollection.filter(function(item) { return item.get('allowed') === true; })); + listViewAllowed.render(); - this.listenTo(listViewAvailable, 'doubleClick', this._moveQuality); - this.listenTo(listViewAllowed, 'doubleClick', this._moveQuality); - - this.listenTo(listViewAvailable, 'moveClicked', this._moveQuality); - this.listenTo(listViewAllowed, 'moveClicked', this._moveQuality); - + this.listenTo(listViewAllowed, 'selectionChanged', this._selectionChanged); this.listenTo(listViewAllowed, 'sortStop', this._updateModel); }, - - _moveQuality: function (event) { - - var quality; - var qualityId = event.get('id'); + + _selectionChanged: function(newSelectedModels, oldSelectedModels) { + var addedModels = _.difference(newSelectedModels, oldSelectedModels); + var removeModels = _.difference(oldSelectedModels, newSelectedModels); - if (this.availableCollection.get(qualityId)) { - quality = this.availableCollection.get(qualityId); - var idealIndex = 0; - var idealMismatches = 1000; - // Insert it at the best possible spot. - for (var i = 0; i <= this.allowedCollection.length; i++) { - var mismatches = 0; - for (var j = 0; j < i; j++) { - if (this.allowedCollection.at(j).get('weight') < quality.get('weight')) - mismatches++; - } - for (j = i; j < this.allowedCollection.length; j++) { - if (this.allowedCollection.at(j).get('weight') > quality.get('weight')) - mismatches++; - } - if (mismatches <= idealMismatches) { - idealIndex = i; - idealMismatches = mismatches; - } - } - - this.availableCollection.remove(quality); - this.allowedCollection.add(quality, {at: idealIndex}); - } - else if (this.allowedCollection.get(qualityId)) { - quality = this.allowedCollection.get(qualityId); - - this.allowedCollection.remove(quality); - this.availableCollection.add(quality); - } - else { - throw 'couldnt find quality id ' + qualityId; - } + _.each(removeModels, function(item) { item.set('allowed', false); }); + _.each(addedModels, function(item) { item.set('allowed', true); }); this._updateModel(); }, _updateModel: function() { - this.model.set('available', this.availableCollection.toJSON().reverse()); - this.model.set('allowed', this.allowedCollection.toJSON().reverse()); + this.model.set('items', this.allowedCollection.toJSON().reverse()); this.render(); }, _saveQualityProfile: function () { var self = this; - var cutoff = _.findWhere(this.model.get('allowed'), { id: parseInt(this.ui.cutoff.val(), 10)}); + var cutoff = _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)}); this.model.set('cutoff', cutoff); var promise = this.model.save(); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html index e905b9b4c..5603f9221 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html @@ -18,8 +18,10 @@
    @@ -30,11 +32,6 @@
    -

    Available

    -
      -
    -
    -

    Allowed diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index de383c735..9a5b65cd9 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -36,7 +36,7 @@ } } -ul.available-list, ul.allowed-list { +ul.allowed-list { .user-select(none); min-height: 100px; @@ -52,51 +52,41 @@ ul.available-list, ul.allowed-list { border: 1px solid #AAA; border-radius: 4px; /* may need vendor varients */ background: #FAFAFA; - cursor: default; + cursor: pointer; - &:hover { - border-color: #888; - background: #EEE; + .quality-label { + color: #CCC; } - - .drag-handle, .move-left-handle, .move-right-handle { - opacity: 0.0; + .drag-handle, .select-handle { + opacity: 0.2; line-height: 20px; cursor: pointer; } - - .move-handle { + + .drag-handle:hover { + opacity: 1.0; cursor: pointer; } } -} - -ul.available-list li { - .move-right-handle { - opacity: 0.2; + + li.selected { + .select-handle { + opacity: 1.0; + cursor: pointer; + } + + .quality-label { + color: #444; + } } - .drag-handle { - display: none; - } - - &:hover .move-right-handle { - opacity: 1.0; - } -} - -ul.allowed-list li { - .drag-handle, .move-left-handle { - opacity: 0.2; - } - - .drag-handle:hover { - opacity: 1.0; - cursor: pointer; - } - - &:hover .move-left-handle { - opacity: 1.0; + li:hover { + border-color: #888; + background: #EEE; + + .select-handle { + opacity: 0.5; + } } } From 75decad601ad637f118624541acef503199fbc8c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 28 Jan 2014 20:13:00 -0800 Subject: [PATCH 07/48] UI looking better for quality profile editing --- .../Converters/EmbeddedDocumentConverter.cs | 4 +- .../Converters/QualityIntConverter.cs | 2 - .../036_update_with_quality_converters.cs | 1 - .../037_add_configurable_qualities.cs | 3 - .../Migration/Framework/SqliteAlter.cs | 7 +- .../Organizer/FileNameBuilder.cs | 1 - src/UI/Config.js | 5 +- .../EditQualityProfileItemViewTemplate.html | 4 +- .../Quality/Profile/EditQualityProfileView.js | 31 +++---- .../EditQualityProfileViewTemplate.html | 15 ++-- .../Profile/QualitySortableCollectionView.js | 2 +- src/UI/Settings/Quality/quality.less | 88 +++++++++++-------- src/UI/Settings/SettingsLayout.js | 4 +- 13 files changed, 81 insertions(+), 86 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs index e7591f3ee..806bbf5f4 100644 --- a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs @@ -1,7 +1,6 @@ using System; using Marr.Data.Converters; using Marr.Data.Mapping; -using NzbDrone.Common.Serializer; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Converters; @@ -25,8 +24,11 @@ namespace NzbDrone.Core.Datastore.Converters SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); SerializerSetting.Converters.Add(new VersionConverter()); + foreach (var converter in converters) + { SerializerSetting.Converters.Add(converter); + } } public virtual object FromDB(ConverterContext context) diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs index ad07994c5..f423758f1 100644 --- a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs @@ -48,7 +48,6 @@ namespace NzbDrone.Core.Datastore.Converters } } - #region JsonConverter public override bool CanConvert(Type objectType) { return objectType == typeof(Quality); @@ -64,6 +63,5 @@ namespace NzbDrone.Core.Datastore.Converters { writer.WriteValue(ToDB(value)); } - #endregion } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index 35f9dbbb9..090ddd33a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -2,7 +2,6 @@ using NzbDrone.Core.Datastore.Migration.Framework; using System.Data; using System.Linq; -using System; using NzbDrone.Common.Serializer; using NzbDrone.Core.Qualities; using System.Collections.Generic; diff --git a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs index c036ee4ba..fddd8e5a5 100644 --- a/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs +++ b/src/NzbDrone.Core/Datastore/Migration/037_add_configurable_qualities.cs @@ -2,10 +2,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; using System.Data; using System.Linq; -using System; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Qualities; -using System.Collections.Generic; namespace NzbDrone.Core.Datastore.Migration { diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteAlter.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteAlter.cs index c8d423a1c..848470de3 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteAlter.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SqliteAlter.cs @@ -45,7 +45,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework var newIndexes = originalIndexes.Union(indexes); - CreateTable(tableName, columns, newIndexes); transaction.Commit(); @@ -57,7 +56,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework using (var transaction = _sqLiteMigrationHelper.BeginTransaction()) { var originalColumns = _sqLiteMigrationHelper.GetColumns(tableName); - var originalIndexes = _sqLiteMigrationHelper.GetIndexes(tableName); + var indexes = _sqLiteMigrationHelper.GetIndexes(tableName); var newColumns = originalColumns.Select(c => { @@ -82,9 +81,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework return c.Value; }).ToList(); - var newIndexes = originalIndexes; - - CreateTable(tableName, newColumns, newIndexes); + CreateTable(tableName, newColumns, indexes); transaction.Commit(); } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 9a35534be..2997be7d5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -150,7 +150,6 @@ namespace NzbDrone.Core.Organizer tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); - return CleanFilename(ReplaceTokens(pattern, tokenValues).Trim()); } diff --git a/src/UI/Config.js b/src/UI/Config.js index 797cdadb3..bbc6b54ed 100644 --- a/src/UI/Config.js +++ b/src/UI/Config.js @@ -9,8 +9,9 @@ define( }, Keys : { DefaultQualityProfileId: 'DefaultQualityProfileId', - DefaultRootFolderId: 'DefaultRootFolderId', - UseSeasonFolder: 'UseSeasonFolder' + DefaultRootFolderId : 'DefaultRootFolderId', + UseSeasonFolder : 'UseSeasonFolder', + AdvancedSettings : 'advancedSettings' }, getValueBoolean: function (key, defaultValue) { diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html index f118c528b..33cc7d2f5 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html @@ -1,3 +1,3 @@ - + {{quality.name}} - + diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js index 49e571939..a602e7dbd 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js @@ -1,6 +1,7 @@ 'use strict'; define( [ + 'underscore', 'vent', 'marionette', 'backbone', @@ -8,8 +9,8 @@ define( 'Settings/Quality/Profile/EditQualityProfileItemView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView', - 'underscore' - ], function (vent, Marionette, Backbone, BackboneSortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, _) { + 'Config' + ], function (_, vent, Marionette, Backbone, QualitySortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, Config) { var view = Marionette.ItemView.extend({ template: 'Settings/Quality/Profile/EditQualityProfileViewTemplate', @@ -26,35 +27,23 @@ define( initialize: function (options) { this.profileCollection = options.profileCollection; - this.allowedCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); + this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); }, onRender: function() { - var MyCollectionView = BackboneSortableCollectionView.extend({ - events : { - // Backbone.CollectionView used mousedown for the click event, which interferes with the sortable. - "click li, td" : "_listItem_onMousedown", - "dblclick li, td" : "_listItem_onDoubleClick", - "click" : "_listBackground_onClick", - "click ul.collection-list, table.collection-list" : "_listBackground_onClick", - "keydown" : "_onKeydown" - } - }); - var listViewAllowed = new MyCollectionView({ + + var listViewAllowed = new QualitySortableCollectionView({ el : this.ui.allowed, modelView : EditQualityProfileItemView, selectable : true, selectMultiple : true, clickToSelect : true, clickToToggle : true, - sortable : true, - sortableOptions : { - handle: '.x-drag-handle' - }, - collection : this.allowedCollection + sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false), + collection : this.itemsCollection }); - listViewAllowed.setSelectedModels(this.allowedCollection.filter(function(item) { return item.get('allowed') === true; })); + listViewAllowed.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; })); listViewAllowed.render(); @@ -73,7 +62,7 @@ define( }, _updateModel: function() { - this.model.set('items', this.allowedCollection.toJSON().reverse()); + this.model.set('items', this.itemsCollection.toJSON().reverse()); this.render(); }, diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html index 5603f9221..619f6c873 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html @@ -29,17 +29,14 @@

    - -
    -
    -

    - Allowed +
    + +
    +
      - + -

      -
        -
      +
      diff --git a/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js index 8af2c7638..0185d82c0 100644 --- a/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js +++ b/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js @@ -6,7 +6,7 @@ define( return BackboneSortableCollectionView.extend({ events : { - 'mousedown li, td' : '_listItem_onMousedown', + 'click li, td' : '_listItem_onMousedown', 'dblclick li, td' : '_listItem_onDoubleClick', 'click' : '_listBackground_onClick', 'click ul.collection-list, table.collection-list' : '_listBackground_onClick', diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 9a5b65cd9..45edb91a0 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -1,5 +1,6 @@ @import "../../Shared/Styles/card"; @import "../../Content/Bootstrap/mixins"; +@import "../../Content/FontAwesome/font-awesome"; .quality-profiles { li { @@ -36,7 +37,7 @@ } } -ul.allowed-list { +ul.qualities { .user-select(none); min-height: 100px; @@ -44,49 +45,65 @@ ul.allowed-list { padding: 0; list-style-type: none; outline: none; + width: 220px; + display: inline-block; li { margin: 2px; - padding: 2px; + padding: 2px 4px; line-height: 20px; - border: 1px solid #AAA; + border: 1px solid #aaaaaa; border-radius: 4px; /* may need vendor varients */ - background: #FAFAFA; + background: #fafafa; cursor: pointer; - + + &.selected { + .select-handle { + opacity: 1.0; + cursor: pointer; + } + + .quality-label { + color: #444444; + } + + .select-handle:before { + .icon(@check); + } + } + + &:hover { + border-color: #888888; + background: #eeeeee; + + .drag-handle { + opacity: 1.0; + cursor: pointer; + } + } + .quality-label { - color: #CCC; + color: #c6c6c6; } .drag-handle, .select-handle { opacity: 0.2; line-height: 20px; cursor: pointer; } - - .drag-handle:hover { - opacity: 1.0; - cursor: pointer; + + .select-handle:before { + .icon(@check-empty); + display: inline-block; + width: 16px; + margin-top: 1px; } } - - li.selected { - .select-handle { - opacity: 1.0; - cursor: pointer; - } - - .quality-label { - color: #444; - } - } - - li:hover { - border-color: #888; - background: #EEE; - - .select-handle { - opacity: 0.5; - } +} + +.qualities-controls { + .help-inline { + vertical-align: top; + margin-top: 5px; } } @@ -102,22 +119,22 @@ ul.allowed-list { border-top: 1px solid #ddd; vertical-align: middle; padding: 5px; - + input { margin-bottom: 0px; } - + .size-label-wrapper { line-height: 20px; } - + .label { min-width: 70px; text-align: center; margin: 0px 1px; padding: 1px 4px; } - + .ui-slider { position: relative; text-align: left; @@ -125,14 +142,14 @@ ul.allowed-list { border-radius: 3px; border: 1px solid #ccc; height: 8px; - + .ui-slider-range { position: absolute; display: block; background-color: #ddd; height: 100%; } - + .ui-slider-handle { position: absolute; z-index: 2; @@ -147,4 +164,3 @@ ul.allowed-list { } } } - diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 71948e76e..db90a0a7d 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -194,7 +194,7 @@ define( }, _setAdvancedSettingsState: function () { - var checked = Config.getValueBoolean('advancedSettings'); + var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings); this.ui.advancedSettings.prop('checked', checked); if (checked) { @@ -204,7 +204,7 @@ define( _toggleAdvancedSettings: function () { var checked = this.ui.advancedSettings.prop('checked'); - Config.setValue('advancedSettings', checked); + Config.setValue(Config.Keys.AdvancedSettings, checked); if (checked) { $('body').addClass('show-advanced-settings'); From 191db1c541494917aed5492024adde24bd23ec9d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 29 Jan 2014 20:45:30 -0800 Subject: [PATCH 08/48] Moved to regions for quality profile editor --- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Qualities/QualityProfileModule.cs | 13 +-- .../Qualities/QualityProfileValidation.cs | 43 +++++++++ .../{ => Edit}/EditQualityProfileItemView.js | 2 +- .../EditQualityProfileItemViewTemplate.html | 0 .../Profile/Edit/EditQualityProfileLayout.js | 92 +++++++++++++++++++ .../EditQualityProfileLayoutTemplate.html | 29 ++++++ .../Profile/Edit/EditQualityProfileView.js | 26 ++++++ .../Edit/EditQualityProfileViewTemplate.html | 21 +++++ .../Edit/QualitySortableCollectionView.js | 24 +++++ .../Quality/Profile/EditQualityProfileView.js | 89 ------------------ .../EditQualityProfileViewTemplate.html | 49 ---------- .../Profile/QualityProfileCollectionView.js | 2 +- .../Quality/Profile/QualityProfileView.js | 4 +- ...e.html => QualityProfileViewTemplate.html} | 0 .../Profile/QualitySortableCollectionView.js | 27 ------ src/UI/Settings/Quality/quality.less | 2 +- 17 files changed, 243 insertions(+), 181 deletions(-) create mode 100644 src/NzbDrone.Api/Qualities/QualityProfileValidation.cs rename src/UI/Settings/Quality/Profile/{ => Edit}/EditQualityProfileItemView.js (61%) rename src/UI/Settings/Quality/Profile/{ => Edit}/EditQualityProfileItemViewTemplate.html (100%) create mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js create mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html create mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js create mode 100644 src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html create mode 100644 src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js delete mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileView.js delete mode 100644 src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html rename src/UI/Settings/Quality/Profile/{QualityProfileTemplate.html => QualityProfileViewTemplate.html} (100%) delete mode 100644 src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index f6425304a..76c7cd2ad 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -137,6 +137,7 @@ + diff --git a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs index 395d67803..ee7ce95cb 100644 --- a/src/NzbDrone.Api/Qualities/QualityProfileModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityProfileModule.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using NzbDrone.Core.Qualities; using NzbDrone.Api.Mapping; -using System.Linq; using FluentValidation; namespace NzbDrone.Api.Qualities @@ -9,27 +8,19 @@ namespace NzbDrone.Api.Qualities public class QualityProfileModule : NzbDroneRestModule { private readonly IQualityProfileService _qualityProfileService; - private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityProfileModule(IQualityProfileService qualityProfileService, - IQualityDefinitionService qualityDefinitionService) + public QualityProfileModule(IQualityProfileService qualityProfileService) : base("/qualityprofiles") { _qualityProfileService = qualityProfileService; - _qualityDefinitionService = qualityDefinitionService; - SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).NotNull(); - SharedValidator.RuleFor(c => c.Items).NotEmpty(); + SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();//.SetValidator(new AllowedValidator()); GetResourceAll = GetAll; - GetResourceById = GetById; - UpdateResource = Update; - CreateResource = Create; - DeleteResource = DeleteProfile; } diff --git a/src/NzbDrone.Api/Qualities/QualityProfileValidation.cs b/src/NzbDrone.Api/Qualities/QualityProfileValidation.cs new file mode 100644 index 000000000..c90ebda61 --- /dev/null +++ b/src/NzbDrone.Api/Qualities/QualityProfileValidation.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace NzbDrone.Api.Qualities +{ + public static class QualityProfileValidation + { + public static IRuleBuilderOptions> MustHaveAllowedQuality(this IRuleBuilder> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new AllowedValidator()); + } + } + + public class AllowedValidator : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js similarity index 61% rename from src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js rename to src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js index e93fa6dee..dbd101a84 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileItemView.js +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemView.js @@ -4,6 +4,6 @@ define( 'marionette' ], function (Marionette) { return Marionette.ItemView.extend({ - template : 'Settings/Quality/Profile/EditQualityProfileItemViewTemplate' + template : 'Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate' }); }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Profile/EditQualityProfileItemViewTemplate.html rename to src/UI/Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate.html diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js new file mode 100644 index 000000000..48eb0b1f4 --- /dev/null +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js @@ -0,0 +1,92 @@ +'use strict'; +define( + [ + 'underscore', + 'vent', + 'marionette', + 'backbone', + 'Settings/Quality/Profile/Edit/EditQualityProfileItemView', + 'Settings/Quality/Profile/Edit/QualitySortableCollectionView', + 'Settings/Quality/Profile/Edit/EditQualityProfileView', + 'Config' + ], function (_, vent, Marionette, Backbone, EditQualityProfileItemView, QualitySortableCollectionView, EditQualityProfileView, Config) { + + return Marionette.Layout.extend({ + template: 'Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate', + + regions: { + fields : '#x-fields', + qualities: '#x-qualities' + }, + + events: { + 'click .x-save': '_saveQualityProfile' + }, + + initialize: function (options) { + this.profileCollection = options.profileCollection; + this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); + }, + + onShow: function () { + this.fieldsView = new EditQualityProfileView({ model: this.model }); + this._showFieldsView(); + + this.sortableListView = new QualitySortableCollectionView({ + selectable : true, + selectMultiple : true, + clickToSelect : true, + clickToToggle : true, + sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false), + + sortableOptions : { + handle: '.x-drag-handle' + }, + + collection: this.itemsCollection, + model : this.model + }); + + this.sortableListView.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; })); + this.qualities.show(this.sortableListView); + + this.listenTo(this.sortableListView, 'selectionChanged', this._selectionChanged); + this.listenTo(this.sortableListView, 'sortStop', this._updateModel); + }, + + _selectionChanged: function(newSelectedModels, oldSelectedModels) { + var addedModels = _.difference(newSelectedModels, oldSelectedModels); + var removeModels = _.difference(oldSelectedModels, newSelectedModels); + + _.each(removeModels, function(item) { item.set('allowed', false); }); + _.each(addedModels, function(item) { item.set('allowed', true); }); + + this._updateModel(); + }, + + _updateModel: function() { + this.model.set('items', this.itemsCollection.toJSON().reverse()); + + this._showFieldsView(); + }, + + _saveQualityProfile: function () { + var self = this; + var cutoff = this.fieldsView.getCutoff(); + this.model.set('cutoff', cutoff); + + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.profileCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _showFieldsView: function () { + this.fields.show(this.fieldsView); + } + }); + }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html new file mode 100644 index 000000000..4c10d0b0f --- /dev/null +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html @@ -0,0 +1,29 @@ + + + diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js new file mode 100644 index 000000000..1365de61c --- /dev/null +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileView.js @@ -0,0 +1,26 @@ +'use strict'; +define( + [ + 'underscore', + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (_, Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate', + + ui: { + cutoff : '.x-cutoff' + }, + + getCutoff: function () { + var self = this; + + return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)}); + } + }); + + AsValidatedView.call(view); + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html new file mode 100644 index 000000000..9be0285ea --- /dev/null +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html @@ -0,0 +1,21 @@ +
      + +
      + +
      +
      +
      + +
      + + + + +
      +
      \ No newline at end of file diff --git a/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js new file mode 100644 index 000000000..73c06c48b --- /dev/null +++ b/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js @@ -0,0 +1,24 @@ +'use strict'; +define( + [ + 'backbone.collectionview', + 'Settings/Quality/Profile/Edit/EditQualityProfileItemView' + ], function (BackboneSortableCollectionView, EditQualityProfileItemView) { + return BackboneSortableCollectionView.extend({ + + className: 'qualities', + modelView: EditQualityProfileItemView, + + attributes: { + 'validation-name': 'items' + }, + + events: { + 'click li, td' : '_listItem_onMousedown', + 'dblclick li, td' : '_listItem_onDoubleClick', + 'click' : '_listBackground_onClick', + 'click ul.collection-list, table.collection-list' : '_listBackground_onClick', + 'keydown' : '_onKeydown' + } + }); + }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js b/src/UI/Settings/Quality/Profile/EditQualityProfileView.js deleted file mode 100644 index a602e7dbd..000000000 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileView.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; -define( - [ - 'underscore', - 'vent', - 'marionette', - 'backbone', - 'Settings/Quality/Profile/QualitySortableCollectionView', - 'Settings/Quality/Profile/EditQualityProfileItemView', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'Config' - ], function (_, vent, Marionette, Backbone, QualitySortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, Config) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/EditQualityProfileViewTemplate', - - ui: { - allowed : '.x-allowed-list', - cutoff : '.x-cutoff' - }, - - events: { - 'click .x-save': '_saveQualityProfile' - }, - - initialize: function (options) { - this.profileCollection = options.profileCollection; - - this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); - }, - - onRender: function() { - - var listViewAllowed = new QualitySortableCollectionView({ - el : this.ui.allowed, - modelView : EditQualityProfileItemView, - selectable : true, - selectMultiple : true, - clickToSelect : true, - clickToToggle : true, - sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false), - collection : this.itemsCollection - }); - - listViewAllowed.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; })); - - listViewAllowed.render(); - - this.listenTo(listViewAllowed, 'selectionChanged', this._selectionChanged); - this.listenTo(listViewAllowed, 'sortStop', this._updateModel); - }, - - _selectionChanged: function(newSelectedModels, oldSelectedModels) { - var addedModels = _.difference(newSelectedModels, oldSelectedModels); - var removeModels = _.difference(oldSelectedModels, newSelectedModels); - - _.each(removeModels, function(item) { item.set('allowed', false); }); - _.each(addedModels, function(item) { item.set('allowed', true); }); - - this._updateModel(); - }, - - _updateModel: function() { - this.model.set('items', this.itemsCollection.toJSON().reverse()); - - this.render(); - }, - - _saveQualityProfile: function () { - var self = this; - var cutoff = _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)}); - this.model.set('cutoff', cutoff); - - var promise = this.model.save(); - - if (promise) { - promise.done(function () { - self.profileCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - } - } - }); - - AsValidatedView.call(view); - return AsModelBoundView.call(view); - - }); diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html deleted file mode 100644 index 619f6c873..000000000 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileViewTemplate.html +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js b/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js index 066bf986e..b3f5e12af 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js +++ b/src/UI/Settings/Quality/Profile/QualityProfileCollectionView.js @@ -3,7 +3,7 @@ define(['AppLayout', 'marionette', 'Settings/Quality/Profile/QualityProfileView', - 'Settings/Quality/Profile/EditQualityProfileView', + 'Settings/Quality/Profile/Edit/EditQualityProfileLayout', 'Settings/Quality/Profile/QualityProfileSchemaCollection', 'underscore' ], function (AppLayout, Marionette, QualityProfileView, EditProfileView, ProfileCollection, _) { diff --git a/src/UI/Settings/Quality/Profile/QualityProfileView.js b/src/UI/Settings/Quality/Profile/QualityProfileView.js index 439ad62f4..4bf9e466d 100644 --- a/src/UI/Settings/Quality/Profile/QualityProfileView.js +++ b/src/UI/Settings/Quality/Profile/QualityProfileView.js @@ -4,7 +4,7 @@ define( [ 'AppLayout', 'marionette', - 'Settings/Quality/Profile/EditQualityProfileView', + 'Settings/Quality/Profile/Edit/EditQualityProfileLayout', 'Settings/Quality/Profile/DeleteView', 'Series/SeriesCollection', 'Mixins/AsModelBoundView', @@ -13,7 +13,7 @@ define( ], function (AppLayout, Marionette, EditProfileView, DeleteProfileView, SeriesCollection, AsModelBoundView) { var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Profile/QualityProfileTemplate', + template: 'Settings/Quality/Profile/QualityProfileViewTemplate', tagName : 'li', ui: { diff --git a/src/UI/Settings/Quality/Profile/QualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Profile/QualityProfileTemplate.html rename to src/UI/Settings/Quality/Profile/QualityProfileViewTemplate.html diff --git a/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js deleted file mode 100644 index 0185d82c0..000000000 --- a/src/UI/Settings/Quality/Profile/QualitySortableCollectionView.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; -define( - [ - 'backbone.collectionview' - ], function (BackboneSortableCollectionView) { - return BackboneSortableCollectionView.extend({ - - events : { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'click' : '_listBackground_onClick', - 'click ul.collection-list, table.collection-list' : '_listBackground_onClick', - 'keydown' : '_onKeydown', - 'click .x-move' : '_onClickMove' - }, - - _onClickMove: function( theEvent ) { - var clickedItemId = this._getClickedItemId( theEvent ); - - if( clickedItemId ) - { - var clickedModel = this.collection.get( clickedItemId ); - this.trigger('moveClicked', clickedModel); - } - } - }); - }); diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less index 45edb91a0..199f87201 100644 --- a/src/UI/Settings/Quality/quality.less +++ b/src/UI/Settings/Quality/quality.less @@ -78,7 +78,7 @@ ul.qualities { .drag-handle { opacity: 1.0; - cursor: pointer; + cursor: move; } } From c4fc3e77b39e586c233a04ef5602a11e57ae8c6d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 29 Jan 2014 20:58:35 -0800 Subject: [PATCH 09/48] Cancelling quality profile editing will refetch it from the server --- .../Profile/Edit/EditQualityProfileLayout.js | 18 +++++++++++++++++- .../Edit/EditQualityProfileLayoutTemplate.html | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js index 48eb0b1f4..1ef9825d5 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayout.js @@ -20,7 +20,8 @@ define( }, events: { - 'click .x-save': '_saveQualityProfile' + 'click .x-save' : '_saveQualityProfile', + 'click .x-cancel': '_cancelQualityProfile' }, initialize: function (options) { @@ -85,6 +86,21 @@ define( } }, + _cancelQualityProfile: function () { + if (!this.model.has('id')) { + vent.trigger(vent.Commands.CloseModalCommand); + return; + } + + var promise = this.model.fetch(); + + if (promise) { + promise.done(function () { + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + _showFieldsView: function () { this.fields.show(this.fieldsView); } diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html index 4c10d0b0f..1cbfffda2 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html @@ -1,5 +1,5 @@  From 31e052438967d9b2cbd5d345c4e38e55d25e6c31 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 30 Jan 2014 20:09:10 +0100 Subject: [PATCH 10/48] Disable background click to prevent deselection of all items. --- .../Quality/Profile/Edit/QualitySortableCollectionView.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js index 73c06c48b..03693ec29 100644 --- a/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js +++ b/src/UI/Settings/Quality/Profile/Edit/QualitySortableCollectionView.js @@ -16,8 +16,6 @@ define( events: { 'click li, td' : '_listItem_onMousedown', 'dblclick li, td' : '_listItem_onDoubleClick', - 'click' : '_listBackground_onClick', - 'click ul.collection-list, table.collection-list' : '_listBackground_onClick', 'keydown' : '_onKeydown' } }); From 45304b1bbc89e4101aa702d7e1599fb360f085f6 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 2 Feb 2014 00:38:19 +0100 Subject: [PATCH 11/48] Solved error in quality comparison for HistoryService --- .../HistorySpecificationFixture.cs | 12 ++-- .../HistoryTests/HistoryServiceFixture.cs | 59 +++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../RssSync/HistorySpecification.cs | 2 +- src/NzbDrone.Core/History/HistoryService.cs | 9 ++- 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 4013271c9..54c8ff6e0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -62,9 +62,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _upgradableQuality = new QualityModel(Quality.SDTV, false); _notupgradableQuality = new QualityModel(Quality.HDTV1080p, true); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_notupgradableQuality); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_notupgradableQuality); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(3)).Returns(null); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_notupgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_notupgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 3)).Returns(null); Mocker.GetMock() .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); @@ -72,12 +72,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithFirstReportUpgradable() { - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); } private void WithSecondReportUpgradable() { - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 2)).Returns(_upgradableQuality); } private void GivenSabnzbdDownloadClient() @@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL1080p, false); _upgradableQuality = new QualityModel(Quality.WEBDL1080p, false); - Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_upgradableQuality); + Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 1)).Returns(_upgradableQuality); _upgradeHistory.IsSatisfiedBy(_parseResultSingle, null).Should().BeFalse(); } diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs new file mode 100644 index 000000000..717f9f915 --- /dev/null +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.History; +using NzbDrone.Core.Qualities; +using System.Collections.Generic; +using NzbDrone.Core.Test.Qualities; +using FluentAssertions; + +namespace NzbDrone.Core.Test.HistoryTests +{ + public class HistoryServiceFixture : CoreTest + { + private QualityProfile _profile; + private QualityProfile _profileCustom; + + [SetUp] + public void Setup() + { + _profile = new QualityProfile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities() }; + _profileCustom = new QualityProfile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities(Quality.DVD) }; + } + + [Test] + public void should_return_null_if_no_history() + { + Mocker.GetMock() + .Setup(v => v.GetBestQualityInHistory(2)) + .Returns(new List()); + + var quality = Subject.GetBestQualityInHistory(_profile, 2); + + quality.Should().BeNull(); + } + + [Test] + public void should_return_best_quality() + { + Mocker.GetMock() + .Setup(v => v.GetBestQualityInHistory(2)) + .Returns(new List { new QualityModel(Quality.DVD), new QualityModel(Quality.Bluray1080p) }); + + var quality = Subject.GetBestQualityInHistory(_profile, 2); + + quality.Should().Be(new QualityModel(Quality.Bluray1080p)); + } + + [Test] + public void should_return_best_quality_with_custom_order() + { + Mocker.GetMock() + .Setup(v => v.GetBestQualityInHistory(2)) + .Returns(new List { new QualityModel(Quality.DVD), new QualityModel(Quality.Bluray1080p) }); + + var quality = Subject.GetBestQualityInHistory(_profileCustom, 2); + + quality.Should().Be(new QualityModel(Quality.DVD)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 3e406aef7..568e6feb6 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -130,6 +130,7 @@ + diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index a4c3b3745..808bba106 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync foreach (var episode in subject.Episodes) { - var bestQualityInHistory = _historyService.GetBestQualityInHistory(episode.Id); + var bestQualityInHistory = _historyService.GetBestQualityInHistory(subject.Series.QualityProfile, episode.Id); if (bestQualityInHistory != null) { _logger.Trace("Comparing history quality with report. History is {0}", bestQualityInHistory); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 07e32b824..4b3282b8f 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.History List All(); void Purge(); void Trim(); - QualityModel GetBestQualityInHistory(int episodeId); + QualityModel GetBestQualityInHistory(QualityProfile qualityProfile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); @@ -81,9 +81,12 @@ namespace NzbDrone.Core.History _historyRepository.Trim(); } - public QualityModel GetBestQualityInHistory(int episodeId) + public QualityModel GetBestQualityInHistory(QualityProfile qualityProfile, int episodeId) { - return _historyRepository.GetBestQualityInHistory(episodeId).OrderByDescending(q => q).FirstOrDefault(); + var comparer = new QualityModelComparer(qualityProfile); + return _historyRepository.GetBestQualityInHistory(episodeId) + .OrderByDescending(q => q, comparer) + .FirstOrDefault(); } public void Handle(EpisodeGrabbedEvent message) From 1eae184b43f2d508e900edbbbda4afea4a5a0796 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 3 Feb 2014 17:31:18 -0800 Subject: [PATCH 12/48] Fixed up form-info icons after merge --- .../Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html | 2 +- .../Quality/Profile/Edit/EditQualityProfileViewTemplate.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html index 1cbfffda2..7f6f1359f 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate.html @@ -14,7 +14,7 @@
      - +
      diff --git a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html index 9be0285ea..87ba4deef 100644 --- a/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html +++ b/src/UI/Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate.html @@ -15,7 +15,7 @@ {{/eachReverse}} - + \ No newline at end of file From fe4f3d5d1e0d8f976467ba96e9b99fe078bd62fb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 3 Feb 2014 17:42:35 -0800 Subject: [PATCH 13/48] Fixed namespace --- src/NzbDrone.Api/Blacklist/BlacklistResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs index f2c27f2c4..b9638cf9c 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using NzbDrone.Api.REST; -using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Blacklist { From c9a77e99a0ac58852a73a41898a117a5cedc2dad Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 4 Feb 2014 18:46:03 -0800 Subject: [PATCH 14/48] Error handling in migration to new quality --- src/NzbDrone.Common/Serializer/Json.cs | 13 ++++++++++--- .../Migration/036_update_with_quality_converters.cs | 13 ++++++++++--- .../Clients/Sabnzbd/SabCommunicationProxy.cs | 10 +++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Common/Serializer/Json.cs b/src/NzbDrone.Common/Serializer/Json.cs index 9a9b0e04b..1abb2019b 100644 --- a/src/NzbDrone.Common/Serializer/Json.cs +++ b/src/NzbDrone.Common/Serializer/Json.cs @@ -41,15 +41,22 @@ namespace NzbDrone.Common.Serializer return JsonConvert.DeserializeObject(json, type, SerializerSetting); } - public static T TryDeserialize(string json) where T : new() + public static bool TryDeserialize(string json, out T result) where T : new() { try { - return Deserialize(json); + result = Deserialize(json); + return true; } catch (JsonReaderException ex) { - return default(T); + result = default(T); + return false; + } + catch (JsonSerializationException ex) + { + result = default(T); + return false; } } diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index 090ddd33a..a89c715e0 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -14,10 +14,12 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("QualityProfiles").AddColumn("Items").AsString().Nullable(); + if (!Schema.Table("QualityProfiles").Column("Items").Exists()) + { + Alter.Table("QualityProfiles").AddColumn("Items").AsString().Nullable(); + } Execute.WithConnection(ConvertQualityProfiles); - Execute.WithConnection(ConvertQualityModels); } @@ -80,7 +82,12 @@ namespace NzbDrone.Core.Datastore.Migration var id = qualityModelReader.GetInt32(0); var qualityJson = qualityModelReader.GetString(1); - var quality = Json.Deserialize(qualityJson); + QualityModel quality; + + if (!Json.TryDeserialize(qualityJson, out quality)) + { + continue; + } var qualityNewJson = qualityModelConverter.ToDB(quality); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs index a372185eb..8e181a25e 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs @@ -31,10 +31,10 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); - - var response = Json.TryDeserialize(ProcessRequest(request, action)); - if (response == null) + SabAddResponse response; + + if (!Json.TryDeserialize(ProcessRequest(request, action), out response)) { response = new SabAddResponse(); response.Status = true; @@ -87,9 +87,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd throw new ApplicationException("Unable to connect to SABnzbd, please check your settings"); } - var result = Json.TryDeserialize(response.Content); + SabJsonError result; - if (result == null) + if (!Json.TryDeserialize(response.Content, out result)) { //Handle plain text responses from SAB result = new SabJsonError(); From 03ba8d071e011096af282dd585d3495d7eae8ed3 Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 19:17:42 -0800 Subject: [PATCH 15/48] Migration to new quality takes seconds not minutes now --- .../Migration/036_update_with_quality_converters.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index a89c715e0..ea6f38fdd 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -74,13 +74,12 @@ namespace NzbDrone.Core.Datastore.Migration using (IDbCommand qualityModelCmd = conn.CreateCommand()) { qualityModelCmd.Transaction = tran; - qualityModelCmd.CommandText = @"SELECT Id, Quality FROM " + tableName; + qualityModelCmd.CommandText = @"SELECT Distinct Quality FROM " + tableName; using (IDataReader qualityModelReader = qualityModelCmd.ExecuteReader()) { while (qualityModelReader.Read()) { - var id = qualityModelReader.GetInt32(0); - var qualityJson = qualityModelReader.GetString(1); + var qualityJson = qualityModelReader.GetString(0); QualityModel quality; @@ -94,9 +93,9 @@ namespace NzbDrone.Core.Datastore.Migration using (IDbCommand updateCmd = conn.CreateCommand()) { updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Id = ?"; + updateCmd.CommandText = "UPDATE " + tableName + " SET Quality = ? WHERE Quality = ?"; updateCmd.AddParameter(qualityNewJson); - updateCmd.AddParameter(id); + updateCmd.AddParameter(qualityJson); updateCmd.ExecuteNonQuery(); } From 3d423f57f8c3fd8438d36fd3083c28fe372fad1e Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 20:20:00 -0800 Subject: [PATCH 16/48] Fixed: Use friendly name instead of "Newznab" when fetching feeds --- src/NzbDrone.Core/Indexers/IndexerBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index bca173fcc..e59bcf4c9 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Indexers } public virtual IParseFeed Parser { get; private set; } - + public abstract IEnumerable RecentFeed { get; } public abstract IEnumerable GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); public abstract IEnumerable GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date); @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Indexers public override string ToString() { - return GetType().Name; + return Definition.Name; } } From 9ee535328f81a3e5cf6c0a0d59367e12176925f4 Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 20:23:29 -0800 Subject: [PATCH 17/48] Fixed: Order Upcoming by date and time --- src/NzbDrone.Api/Calendar/CalendarModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index 8a7df0dab..141961ad5 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -51,7 +51,7 @@ namespace NzbDrone.Api.Calendar var resources = ToListResource(() => _episodeService.EpisodesBetweenDates(start, end)) .LoadSubtype(e => e.SeriesId, _seriesRepository); - return resources.OrderBy(e => e.AirDate).ToList(); + return resources.OrderBy(e => e.AirDateUtc).ToList(); } public void Handle(EpisodeGrabbedEvent message) From 5a673a33ab65322ff3488db25f34cd1c0df1fa05 Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 20:36:16 -0800 Subject: [PATCH 18/48] Moved /api/episodes to /api/episode to align with other endpoints --- src/NzbDrone.Api/Episodes/EpisodeModule.cs | 2 +- src/UI/Series/EpisodeCollection.js | 2 +- src/UI/Series/EpisodeModel.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs index 46f6b7f94..91589b850 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Api.Episodes private readonly IEpisodeService _episodeService; public EpisodeModule(ICommandExecutor commandExecutor, IEpisodeService episodeService) - : base(commandExecutor, "episodes") + : base(commandExecutor) { _episodeService = episodeService; diff --git a/src/UI/Series/EpisodeCollection.js b/src/UI/Series/EpisodeCollection.js index 8c72b2440..570a72de7 100644 --- a/src/UI/Series/EpisodeCollection.js +++ b/src/UI/Series/EpisodeCollection.js @@ -5,7 +5,7 @@ define( 'Series/EpisodeModel' ], function (Backbone, EpisodeModel) { return Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/episodes', + url : window.NzbDrone.ApiRoot + '/episode', model: EpisodeModel, state: { diff --git a/src/UI/Series/EpisodeModel.js b/src/UI/Series/EpisodeModel.js index 9a040715d..8f6459d47 100644 --- a/src/UI/Series/EpisodeModel.js +++ b/src/UI/Series/EpisodeModel.js @@ -11,7 +11,7 @@ define( }, methodUrls: { - 'update': window.NzbDrone.ApiRoot + '/episodes' + 'update': window.NzbDrone.ApiRoot + '/episode' }, sync: function(method, model, options) { From cdbd7273b7c64471c1e81dde90422e0d2ae39c09 Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 21:29:43 -0800 Subject: [PATCH 19/48] Fixed broken episode integration tests --- src/NzbDrone.Integration.Test/Client/EpisodeClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs b/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs index 437226c2f..46d0b8e03 100644 --- a/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs +++ b/src/NzbDrone.Integration.Test/Client/EpisodeClient.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Integration.Test.Client public class EpisodeClient : ClientBase { public EpisodeClient(IRestClient restClient, string apiKey) - : base(restClient, apiKey, "episodes") + : base(restClient, apiKey, "episode") { } From f9b0b2206c809ca317303eeefe64e6d2df0344d0 Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 21:29:57 -0800 Subject: [PATCH 20/48] Added RescanSeries command --- .../MediaFiles/Commands/RescanSeriesCommand.cs | 17 +++++++++++++++++ src/NzbDrone.Core/MediaFiles/DiskScanService.cs | 16 ++++++++++++++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs new file mode 100644 index 000000000..81ee0951f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RescanSeriesCommand : Command + { + public int SeriesId { get; set; } + + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 6c237fa6d..0b1e51be6 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using NLog; using NzbDrone.Common; @@ -21,13 +22,15 @@ namespace NzbDrone.Core.MediaFiles public class DiskScanService : IDiskScanService, - IHandle + IHandle, + IExecute { private readonly IDiskProvider _diskProvider; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly ICommandExecutor _commandExecutor; private readonly IConfigService _configService; + private readonly ISeriesService _seriesService; private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, @@ -35,6 +38,7 @@ namespace NzbDrone.Core.MediaFiles IImportApprovedEpisodes importApprovedEpisodes, ICommandExecutor commandExecutor, IConfigService configService, + ISeriesService seriesService, Logger logger) { _diskProvider = diskProvider; @@ -42,6 +46,7 @@ namespace NzbDrone.Core.MediaFiles _importApprovedEpisodes = importApprovedEpisodes; _commandExecutor = commandExecutor; _configService = configService; + _seriesService = seriesService; _logger = logger; } @@ -90,5 +95,12 @@ namespace NzbDrone.Core.MediaFiles { Scan(message.Series); } + + public void Execute(RescanSeriesCommand message) + { + var series = _seriesService.GetSeries(message.SeriesId); + + Scan(series); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 16b44a898..f52c84c8c 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -289,6 +289,7 @@ + From 16dd5b621ead55fc9ea8289fb2547b9daa9ce7de Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 21:52:53 -0800 Subject: [PATCH 21/48] ScheduledTasks won't run immediately after first start --- src/NzbDrone.Core/Jobs/TaskManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 0470c17ee..e40e08f8e 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, }; - var currentTasks = _scheduledTaskRepository.All(); + var currentTasks = _scheduledTaskRepository.All().ToList(); _logger.Debug("Initializing jobs. Available: {0} Existing:{1}", defaultTasks.Count(), currentTasks.Count()); @@ -76,6 +76,11 @@ namespace NzbDrone.Core.Jobs currentDefinition.Interval = defaultTask.Interval; + if (currentDefinition.Id == 0) + { + currentDefinition.LastExecution = DateTime.UtcNow; + } + _scheduledTaskRepository.Upsert(currentDefinition); } } From 2b5c512d644418bbec29671697466aa8cc81fbfe Mon Sep 17 00:00:00 2001 From: markus101 Date: Tue, 4 Feb 2014 23:08:33 -0800 Subject: [PATCH 22/48] Get series from DB before publishing add series, so we can use LazyLoaded values --- src/NzbDrone.Core/Tv/SeriesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 5387f3704..b4fe33ea1 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Tv newSeries.CleanTitle = Parser.Parser.CleanSeriesTitle(newSeries.Title); _seriesRepository.Insert(newSeries); - _eventAggregator.PublishEvent(new SeriesAddedEvent(newSeries)); + _eventAggregator.PublishEvent(new SeriesAddedEvent(GetSeries(newSeries.Id))); return newSeries; } From d648056bc400ba13cff8a72adcc07b685d010f7e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Feb 2014 16:55:19 -0800 Subject: [PATCH 23/48] Better trace messaging for Download Decision specs Fixed: Trace logs will contain more details when a release is rejected --- .../Specifications/AcceptableSizeSpecification.cs | 2 +- .../Specifications/BlacklistSpecification.cs | 2 +- .../Specifications/CutoffSpecification.cs | 1 + .../Specifications/NotInQueueSpecification.cs | 7 ++++++- .../Specifications/NotSampleSpecification.cs | 10 +++++++++- .../Specifications/RssSync/HistorySpecification.cs | 1 + .../Specifications/RssSync/ProperSpecification.cs | 2 +- 7 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index e63edaa8c..88ea7550c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (quality == Quality.Unknown) { _logger.Trace("Unknown quality. skipping size check."); - return false; + return true; } var qualityDefinition = _qualityDefinitionService.Get(quality); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 635c95909..dc800715e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (_blacklistService.Blacklisted(subject.Release.Title)) { - _logger.Trace("{0} is blacklisted", subject.Release.Title); + _logger.Trace("{0} is blacklisted, rejecting.", subject.Release.Title); return false; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index f43df2836..e087c6ef3 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Series.QualityProfile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { + _logger.Trace("Cutoff already met, rejecting."); return false; } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index 3827f26a7..2b7f51ca0 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -40,7 +40,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications var queue = downloadClient.GetQueue().Select(q => q.RemoteEpisode); - return !IsInQueue(subject, queue); + if (IsInQueue(subject, queue)) + { + _logger.Trace("Already in queue, rejecting."); + } + + return true; } private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable queue) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index afcb7149a..5d6a2da77 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -1,16 +1,24 @@ -using NzbDrone.Core.IndexerSearch.Definitions; +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications { public class NotSampleSpecification : IDecisionEngineSpecification { + private readonly Logger _logger; public string RejectionReason { get { return "Sample"; } } + public NotSampleSpecification(Logger logger) + { + _logger = logger; + } + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (subject.Release.Title.ToLower().Contains("sample") && subject.Release.Size < 70.Megabytes()) { + _logger.Trace("Sample release, rejecting."); return false; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 808bba106..b2e034cac 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -51,6 +51,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) { + _logger.Trace("Latest history item is downloading, rejecting."); return false; } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index f0a133409..6d099eeba 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { if (file.DateAdded < DateTime.Today.AddDays(-7)) { - _logger.Trace("Proper for old file, skipping: {0}", subject); + _logger.Trace("Proper for old file, rejecting: {0}", subject); return false; } From 235bbc2d91b2580e464acc16fb8851a5dccc93d0 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Feb 2014 23:39:28 -0800 Subject: [PATCH 24/48] Single quotes around index column name won't die --- .../Datastore/SQLiteMigrationHelperTests/AlterFixture.cs | 1 - .../Datastore/Migration/Framework/SQLiteMigrationHelper.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs index d93d0f262..17b45da97 100644 --- a/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/SQLiteMigrationHelperTests/AlterFixture.cs @@ -105,7 +105,6 @@ namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests newIndexes.Select(c=>c.Column).Should().BeEquivalentTo(indexes.Select(c=>c.Column)); } - [Test] public void should_be_able_to_create_table_with_new_indexes() { diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs index 42f8f8e7b..3bb08dace 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/SQLiteMigrationHelper.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework private static readonly Regex SchemaRegex = new Regex(@"['\""\[](?\w+)['\""\]]\s(?[\w-\s]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); - private static readonly Regex IndexRegex = new Regex(@"\(""(?.*)""\s(?ASC|DESC)\)$", + private static readonly Regex IndexRegex = new Regex(@"\((?:""|')(?.*)(?:""|')\s(?ASC|DESC)\)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline); public SqLiteMigrationHelper(IConnectionStringFactory connectionStringFactory, Logger logger) @@ -96,8 +96,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework var reader = command.ExecuteReader(); var sqls = ReadArray(reader).ToList(); - - var indexes = new List(); foreach (var indexSql in sqls) @@ -105,6 +103,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework var newIndex = new SQLiteIndex(); var matches = IndexRegex.Match(indexSql); + if (!matches.Success) continue;; + newIndex.Column = matches.Groups["col"].Value; newIndex.Unique = indexSql.Contains("UNIQUE"); newIndex.Table = tableName; From 823ace02a6fb132b96268aa53216780d85cc2aae Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Feb 2014 23:43:58 -0800 Subject: [PATCH 25/48] Fixed not in queue spec --- .../DecisionEngine/Specifications/NotInQueueSpecification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index 2b7f51ca0..fa6312258 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (IsInQueue(subject, queue)) { _logger.Trace("Already in queue, rejecting."); + return false; } return true; From 4d40ce88c297b462ce0ad63507f014c7962506cc Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Feb 2014 23:45:00 -0800 Subject: [PATCH 26/48] Return false for size when quality is unknown --- .../Specifications/AcceptableSizeSpecification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 88ea7550c..e63edaa8c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (quality == Quality.Unknown) { _logger.Trace("Unknown quality. skipping size check."); - return true; + return false; } var qualityDefinition = _qualityDefinitionService.Get(quality); From 119a4ff39c850927b067854eb5c0752509e42b0e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 6 Feb 2014 13:31:09 -0800 Subject: [PATCH 27/48] Proper wiki link for add series --- src/UI/AddSeries/NotFoundTemplate.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/AddSeries/NotFoundTemplate.html b/src/UI/AddSeries/NotFoundTemplate.html index cfe1e34e6..2b0d75a9b 100644 --- a/src/UI/AddSeries/NotFoundTemplate.html +++ b/src/UI/AddSeries/NotFoundTemplate.html @@ -2,6 +2,6 @@

      Sorry. We couldn't find any series matching '{{term}}'

      - Why can't I find my show? + Why can't I find my show? From f69bb790771c85f90ddd7f93cf16376e7d7b9d1d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 12 Jan 2014 17:14:57 -0800 Subject: [PATCH 28/48] Shutdown! Restart working for services --- .../EnvironmentInfo/RuntimeInfo.cs | 32 +++++++---- .../Instrumentation/LogTargets.cs | 3 +- src/NzbDrone.Common/ServiceProvider.cs | 16 ++++++ .../Lifecycle/ApplicationRestartRequested.cs | 9 ++++ .../Lifecycle/Commands/RestartCommand.cs | 8 +++ .../Lifecycle/Commands/ShutdownCommand.cs | 8 +++ .../Lifecycle/LifestyleService.cs | 53 +++++++++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 4 ++ src/NzbDrone.Host/ApplicationServer.cs | 30 +++++++---- src/NzbDrone.Host/Bootstrap.cs | 4 +- src/NzbDrone/SysTray/SysTrayApp.cs | 10 +++- 11 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs create mode 100644 src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs create mode 100644 src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs create mode 100644 src/NzbDrone.Core/Lifecycle/LifestyleService.cs diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 7f81d23fd..4937ea1cb 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -13,25 +13,23 @@ namespace NzbDrone.Common.EnvironmentInfo bool IsUserInteractive { get; } bool IsAdmin { get; } bool IsWindowsService { get; } + bool IsConsole { get; } + bool IsRunning { get; set; } } public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; + private static readonly string ProcessName = Process.GetCurrentProcess().ProcessName.ToLower(); public RuntimeInfo(Logger logger, IServiceProvider serviceProvider) { _logger = logger; IsWindowsService = !IsUserInteractive && - OsInfo.IsWindows && - serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && - serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; - } - - public bool IsUserInteractive - { - get { return Environment.UserInteractive; } + OsInfo.IsWindows && + serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && + serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; } static RuntimeInfo() @@ -39,6 +37,11 @@ namespace NzbDrone.Common.EnvironmentInfo IsProduction = InternalIsProduction(); } + public bool IsUserInteractive + { + get { return Environment.UserInteractive; } + } + public bool IsAdmin { get @@ -58,7 +61,18 @@ namespace NzbDrone.Common.EnvironmentInfo public bool IsWindowsService { get; private set; } - private static readonly string ProcessName = Process.GetCurrentProcess().ProcessName.ToLower(); + public bool IsConsole + { + get + { + return (OsInfo.IsWindows && + IsUserInteractive && + ProcessName.Equals("NzbDrone.Console.exe", StringComparison.InvariantCultureIgnoreCase)) || + OsInfo.IsLinux; + } + } + + public bool IsRunning { get; set; } public static bool IsProduction { get; private set; } diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 514caf506..f44ce456c 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -5,6 +5,7 @@ using NLog; using NLog.Config; using NLog.Targets; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; namespace NzbDrone.Common.Instrumentation { @@ -30,7 +31,7 @@ namespace NzbDrone.Common.Instrumentation } else { - if (inConsole && (OsInfo.IsLinux || new RuntimeInfo(null, new ServiceProvider()).IsUserInteractive)) + if (inConsole && (OsInfo.IsLinux || new RuntimeInfo(null, new ServiceProvider(new ProcessProvider())).IsUserInteractive)) { RegisterConsole(); } diff --git a/src/NzbDrone.Common/ServiceProvider.cs b/src/NzbDrone.Common/ServiceProvider.cs index e669321cf..1137017f8 100644 --- a/src/NzbDrone.Common/ServiceProvider.cs +++ b/src/NzbDrone.Common/ServiceProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.ServiceProcess; using NLog; using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Processes; namespace NzbDrone.Common { @@ -20,14 +21,22 @@ namespace NzbDrone.Common void Stop(string serviceName); void Start(string serviceName); ServiceControllerStatus GetStatus(string serviceName); + void Restart(string serviceName); } public class ServiceProvider : IServiceProvider { public const string NZBDRONE_SERVICE_NAME = "NzbDrone"; + private readonly IProcessProvider _processProvider; + private static readonly Logger Logger = NzbDroneLogger.GetLogger(); + public ServiceProvider(IProcessProvider processProvider) + { + _processProvider = processProvider; + } + public virtual bool ServiceExist(string name) { Logger.Debug("Checking if service {0} exists.", name); @@ -173,5 +182,12 @@ namespace NzbDrone.Common Logger.Error("Service start request has timed out. {0}", service.Status); } } + + public void Restart(string serviceName) + { + var args = String.Format("/C net.exe stop \"{0}\" && net.exe start \"{0}\"", serviceName); + + _processProvider.Start("cmd.exe", args); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs b/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs new file mode 100644 index 000000000..7aa08bb1f --- /dev/null +++ b/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs @@ -0,0 +1,9 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Lifecycle +{ + public class ApplicationRestartRequested : IEvent + { + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs new file mode 100644 index 000000000..82c20cc07 --- /dev/null +++ b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs @@ -0,0 +1,8 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Lifecycle.Commands +{ + public class RestartCommand : Command + { + } +} diff --git a/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs new file mode 100644 index 000000000..b0fffd8e5 --- /dev/null +++ b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs @@ -0,0 +1,8 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Lifecycle.Commands +{ + public class ShutdownCommand : Command + { + } +} diff --git a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs new file mode 100644 index 000000000..d08aee767 --- /dev/null +++ b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs @@ -0,0 +1,53 @@ +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; +using NzbDrone.Core.Lifecycle.Commands; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using IServiceProvider = NzbDrone.Common.IServiceProvider; + +namespace NzbDrone.Core.Lifecycle +{ + public class LifestyleService: IExecute, IExecute + { + private readonly IEventAggregator _eventAggregator; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IServiceProvider _serviceProvider; + private readonly IProcessProvider _processProvider; + + + public LifestyleService(IEventAggregator eventAggregator, + IRuntimeInfo runtimeInfo, + IServiceProvider serviceProvider, + IProcessProvider processProvider) + { + _eventAggregator = eventAggregator; + _runtimeInfo = runtimeInfo; + _serviceProvider = serviceProvider; + _processProvider = processProvider; + } + + public void Execute(ShutdownCommand message) + { + if (_runtimeInfo.IsWindowsService) + { + _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); + } + + else + { + _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); + } + } + + public void Execute(RestartCommand message) + { + if (_runtimeInfo.IsWindowsService) + { + _serviceProvider.Restart(ServiceProvider.NZBDRONE_SERVICE_NAME); + } + + _eventAggregator.PublishEvent(new ApplicationRestartRequested()); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f52c84c8c..800258bde 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -290,6 +290,10 @@ + + + + diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index 6ab53244f..b506e64fd 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -2,18 +2,19 @@ using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Host.Owin; namespace NzbDrone.Host { public interface INzbDroneServiceFactory { - bool IsServiceStopped { get; } ServiceBase Build(); void Start(); } - public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory + public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory, IHandle { private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; @@ -42,6 +43,7 @@ namespace NzbDrone.Host public void Start() { + _runtimeInfo.IsRunning = true; _hostController.StartServer(); if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER) @@ -55,18 +57,28 @@ namespace NzbDrone.Host protected override void OnStop() { - _logger.Info("Attempting to stop application."); - _hostController.StopServer(); - _logger.Info("Application has finished stop routine."); - IsServiceStopped = true; + Shutdown(); } - public bool IsServiceStopped { get; private set; } - public ServiceBase Build() { return this; } - } + private void Shutdown() + { + _logger.Info("Attempting to stop application."); + _hostController.StopServer(); + _logger.Info("Application has finished stop routine."); + _runtimeInfo.IsRunning = false; + } + + public void Handle(ApplicationShutdownRequested message) + { + if (!_runtimeInfo.IsWindowsService) + { + Shutdown(); + } + } + } } \ No newline at end of file diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 7909ea526..87448075b 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -72,9 +72,9 @@ namespace NzbDrone.Host return; } - var serviceFactory = _container.Resolve(); + var runTimeInfo = _container.Resolve(); - while (!serviceFactory.IsServiceStopped) + while (runTimeInfo.IsRunning) { Thread.Sleep(1000); } diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index bfbc96cb2..730ea346f 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -68,7 +68,15 @@ namespace NzbDrone.SysTray _trayIcon.Dispose(); } - base.Dispose(isDisposing); + if (InvokeRequired) + { + base.Invoke(new MethodInvoker(() => Dispose(isDisposing))); + } + + else + { + base.Dispose(isDisposing); + } } private void OnExit(object sender, EventArgs e) From aa1a76fe7281ea7a27a115ca00f5ddc6a0c94b8e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 13 Jan 2014 17:35:16 -0800 Subject: [PATCH 29/48] Restart for Windows --- .../EnvironmentInfo/RuntimeInfo.cs | 4 +- .../EnvironmentInfo/StartupContext.cs | 2 +- .../Lifecycle/ApplicationRestartRequested.cs | 9 ----- .../Lifecycle/ApplicationShutdownRequested.cs | 9 +++++ .../Lifecycle/LifestyleService.cs | 39 +++++++++++++++---- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 - src/NzbDrone.Host/ApplicationServer.cs | 2 +- src/NzbDrone.Host/Bootstrap.cs | 12 ++++-- 8 files changed, 53 insertions(+), 25 deletions(-) delete mode 100644 src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 4937ea1cb..aec416e61 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -4,10 +4,10 @@ using System.IO; using System.Security.Principal; using System.ServiceProcess; using NLog; +using NzbDrone.Common.Processes; namespace NzbDrone.Common.EnvironmentInfo { - public interface IRuntimeInfo { bool IsUserInteractive { get; } @@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo { return (OsInfo.IsWindows && IsUserInteractive && - ProcessName.Equals("NzbDrone.Console.exe", StringComparison.InvariantCultureIgnoreCase)) || + ProcessName.Equals(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase)) || OsInfo.IsLinux; } } diff --git a/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs b/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs index 0702fc861..3331b39e4 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/StartupContext.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Common.EnvironmentInfo internal const string INSTALL_SERVICE = "i"; internal const string UNINSTALL_SERVICE = "u"; public const string HELP = "?"; + public const string TERMINATE = "terminateexisting"; public StartupContext(params string[] args) { @@ -58,6 +59,5 @@ namespace NzbDrone.Common.EnvironmentInfo return Flags.Contains(UNINSTALL_SERVICE); } } - } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs b/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs deleted file mode 100644 index 7aa08bb1f..000000000 --- a/src/NzbDrone.Core/Lifecycle/ApplicationRestartRequested.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NzbDrone.Common.Messaging; - -namespace NzbDrone.Core.Lifecycle -{ - public class ApplicationRestartRequested : IEvent - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs index 6c343546e..50446ed1d 100644 --- a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs +++ b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs @@ -4,6 +4,15 @@ namespace NzbDrone.Core.Lifecycle { public class ApplicationShutdownRequested : IEvent { + public bool Restarting { get; set; } + public ApplicationShutdownRequested() + { + } + + public ApplicationShutdownRequested(bool restarting) + { + Restarting = restarting; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs index d08aee767..bc09bf877 100644 --- a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common; +using System.IO; +using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; using NzbDrone.Core.Lifecycle.Commands; @@ -12,42 +13,66 @@ namespace NzbDrone.Core.Lifecycle { private readonly IEventAggregator _eventAggregator; private readonly IRuntimeInfo _runtimeInfo; + private readonly IAppFolderInfo _appFolderInfo; private readonly IServiceProvider _serviceProvider; private readonly IProcessProvider _processProvider; public LifestyleService(IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, + IAppFolderInfo appFolderInfo, IServiceProvider serviceProvider, IProcessProvider processProvider) { _eventAggregator = eventAggregator; _runtimeInfo = runtimeInfo; + _appFolderInfo = appFolderInfo; _serviceProvider = serviceProvider; _processProvider = processProvider; } public void Execute(ShutdownCommand message) { + _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); + if (_runtimeInfo.IsWindowsService) { _serviceProvider.Stop(ServiceProvider.NZBDRONE_SERVICE_NAME); } - - else - { - _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); - } } public void Execute(RestartCommand message) { + _eventAggregator.PublishEvent(new ApplicationShutdownRequested(true)); + if (_runtimeInfo.IsWindowsService) { _serviceProvider.Restart(ServiceProvider.NZBDRONE_SERVICE_NAME); } - _eventAggregator.PublishEvent(new ApplicationRestartRequested()); + else + { + //TODO: move this to environment specific projects + if (OsInfo.IsWindows) + { + if (_runtimeInfo.IsConsole) + { + //Run console with switch + var path = Path.Combine(_appFolderInfo.StartUpFolder, + ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe"); + + _processProvider.SpawnNewProcess(path, "--terminateexisting --nobrowser"); + } + + else + { + var path = Path.Combine(_appFolderInfo.StartUpFolder, + ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe"); + + _processProvider.Start(path, "--terminateexisting --nobrowser"); + } + } + } } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 800258bde..83d567d8a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -292,7 +292,6 @@ - diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index b506e64fd..4d1af5f06 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Host public void Handle(ApplicationShutdownRequested message) { - if (!_runtimeInfo.IsWindowsService) + if (!_runtimeInfo.IsWindowsService && !message.Restarting) { Shutdown(); } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 87448075b..2890d4ec3 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Host var appMode = GetApplicationMode(startupContext); - Start(appMode); + Start(appMode, startupContext); if (startCallback != null) { @@ -54,11 +54,11 @@ namespace NzbDrone.Host } } - private static void Start(ApplicationModes applicationModes) + private static void Start(ApplicationModes applicationModes, StartupContext startupContext) { if (!IsInUtilityMode(applicationModes)) { - EnsureSingleInstance(applicationModes == ApplicationModes.Service); + EnsureSingleInstance(applicationModes == ApplicationModes.Service, startupContext); } DbFactory.RegisterDatabase(_container); @@ -80,7 +80,7 @@ namespace NzbDrone.Host } } - private static void EnsureSingleInstance(bool isService) + private static void EnsureSingleInstance(bool isService, StartupContext startupContext) { var instancePolicy = _container.Resolve(); @@ -88,6 +88,10 @@ namespace NzbDrone.Host { instancePolicy.KillAllOtherInstance(); } + else if (startupContext.Flags.Contains(StartupContext.TERMINATE)) + { + instancePolicy.KillAllOtherInstance(); + } else { instancePolicy.PreventStartIfAlreadyRunning(); From 6ff9c9f61edce20e740e300a5a83fbfdc9d53094 Mon Sep 17 00:00:00 2001 From: markus101 Date: Mon, 3 Feb 2014 23:11:36 -0800 Subject: [PATCH 30/48] Shutdown working on mono --- .../Processes/ProcessProvider.cs | 1 + src/NzbDrone.Console/ConsoleApp.cs | 3 +++ src/NzbDrone.Core/Jobs/Scheduler.cs | 21 ++++++++------- src/NzbDrone.Core/Queue/QueueScheduler.cs | 6 ++++- src/NzbDrone.Host/ApplicationServer.cs | 26 ++++++++++++++++--- .../NzbDronePersistentConnection.cs | 1 - 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 785b029e5..cdb47174b 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -269,6 +269,7 @@ namespace NzbDrone.Common.Processes if (process.HasExited) { + Logger.Trace("Process has already exited"); return; } diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 1cf0aae11..a88122209 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -25,6 +25,9 @@ namespace NzbDrone.Console Logger.FatalException("EPIC FAIL!", e); System.Console.WriteLine("Press any key to exit..."); System.Console.ReadLine(); + + //Need this to terminate on mono (thanks nlog) + LogManager.Configuration = null; } } } diff --git a/src/NzbDrone.Core/Jobs/Scheduler.cs b/src/NzbDrone.Core/Jobs/Scheduler.cs index e0d6e8194..6588d3de8 100644 --- a/src/NzbDrone.Core/Jobs/Scheduler.cs +++ b/src/NzbDrone.Core/Jobs/Scheduler.cs @@ -27,16 +27,6 @@ namespace NzbDrone.Core.Jobs _logger = logger; } - public void Handle(ApplicationStartedEvent message) - { - _cancellationTokenSource = new CancellationTokenSource(); - Timer.Interval = 1000 * 30; - Timer.Elapsed += (o, args) => Task.Factory.StartNew(ExecuteCommands, _cancellationTokenSource.Token) - .LogExceptions(); - - Timer.Start(); - } - private void ExecuteCommands() { try @@ -70,8 +60,19 @@ namespace NzbDrone.Core.Jobs } } + public void Handle(ApplicationStartedEvent message) + { + _cancellationTokenSource = new CancellationTokenSource(); + Timer.Interval = 1000 * 30; + Timer.Elapsed += (o, args) => Task.Factory.StartNew(ExecuteCommands, _cancellationTokenSource.Token) + .LogExceptions(); + + Timer.Start(); + } + public void Handle(ApplicationShutdownRequested message) { + _logger.Info("Shutting down scheduler"); _cancellationTokenSource.Cancel(true); Timer.Stop(); } diff --git a/src/NzbDrone.Core/Queue/QueueScheduler.cs b/src/NzbDrone.Core/Queue/QueueScheduler.cs index 39be36628..55a8d69e4 100644 --- a/src/NzbDrone.Core/Queue/QueueScheduler.cs +++ b/src/NzbDrone.Core/Queue/QueueScheduler.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using NLog; using NzbDrone.Common.TPL; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; @@ -11,12 +12,14 @@ namespace NzbDrone.Core.Queue IHandle { private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; private static readonly Timer Timer = new Timer(); private static CancellationTokenSource _cancellationTokenSource; - public QueueScheduler(IEventAggregator eventAggregator) + public QueueScheduler(IEventAggregator eventAggregator, Logger logger) { _eventAggregator = eventAggregator; + _logger = logger; } private void CheckQueue() @@ -47,6 +50,7 @@ namespace NzbDrone.Core.Queue public void Handle(ApplicationShutdownRequested message) { + _logger.Info("Shutting down queue scheduler"); _cancellationTokenSource.Cancel(true); Timer.Stop(); } diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index 4d1af5f06..946334ec6 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -1,6 +1,8 @@ -using System.ServiceProcess; +using System; +using System.ServiceProcess; using NLog; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Processes; using NzbDrone.Core.Configuration; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; @@ -22,10 +24,17 @@ namespace NzbDrone.Host private readonly PriorityMonitor _priorityMonitor; private readonly IStartupContext _startupContext; private readonly IBrowserService _browserService; + private readonly IProcessProvider _processProvider; private readonly Logger _logger; - public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController, - IRuntimeInfo runtimeInfo, PriorityMonitor priorityMonitor, IStartupContext startupContext, IBrowserService browserService, Logger logger) + public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, + IHostController hostController, + IRuntimeInfo runtimeInfo, + PriorityMonitor priorityMonitor, + IStartupContext startupContext, + IBrowserService browserService, + IProcessProvider processProvider, + Logger logger) { _configFileProvider = configFileProvider; _hostController = hostController; @@ -33,6 +42,7 @@ namespace NzbDrone.Host _priorityMonitor = priorityMonitor; _startupContext = startupContext; _browserService = browserService; + _processProvider = processProvider; _logger = logger; } @@ -43,6 +53,11 @@ namespace NzbDrone.Host public void Start() { + if (OsInfo.IsLinux) + { + Console.CancelKeyPress += (sender, eventArgs) => _processProvider.Kill(_processProvider.GetCurrentProcess().Id); + } + _runtimeInfo.IsRunning = true; _hostController.StartServer(); @@ -75,6 +90,11 @@ namespace NzbDrone.Host public void Handle(ApplicationShutdownRequested message) { + if (OsInfo.IsLinux) + { + _processProvider.Kill(_processProvider.GetCurrentProcess().Id); + } + if (!_runtimeInfo.IsWindowsService && !message.Restarting) { Shutdown(); diff --git a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs index c30ee555f..815238022 100644 --- a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs +++ b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs @@ -14,7 +14,6 @@ namespace NzbDrone.SignalR } } - public void Execute(BroadcastSignalRMessage message) { Context.Connection.Broadcast(message.Body); From be9b7284b59df885e024b94763b4b7e27b2437e1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 6 Feb 2014 21:18:48 -0800 Subject: [PATCH 31/48] Added shutdown and restart buttons to system UI New: Shutdown and restart from UI Fixed: ctrl+c will shutdown app on mono --- .../Lifecycle/Commands/RestartCommand.cs | 7 ++++++ .../Lifecycle/Commands/ShutdownCommand.cs | 7 ++++++ .../Lifecycle/LifestyleService.cs | 9 ++++++- src/UI/Content/icons.less | 9 +++++++ src/UI/Content/theme.less | 23 ++++++++++++++++++ src/UI/System/SystemLayout.js | 24 +++++++++++++++---- src/UI/System/SystemLayoutTemplate.html | 13 ++++++++++ 7 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs index 82c20cc07..6f0324c6a 100644 --- a/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs +++ b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs @@ -4,5 +4,12 @@ namespace NzbDrone.Core.Lifecycle.Commands { public class RestartCommand : Command { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } } } diff --git a/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs index b0fffd8e5..f1d20c46e 100644 --- a/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs +++ b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs @@ -4,5 +4,12 @@ namespace NzbDrone.Core.Lifecycle.Commands { public class ShutdownCommand : Command { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } } } diff --git a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs index bc09bf877..f051e723b 100644 --- a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifestyleService.cs @@ -1,7 +1,9 @@ using System.IO; +using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; +using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Lifecycle.Commands; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -16,23 +18,27 @@ namespace NzbDrone.Core.Lifecycle private readonly IAppFolderInfo _appFolderInfo; private readonly IServiceProvider _serviceProvider; private readonly IProcessProvider _processProvider; + private readonly Logger _logger; public LifestyleService(IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, IAppFolderInfo appFolderInfo, IServiceProvider serviceProvider, - IProcessProvider processProvider) + IProcessProvider processProvider, + Logger logger) { _eventAggregator = eventAggregator; _runtimeInfo = runtimeInfo; _appFolderInfo = appFolderInfo; _serviceProvider = serviceProvider; _processProvider = processProvider; + _logger = logger; } public void Execute(ShutdownCommand message) { + _logger.ProgressInfo("Shutdown requested, goodbye."); _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); if (_runtimeInfo.IsWindowsService) @@ -43,6 +49,7 @@ namespace NzbDrone.Core.Lifecycle public void Execute(RestartCommand message) { + _logger.ProgressInfo("Restart requested, brb."); _eventAggregator.PublishEvent(new ApplicationShutdownRequested(true)); if (_runtimeInfo.IsWindowsService) diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 3a648cf33..6b1d51aca 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -167,3 +167,12 @@ .icon(@cloud-download); color: @errorText; } + +.icon-nd-shutdown:before { + .icon(@off); + color: @errorText; +} + +.icon-nd-restart:before { + .icon(@repeat); +} \ No newline at end of file diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index e5916eb0c..68581bead 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -218,4 +218,27 @@ body { width: 100%; height: 55px; opacity: 0; +} + +.lifecycle-controls { + font-size: 20px; + + i { + cursor: pointer; + } + + .drone-button { + height: .8em; + margin-left: -8px; + + &:hover { + .icon-stack-base:before { + .icon(@sign-blank) + } + + .icon-nd-restart:before { + color: white; + } + } + } } \ No newline at end of file diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js index d4a34ae12..7a5d51368 100644 --- a/src/UI/System/SystemLayout.js +++ b/src/UI/System/SystemLayout.js @@ -5,12 +5,14 @@ define( 'marionette', 'System/Info/SystemInfoLayout', 'System/Logs/LogsLayout', - 'System/Update/UpdateLayout' + 'System/Update/UpdateLayout', + 'Commands/CommandController' ], function (Backbone, Marionette, SystemInfoLayout, LogsLayout, - UpdateLayout) { + UpdateLayout, + CommandController) { return Marionette.Layout.extend({ template: 'System/SystemLayoutTemplate', @@ -27,9 +29,11 @@ define( }, events: { - 'click .x-info-tab' : '_showInfo', + 'click .x-info-tab' : '_showInfo', 'click .x-logs-tab' : '_showLogs', - 'click .x-updates-tab': '_showUpdates' + 'click .x-updates-tab': '_showUpdates', + 'click .x-shutdown' : '_shutdown', + 'click .x-restart' : '_restart' }, initialize: function (options) { @@ -83,6 +87,18 @@ define( this.updates.show(new UpdateLayout()); this.ui.updatesTab.tab('show'); this._navigate('system/updates'); + }, + + _shutdown: function () { + CommandController.Execute('shutdown', { + name : 'shutdown' + }); + }, + + _restart: function () { + CommandController.Execute('restart', { + name : 'restart' + }); } }); }); diff --git a/src/UI/System/SystemLayoutTemplate.html b/src/UI/System/SystemLayoutTemplate.html index d95a3996b..2d3e45480 100644 --- a/src/UI/System/SystemLayoutTemplate.html +++ b/src/UI/System/SystemLayoutTemplate.html @@ -2,6 +2,19 @@
    • Info
    • Logs
    • Updates
    • +
    • + + + + + + {{#if_windows}} + + + + + {{/if_windows}} +
    • From 85b211738b05b303436c7b5522d2fd64a86b8819 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 6 Feb 2014 18:13:24 -0800 Subject: [PATCH 32/48] New: Backup database before updating --- src/NzbDrone.Common/PathExtensions.cs | 6 +++ src/NzbDrone.Update/NzbDrone.Update.csproj | 1 + src/NzbDrone.Update/UpdateApp.cs | 4 +- .../UpdateEngine/BackupAndRestore.cs | 4 +- .../UpdateEngine/BackupAppData.cs | 44 +++++++++++++++++++ .../UpdateEngine/InstallUpdateService.cs | 15 +++++-- 6 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Update/UpdateEngine/BackupAppData.cs diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index 5bf143796..763b1d1c0 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Common private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "nzbdrone_update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "nzbdrone" + Path.DirectorySeparatorChar; private static readonly string UPDATE_BACKUP_FOLDER_NAME = "nzbdrone_backup" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "nzbdrone_appdata_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; @@ -155,6 +156,11 @@ namespace NzbDrone.Common return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_BACKUP_FOLDER_NAME); } + public static string GetUpdateBackUpAppDataFolder(this IAppFolderInfo appFolderInfo) + { + return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_BACKUP_APPDATA_FOLDER_NAME); + } + public static string GetUpdatePackageFolder(this IAppFolderInfo appFolderInfo) { return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_PACKAGE_FOLDER_NAME); diff --git a/src/NzbDrone.Update/NzbDrone.Update.csproj b/src/NzbDrone.Update/NzbDrone.Update.csproj index d09ab8021..908102ff9 100644 --- a/src/NzbDrone.Update/NzbDrone.Update.csproj +++ b/src/NzbDrone.Update/NzbDrone.Update.csproj @@ -60,6 +60,7 @@ + diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 83531b153..35d7febca 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -50,10 +50,10 @@ namespace NzbDrone.Update public void Start(string[] args) { - int processId = ParseProcessId(args); + var processId = ParseProcessId(args); var exeFileInfo = new FileInfo(_processProvider.GetProcessById(processId).StartPath); - string targetFolder = exeFileInfo.Directory.FullName; + var targetFolder = exeFileInfo.Directory.FullName; logger.Info("Starting update process. Target Path:{0}", targetFolder); _installUpdateService.Start(targetFolder); diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs index 3787b25d3..a934e6fc6 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Update.UpdateEngine { public interface IBackupAndRestore { - void BackUp(string source); + void Backup(string source); void Restore(string target); } @@ -24,7 +24,7 @@ namespace NzbDrone.Update.UpdateEngine _logger = logger; } - public void BackUp(string source) + public void Backup(string source) { _logger.Info("Creating backup of existing installation"); _diskProvider.CopyFolder(source, _appFolderInfo.GetUpdateBackUpFolder()); diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs new file mode 100644 index 000000000..42d0dcad5 --- /dev/null +++ b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs @@ -0,0 +1,44 @@ +using System.IO; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Update.UpdateEngine +{ + public interface IBackupAppData + { + void Backup(); + } + + public class BackupAppData : IBackupAppData + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public BackupAppData(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, Logger logger) + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + _logger = logger; + } + + public void Backup() + { + _logger.Info("Backing up appdata (database/config)"); + var appDataPath = _appFolderInfo.GetAppDataPath(); + var backupFolderAppData = _appFolderInfo.GetUpdateBackUpAppDataFolder(); + var binFolder = Path.Combine(backupFolderAppData, "bin"); + + _diskProvider.CreateFolder(backupFolderAppData); + _diskProvider.CopyFolder(appDataPath, backupFolderAppData); + + if (_diskProvider.FolderExists(binFolder)) + { + _logger.Info("Deleting bin folder from appdata"); + _diskProvider.DeleteFolder(binFolder, true); + } + } + } +} diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index 0e6001677..f1b731e34 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -19,17 +19,25 @@ namespace NzbDrone.Update.UpdateEngine private readonly ITerminateNzbDrone _terminateNzbDrone; private readonly IAppFolderInfo _appFolderInfo; private readonly IBackupAndRestore _backupAndRestore; + private readonly IBackupAppData _backupAppData; private readonly IStartNzbDrone _startNzbDrone; private readonly Logger _logger; - public InstallUpdateService(IDiskProvider diskProvider, IDetectApplicationType detectApplicationType, ITerminateNzbDrone terminateNzbDrone, - IAppFolderInfo appFolderInfo, IBackupAndRestore backupAndRestore, IStartNzbDrone startNzbDrone, Logger logger) + public InstallUpdateService(IDiskProvider diskProvider, + IDetectApplicationType detectApplicationType, + ITerminateNzbDrone terminateNzbDrone, + IAppFolderInfo appFolderInfo, + IBackupAndRestore backupAndRestore, + IBackupAppData backupAppData, + IStartNzbDrone startNzbDrone, + Logger logger) { _diskProvider = diskProvider; _detectApplicationType = detectApplicationType; _terminateNzbDrone = terminateNzbDrone; _appFolderInfo = appFolderInfo; _backupAndRestore = backupAndRestore; + _backupAppData = backupAppData; _startNzbDrone = startNzbDrone; _logger = logger; } @@ -59,7 +67,8 @@ namespace NzbDrone.Update.UpdateEngine { _terminateNzbDrone.Terminate(); - _backupAndRestore.BackUp(installationFolder); + _backupAndRestore.Backup(installationFolder); + _backupAppData.Backup(); _logger.Info("Moving update package to target"); From e16a6f2b9c8a3bcf6919c3bc094346ef7228df7b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 7 Feb 2014 18:16:19 -0800 Subject: [PATCH 33/48] Only backup database and config file before update --- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 14 ++++++++++++++ src/NzbDrone.Common/Disk/IDiskProvider.cs | 1 + src/NzbDrone.Common/PathExtensions.cs | 10 ++++++++++ src/NzbDrone.Update/UpdateEngine/BackupAppData.cs | 11 ++--------- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index b9c506a2e..b1af0ccaa 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -204,6 +204,20 @@ namespace NzbDrone.Common.Disk File.Delete(path); } + public void CopyFile(string source, string destination, bool overwrite = false) + { + Ensure.That(source, () => source).IsValidPath(); + Ensure.That(destination, () => destination).IsValidPath(); + + if (source.PathEquals(destination)) + { + Logger.Warn("Source and destination can't be the same {0}", source); + return; + } + + File.Copy(source, destination, overwrite); + } + public void MoveFile(string source, string destination) { Ensure.That(source, () => source).IsValidPath(); diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 3e8b8ceff..0bb415452 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Common.Disk void CopyFolder(string source, string destination); void MoveFolder(string source, string destination); void DeleteFile(string path); + void CopyFile(string source, string destination, bool overwrite = false); void MoveFile(string source, string destination); void DeleteFolder(string path, bool recursive); string ReadAllText(string filePath); diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index 763b1d1c0..309fee9cb 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -161,6 +161,16 @@ namespace NzbDrone.Common return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_BACKUP_APPDATA_FOLDER_NAME); } + public static string GetUpdateBackupConfigFile(this IAppFolderInfo appFolderInfo) + { + return Path.Combine(GetUpdateBackUpAppDataFolder(appFolderInfo), APP_CONFIG_FILE); + } + + public static string GetUpdateBackupDatabase(this IAppFolderInfo appFolderInfo) + { + return Path.Combine(GetUpdateBackUpAppDataFolder(appFolderInfo), NZBDRONE_DB); + } + public static string GetUpdatePackageFolder(this IAppFolderInfo appFolderInfo) { return Path.Combine(GetUpdateSandboxFolder(appFolderInfo), UPDATE_PACKAGE_FOLDER_NAME); diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs index 42d0dcad5..038732367 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs @@ -27,18 +27,11 @@ namespace NzbDrone.Update.UpdateEngine public void Backup() { _logger.Info("Backing up appdata (database/config)"); - var appDataPath = _appFolderInfo.GetAppDataPath(); var backupFolderAppData = _appFolderInfo.GetUpdateBackUpAppDataFolder(); - var binFolder = Path.Combine(backupFolderAppData, "bin"); _diskProvider.CreateFolder(backupFolderAppData); - _diskProvider.CopyFolder(appDataPath, backupFolderAppData); - - if (_diskProvider.FolderExists(binFolder)) - { - _logger.Info("Deleting bin folder from appdata"); - _diskProvider.DeleteFolder(binFolder, true); - } + _diskProvider.CopyFile(_appFolderInfo.GetConfigPath(), _appFolderInfo.GetUpdateBackupConfigFile(), true); + _diskProvider.CopyFile(_appFolderInfo.GetNzbDroneDatabase(), _appFolderInfo.GetUpdateBackupDatabase(), true); } } } From e1318170224be2d98cbedd2ed9e7e73a3a39faa5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 7 Feb 2014 18:21:44 -0800 Subject: [PATCH 34/48] Disable nancyfx diagnostics unless in debug --- src/NzbDrone.Api/NancyBootstrapper.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/NzbDrone.Api/NancyBootstrapper.cs b/src/NzbDrone.Api/NancyBootstrapper.cs index d9496e038..0f5e89721 100644 --- a/src/NzbDrone.Api/NancyBootstrapper.cs +++ b/src/NzbDrone.Api/NancyBootstrapper.cs @@ -3,6 +3,7 @@ using Nancy.Bootstrapper; using Nancy.Diagnostics; using NzbDrone.Api.ErrorManagement; using NzbDrone.Api.Extensions.Pipelines; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Lifecycle; @@ -26,6 +27,11 @@ namespace NzbDrone.Api { _logger.Info("Starting NzbDrone API"); + if (RuntimeInfo.IsProduction) + { + DiagnosticsHook.Disable(pipelines); + } + RegisterPipelines(pipelines); container.Resolve().Register(); From c3eb9a7a8cc20ea6c0824d4429ec716e96ca750c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 7 Feb 2014 18:32:49 -0800 Subject: [PATCH 35/48] Renamed some series index views --- .../SeriesOverviewCollectionView.js} | 4 ++-- .../SeriesOverviewCollectionViewTemplate.html} | 0 .../{List/ItemView.js => Overview/SeriesOverviewItemView.js} | 2 +- .../SeriesOverviewItemViewTemplate.html} | 0 .../{CollectionView.js => SeriesPostersCollectionView.js} | 4 ++-- ...Template.html => SeriesPostersCollectionViewTemplate.html} | 0 .../Index/Posters/{ItemView.js => SeriesPostersItemView.js} | 2 +- .../{ItemTemplate.html => SeriesPostersItemViewTemplate.html} | 0 src/UI/Series/Index/SeriesIndexLayout.js | 4 ++-- 9 files changed, 8 insertions(+), 8 deletions(-) rename src/UI/Series/Index/{List/CollectionView.js => Overview/SeriesOverviewCollectionView.js} (64%) rename src/UI/Series/Index/{List/CollectionTemplate.html => Overview/SeriesOverviewCollectionViewTemplate.html} (100%) rename src/UI/Series/Index/{List/ItemView.js => Overview/SeriesOverviewItemView.js} (89%) rename src/UI/Series/Index/{List/ItemTemplate.html => Overview/SeriesOverviewItemViewTemplate.html} (100%) rename src/UI/Series/Index/Posters/{CollectionView.js => SeriesPostersCollectionView.js} (65%) rename src/UI/Series/Index/Posters/{CollectionTemplate.html => SeriesPostersCollectionViewTemplate.html} (100%) rename src/UI/Series/Index/Posters/{ItemView.js => SeriesPostersItemView.js} (93%) rename src/UI/Series/Index/Posters/{ItemTemplate.html => SeriesPostersItemViewTemplate.html} (100%) diff --git a/src/UI/Series/Index/List/CollectionView.js b/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js similarity index 64% rename from src/UI/Series/Index/List/CollectionView.js rename to src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js index 8f6b5ae7f..986edd574 100644 --- a/src/UI/Series/Index/List/CollectionView.js +++ b/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js @@ -3,12 +3,12 @@ define( [ 'marionette', - 'Series/Index/List/ItemView' + 'Series/Index/Overview/SeriesOverviewItemView' ], function (Marionette, ListItemView) { return Marionette.CompositeView.extend({ itemView : ListItemView, itemViewContainer: '#x-series-list', - template : 'Series/Index/List/CollectionTemplate' + template : 'Series/Index/Overview/SeriesOverviewCollectionViewTemplate' }); }); diff --git a/src/UI/Series/Index/List/CollectionTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html similarity index 100% rename from src/UI/Series/Index/List/CollectionTemplate.html rename to src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.html diff --git a/src/UI/Series/Index/List/ItemView.js b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js similarity index 89% rename from src/UI/Series/Index/List/ItemView.js rename to src/UI/Series/Index/Overview/SeriesOverviewItemView.js index e6da6377b..1c4970a65 100644 --- a/src/UI/Series/Index/List/ItemView.js +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js @@ -6,7 +6,7 @@ define( 'marionette' ], function (vent, Marionette) { return Marionette.ItemView.extend({ - template: 'Series/Index/List/ItemTemplate', + template: 'Series/Index/Overview/SeriesOverviewItemViewTemplate', ui: { 'progressbar': '.progress .bar' diff --git a/src/UI/Series/Index/List/ItemTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html similarity index 100% rename from src/UI/Series/Index/List/ItemTemplate.html rename to src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html diff --git a/src/UI/Series/Index/Posters/CollectionView.js b/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js similarity index 65% rename from src/UI/Series/Index/Posters/CollectionView.js rename to src/UI/Series/Index/Posters/SeriesPostersCollectionView.js index 06600e8fb..ead7a910a 100644 --- a/src/UI/Series/Index/Posters/CollectionView.js +++ b/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js @@ -3,12 +3,12 @@ define( [ 'marionette', - 'Series/Index/Posters/ItemView' + 'Series/Index/Posters/SeriesPostersItemView' ], function (Marionette, PosterItemView) { return Marionette.CompositeView.extend({ itemView : PosterItemView, itemViewContainer: '#x-series-posters', - template : 'Series/Index/Posters/CollectionTemplate' + template : 'Series/Index/Posters/SeriesPostersCollectionViewTemplate' }); }); diff --git a/src/UI/Series/Index/Posters/CollectionTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html similarity index 100% rename from src/UI/Series/Index/Posters/CollectionTemplate.html rename to src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.html diff --git a/src/UI/Series/Index/Posters/ItemView.js b/src/UI/Series/Index/Posters/SeriesPostersItemView.js similarity index 93% rename from src/UI/Series/Index/Posters/ItemView.js rename to src/UI/Series/Index/Posters/SeriesPostersItemView.js index 02d4ba713..997c1c9e1 100644 --- a/src/UI/Series/Index/Posters/ItemView.js +++ b/src/UI/Series/Index/Posters/SeriesPostersItemView.js @@ -8,7 +8,7 @@ define( return Marionette.ItemView.extend({ tagName : 'li', - template: 'Series/Index/Posters/ItemTemplate', + template: 'Series/Index/Posters/SeriesPostersItemViewTemplate', ui: { diff --git a/src/UI/Series/Index/Posters/ItemTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html similarity index 100% rename from src/UI/Series/Index/Posters/ItemTemplate.html rename to src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 3313bca83..ac3262063 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -3,8 +3,8 @@ define( [ 'marionette', 'backgrid', - 'Series/Index/Posters/CollectionView', - 'Series/Index/List/CollectionView', + 'Series/Index/Posters/SeriesPostersCollectionView', + 'Series/Index/Overview/SeriesOverviewCollectionView', 'Series/Index/EmptyView', 'Series/SeriesCollection', 'Cells/RelativeDateCell', From fcd05cda6091f26004243df2e51d85377ef4b330 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 7 Feb 2014 21:05:52 -0800 Subject: [PATCH 36/48] Fixed: Show title on posters view when poster is unavailable --- src/UI/.idea/jsLinters/jshint.xml | 8 ++--- src/UI/Handlebars/Helpers/Html.js | 10 ++++--- .../SeriesPostersItemViewTemplate.html | 7 ++--- src/UI/Series/series.less | 29 +++++++++++-------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/UI/.idea/jsLinters/jshint.xml b/src/UI/.idea/jsLinters/jshint.xml index 96fa6c67a..e85398a55 100644 --- a/src/UI/.idea/jsLinters/jshint.xml +++ b/src/UI/.idea/jsLinters/jshint.xml @@ -8,24 +8,22 @@
      -
      -
      {{title}}
      -
      -
      {{#if_eq status compare="continuing"}} diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index 474c61d99..2e23c92a5 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -95,16 +95,6 @@ } } - .title-container { - position : relative; - - .title { - position : absolute; - top : -100px; - opacity : 0.0; - } - } - .labels { display : inline-block; .opacity(0.75); @@ -130,6 +120,21 @@ overflow : hidden; display : inline-block; + .placeholder-image ~ .title { + opacity: 1.0; + } + + .title { + position : absolute; + top : 25px; + color : #f5f5f5; + width : 100%; + font-size : 22px; + line-height: 24px; + opacity : 0.0; + font-weight: 100; + } + .ended-banner { color : #eeeeee; background-color : #b94a48; @@ -137,8 +142,8 @@ -moz-transform-origin : 50% 50%; -webkit-transform-origin : 50% 50%; position : absolute; - width : 300px; - top : 175px; + width : 320px; + top : 200px; left : -122px; text-align : center; .opacity(0.9); From 469d802b7b0406b7316c12ad04254b51978bba6d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 7 Feb 2014 22:53:35 -0800 Subject: [PATCH 37/48] Shutdown and restart are buttons now --- src/UI/Content/theme.less | 23 ----------------------- src/UI/System/SystemLayoutTemplate.html | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 68581bead..2ce038698 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -219,26 +219,3 @@ body { height: 55px; opacity: 0; } - -.lifecycle-controls { - font-size: 20px; - - i { - cursor: pointer; - } - - .drone-button { - height: .8em; - margin-left: -8px; - - &:hover { - .icon-stack-base:before { - .icon(@sign-blank) - } - - .icon-nd-restart:before { - color: white; - } - } - } -} \ No newline at end of file diff --git a/src/UI/System/SystemLayoutTemplate.html b/src/UI/System/SystemLayoutTemplate.html index 2d3e45480..3e5a10b8e 100644 --- a/src/UI/System/SystemLayoutTemplate.html +++ b/src/UI/System/SystemLayoutTemplate.html @@ -3,17 +3,16 @@
    • Logs
    • Updates
    • - - - - - - {{#if_windows}} - - - - - {{/if_windows}} +
      + + {{#if_windows}} + + {{/if_windows}} +
    • From e47b4c7686e258ffd890c63474e1d98272349994 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 5 Feb 2014 16:30:14 -0800 Subject: [PATCH 38/48] New: Series lists will auto update when files are imported/deleted --- src/NzbDrone.Api/Series/SeriesModule.cs | 51 +++++++++++++++++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Tv/Events/SeriesEditedEvent.cs | 14 +++++ src/NzbDrone.Core/Tv/SeriesService.cs | 7 ++- src/UI/Cells/SeriesStatusCell.js | 16 +++--- src/UI/Mixins/AsFilteredCollection.js | 8 ++- src/UI/Mixins/backbone.signalr.mixin.js | 6 +++ src/UI/Series/Index/SeriesIndexLayout.js | 49 ++++++++++++++---- src/UI/Series/SeriesCollection.js | 5 +- .../Shared/Toolbar/Radio/RadioButtonView.js | 1 - 10 files changed, 131 insertions(+), 27 deletions(-) create mode 100644 src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index ea01b1a06..d87a0f4ec 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -2,23 +2,39 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.SeriesStats; using NzbDrone.Core.Tv; using NzbDrone.Api.Validation; using NzbDrone.Api.Mapping; +using NzbDrone.Core.Tv.Events; namespace NzbDrone.Api.Series { - public class SeriesModule : NzbDroneRestModule + public class SeriesModule : NzbDroneRestModuleWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + { + private readonly ICommandExecutor _commandExecutor; private readonly ISeriesService _seriesService; private readonly ISeriesStatisticsService _seriesStatisticsService; private readonly IMapCoversToLocal _coverMapper; - public SeriesModule(ISeriesService seriesService, ISeriesStatisticsService seriesStatisticsService, IMapCoversToLocal coverMapper) - : base("/Series") + public SeriesModule(ICommandExecutor commandExecutor, + ISeriesService seriesService, + ISeriesStatisticsService seriesStatisticsService, + IMapCoversToLocal coverMapper) + : base(commandExecutor) { + _commandExecutor = commandExecutor; _seriesService = seriesService; _seriesStatisticsService = seriesStatisticsService; _coverMapper = coverMapper; @@ -74,6 +90,8 @@ namespace NzbDrone.Api.Series private void UpdateSeries(SeriesResource seriesResource) { GetNewId(_seriesService.UpdateSeries, seriesResource); + + BroadcastResourceChange(ModelAction.Updated, seriesResource); } private void DeleteSeries(int id) @@ -119,5 +137,32 @@ namespace NzbDrone.Api.Series resource.EpisodeFileCount = seriesStatistics.EpisodeFileCount; resource.NextAiring = seriesStatistics.NextAiring; } + + public void Handle(EpisodeImportedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.SeriesId); + } + + public void Handle(EpisodeFileDeletedEvent message) + { + if (message.ForUpgrade) return; + + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.SeriesId); + } + + public void Handle(SeriesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + + public void Handle(SeriesEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + } + + public void Handle(SeriesDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Series.InjectTo()); + } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 83d567d8a..1944dc15a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -489,6 +489,7 @@ + diff --git a/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs new file mode 100644 index 000000000..33371a4e7 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/SeriesEditedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class SeriesEditedEvent : IEvent + { + public Series Series { get; private set; } + + public SeriesEditedEvent(Series series) + { + Series = series; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index b4fe33ea1..ee7af6841 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -136,7 +136,10 @@ namespace NzbDrone.Core.Tv } } - return _seriesRepository.Update(series); + var updatedSeries = _seriesRepository.Update(series); + _eventAggregator.PublishEvent(new SeriesEditedEvent(updatedSeries)); + + return updatedSeries; } public List UpdateSeries(List series) @@ -148,6 +151,8 @@ namespace NzbDrone.Core.Tv var folderName = new DirectoryInfo(s.Path).Name; s.Path = Path.Combine(s.RootFolderPath, folderName); } + + _eventAggregator.PublishEvent(new SeriesEditedEvent(s)); } _seriesRepository.UpdateMany(series); diff --git a/src/UI/Cells/SeriesStatusCell.js b/src/UI/Cells/SeriesStatusCell.js index b180a172c..027e5aea3 100644 --- a/src/UI/Cells/SeriesStatusCell.js +++ b/src/UI/Cells/SeriesStatusCell.js @@ -1,9 +1,9 @@ 'use strict'; define( [ - 'backgrid' - ], function (Backgrid) { - return Backgrid.Cell.extend({ + 'Cells/NzbDroneCell' + ], function (NzbDroneCell) { + return NzbDroneCell.extend({ className: 'series-status-cell', render: function () { @@ -13,20 +13,24 @@ define( if (status === 'ended') { this.$el.html(''); - this.model.set('statusWeight', 3); + this._setStatusWeight(3); } else if (!monitored) { this.$el.html(''); - this.model.set('statusWeight', 2); + this._setStatusWeight(2); } else { this.$el.html(''); - this.model.set('statusWeight', 1); + this._setStatusWeight(1); } return this; + }, + + _setStatusWeight: function (weight) { + this.model.set('statusWeight', weight, {silent: true}); } }); }); diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js index 447f60edb..2a0e17991 100644 --- a/src/UI/Mixins/AsFilteredCollection.js +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -32,7 +32,7 @@ define( this.prototype._makeFullCollection = function (models, options) { var self = this; - self.shadowCollection = originalMakeFullCollection.apply(this, [models, options]); + self.shadowCollection = originalMakeFullCollection.call(this, models, options); var filterModel = function(model) { if (!self.state.filterKey || !self.state.filterValue) @@ -46,12 +46,10 @@ define( }; var filteredModels = self.shadowCollection.filtered(); - - var fullCollection = originalMakeFullCollection.apply(this, [filteredModels, options]); - + var fullCollection = originalMakeFullCollection.call(this, filteredModels, options); fullCollection.resetFiltered = function(options) { - Backbone.Collection.prototype.reset.apply(this, [self.shadowCollection.filtered(), options]); + Backbone.Collection.prototype.reset.call(this, self.shadowCollection.filtered(), options); }; fullCollection.reset = function (models, options) { diff --git a/src/UI/Mixins/backbone.signalr.mixin.js b/src/UI/Mixins/backbone.signalr.mixin.js index 04b1dfd7f..a29ed4882 100644 --- a/src/UI/Mixins/backbone.signalr.mixin.js +++ b/src/UI/Mixins/backbone.signalr.mixin.js @@ -22,6 +22,12 @@ define( return; } + if (options.action === 'deleted') { + collection.remove(new collection.model(options.resource, {parse: true})); + + return; + } + var model = new collection.model(options.resource, {parse: true}); //updateOnly will prevent the collection from adding a new item diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index ac3262063..5d732d484 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -1,6 +1,7 @@ 'use strict'; define( [ + 'underscore', 'marionette', 'backgrid', 'Series/Index/Posters/SeriesPostersCollectionView', @@ -16,9 +17,9 @@ define( 'Cells/SeriesStatusCell', 'Series/Index/FooterView', 'Series/Index/FooterModel', - 'Shared/Toolbar/ToolbarLayout', - 'underscore' - ], function (Marionette, + 'Shared/Toolbar/ToolbarLayout' + ], function (_, + Marionette, Backgrid, PosterCollectionView, ListCollectionView, @@ -33,8 +34,7 @@ define( SeriesStatusCell, FooterView, FooterModel, - ToolbarLayout, - _) { + ToolbarLayout) { return Marionette.Layout.extend({ template: 'Series/Index/SeriesIndexLayoutTemplate', @@ -131,8 +131,25 @@ define( initialize: function () { this.seriesCollection = SeriesCollection.clone(); - this.listenTo(SeriesCollection, 'sync', this._renderView); - this.listenTo(SeriesCollection, 'remove', this._renderView); + this.listenTo(SeriesCollection, 'sync', function (model, collection, options) { + this.seriesCollection.shadowCollection.add(model, options); + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(SeriesCollection, 'add', function (model, collection, options) { + this.seriesCollection.shadowCollection.add(model, options); + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(SeriesCollection, 'remove', function (model, collection, options) { + this.seriesCollection.shadowCollection.remove(model, options); + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.sortingOptions = { type : 'sorting', @@ -255,7 +272,7 @@ define( }, _showList: function () { - this.currentView = new ListCollectionView({ + this.currentView = new ListCollectionView({ collection: this.seriesCollection }); @@ -269,7 +286,7 @@ define( this._renderView(); }, - + _renderView: function () { if (SeriesCollection.length === 0) { @@ -278,6 +295,8 @@ define( this.toolbar2.close(); } else { + this._resetFilter(); + this.seriesRegion.show(this.currentView); this._showToolbar(); @@ -295,6 +314,18 @@ define( this.seriesCollection.setFilterMode(mode); }, + _resetFilter: function () { + var key = this.seriesCollection.state.filterKey; + var value = this.seriesCollection.state.filterValue; + + this.seriesCollection.setFilter([ null, null ]); + this.seriesCollection.setFilter([ key, value ]); + }, + + _shadowTest: function () { + window.alert('added to shadow'); + }, + _showToolbar: function () { if (this.toolbar.currentView) { diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index 4f00e6c42..bbd70e940 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -8,7 +8,8 @@ define( 'api!series', 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection', - 'moment' + 'moment', + 'Mixins/backbone.signalr.mixin' ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsPersistedStateCollection, Moment) { var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', @@ -72,5 +73,5 @@ define( var MixedIn = AsPersistedStateCollection.call(FilteredCollection); var collection = new MixedIn(SeriesData, { full: true }); - return collection; + return collection.bindSignalR(); }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index baa67a5da..af2f0c903 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -50,7 +50,6 @@ define( throw 'ownerContext must be set.'; } - var callback = this.model.get('callback'); if (callback) { callback.call(this.model.ownerContext, this); From 0247aebb25e401011c90434f0714b09d21c46e14 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 8 Feb 2014 11:44:38 -0800 Subject: [PATCH 39/48] Fixed: Adding an invalid root folder will show a usable error message --- src/NzbDrone.Api/RootFolders/RootFolderModule.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index 6691032d8..5d0298698 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.RootFolders; using NzbDrone.Api.Mapping; @@ -30,7 +33,15 @@ namespace NzbDrone.Api.RootFolders private int CreateRootFolder(RootFolderResource rootFolderResource) { - return GetNewId(_rootFolderService.Add, rootFolderResource); + try + { + return GetNewId(_rootFolderService.Add, rootFolderResource); + } + catch (Exception ex) + { + throw new ValidationException(new [] { new ValidationFailure("Path", ex.Message) }); + } + } private List GetRootFolders() From dff67261142299ab4e6d232fe653579c79f1786e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 8 Feb 2014 11:54:14 -0800 Subject: [PATCH 40/48] Enter on add root folder will add folder Renamed Root Folder js files --- src/UI/AddSeries/AddSeriesLayout.js | 4 ++-- ...{Collection.js => RootFolderCollection.js} | 2 +- ...ionView.js => RootFolderCollectionView.js} | 2 +- .../{ItemView.js => RootFolderItemView.js} | 2 +- ...e.html => RootFolderItemViewTemplate.html} | 0 .../{Layout.js => RootFolderLayout.js} | 19 ++++++++++++++----- ...ate.html => RootFolderLayoutTemplate.html} | 0 .../{Model.js => RootFolderModel.js} | 0 src/UI/AddSeries/SearchResultView.js | 4 ++-- .../Series/Editor/SeriesEditorFooterView.js | 4 ++-- 10 files changed, 23 insertions(+), 14 deletions(-) rename src/UI/AddSeries/RootFolders/{Collection.js => RootFolderCollection.js} (89%) rename src/UI/AddSeries/RootFolders/{CollectionView.js => RootFolderCollectionView.js} (85%) rename src/UI/AddSeries/RootFolders/{ItemView.js => RootFolderItemView.js} (91%) rename src/UI/AddSeries/RootFolders/{ItemViewTemplate.html => RootFolderItemViewTemplate.html} (100%) rename src/UI/AddSeries/RootFolders/{Layout.js => RootFolderLayout.js} (79%) rename src/UI/AddSeries/RootFolders/{LayoutTemplate.html => RootFolderLayoutTemplate.html} (100%) rename src/UI/AddSeries/RootFolders/{Model.js => RootFolderModel.js} (100%) diff --git a/src/UI/AddSeries/AddSeriesLayout.js b/src/UI/AddSeries/AddSeriesLayout.js index 1b778546d..e6e265abe 100644 --- a/src/UI/AddSeries/AddSeriesLayout.js +++ b/src/UI/AddSeries/AddSeriesLayout.js @@ -4,11 +4,11 @@ define( 'vent', 'AppLayout', 'marionette', - 'AddSeries/RootFolders/Layout', + 'AddSeries/RootFolders/RootFolderLayout', 'AddSeries/Existing/AddExistingSeriesCollectionView', 'AddSeries/AddSeriesView', 'Quality/QualityProfileCollection', - 'AddSeries/RootFolders/Collection', + 'AddSeries/RootFolders/RootFolderCollection', 'Series/SeriesCollection' ], function (vent, AppLayout, diff --git a/src/UI/AddSeries/RootFolders/Collection.js b/src/UI/AddSeries/RootFolders/RootFolderCollection.js similarity index 89% rename from src/UI/AddSeries/RootFolders/Collection.js rename to src/UI/AddSeries/RootFolders/RootFolderCollection.js index 157bf19ce..364cecea9 100644 --- a/src/UI/AddSeries/RootFolders/Collection.js +++ b/src/UI/AddSeries/RootFolders/RootFolderCollection.js @@ -2,7 +2,7 @@ define( [ 'backbone', - 'AddSeries/RootFolders/Model', + 'AddSeries/RootFolders/RootFolderModel', 'Mixins/backbone.signalr.mixin' ], function (Backbone, RootFolderModel) { diff --git a/src/UI/AddSeries/RootFolders/CollectionView.js b/src/UI/AddSeries/RootFolders/RootFolderCollectionView.js similarity index 85% rename from src/UI/AddSeries/RootFolders/CollectionView.js rename to src/UI/AddSeries/RootFolders/RootFolderCollectionView.js index 689b16132..35ab928d6 100644 --- a/src/UI/AddSeries/RootFolders/CollectionView.js +++ b/src/UI/AddSeries/RootFolders/RootFolderCollectionView.js @@ -3,7 +3,7 @@ define( [ 'marionette', - 'AddSeries/RootFolders/ItemView' + 'AddSeries/RootFolders/RootFolderItemView' ], function (Marionette, RootFolderItemView) { diff --git a/src/UI/AddSeries/RootFolders/ItemView.js b/src/UI/AddSeries/RootFolders/RootFolderItemView.js similarity index 91% rename from src/UI/AddSeries/RootFolders/ItemView.js rename to src/UI/AddSeries/RootFolders/RootFolderItemView.js index 8135d69ab..1fe1e72e7 100644 --- a/src/UI/AddSeries/RootFolders/ItemView.js +++ b/src/UI/AddSeries/RootFolders/RootFolderItemView.js @@ -7,7 +7,7 @@ define( return Marionette.ItemView.extend({ - template: 'AddSeries/RootFolders/ItemViewTemplate', + template: 'AddSeries/RootFolders/RootFolderItemViewTemplate', tagName : 'tr', initialize: function () { diff --git a/src/UI/AddSeries/RootFolders/ItemViewTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html similarity index 100% rename from src/UI/AddSeries/RootFolders/ItemViewTemplate.html rename to src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.html diff --git a/src/UI/AddSeries/RootFolders/Layout.js b/src/UI/AddSeries/RootFolders/RootFolderLayout.js similarity index 79% rename from src/UI/AddSeries/RootFolders/Layout.js rename to src/UI/AddSeries/RootFolders/RootFolderLayout.js index 3b54c90a1..7a0886c6f 100644 --- a/src/UI/AddSeries/RootFolders/Layout.js +++ b/src/UI/AddSeries/RootFolders/RootFolderLayout.js @@ -3,16 +3,16 @@ define( [ 'marionette', - 'AddSeries/RootFolders/CollectionView', - 'AddSeries/RootFolders/Collection', - 'AddSeries/RootFolders/Model', + 'AddSeries/RootFolders/RootFolderCollectionView', + 'AddSeries/RootFolders/RootFolderCollection', + 'AddSeries/RootFolders/RootFolderModel', 'Shared/LoadingView', 'Mixins/AsValidatedView', 'Mixins/AutoComplete' ], function (Marionette, RootFolderCollectionView, RootFolderCollection, RootFolderModel, LoadingView, AsValidatedView) { var layout = Marionette.Layout.extend({ - template: 'AddSeries/RootFolders/LayoutTemplate', + template: 'AddSeries/RootFolders/RootFolderLayoutTemplate', ui: { pathInput: '.x-path input' @@ -23,7 +23,8 @@ define( }, events: { - 'click .x-add': '_addFolder' + 'click .x-add': '_addFolder', + 'keydown .x-path input': '_keydown' }, initialize: function () { @@ -65,6 +66,14 @@ define( _showCurrentDirs: function () { this.currentDirs.show(this.rootfolderListView); + }, + + _keydown: function (e) { + if (e.keyCode !== 13) { + return; + } + + this._addFolder(); } }); diff --git a/src/UI/AddSeries/RootFolders/LayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html similarity index 100% rename from src/UI/AddSeries/RootFolders/LayoutTemplate.html rename to src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html diff --git a/src/UI/AddSeries/RootFolders/Model.js b/src/UI/AddSeries/RootFolders/RootFolderModel.js similarity index 100% rename from src/UI/AddSeries/RootFolders/Model.js rename to src/UI/AddSeries/RootFolders/RootFolderModel.js diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index eced403ec..c176115ed 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -6,8 +6,8 @@ define( 'underscore', 'marionette', 'Quality/QualityProfileCollection', - 'AddSeries/RootFolders/Collection', - 'AddSeries/RootFolders/Layout', + 'AddSeries/RootFolders/RootFolderCollection', + 'AddSeries/RootFolders/RootFolderLayout', 'Series/SeriesCollection', 'Config', 'Shared/Messenger', diff --git a/src/UI/Series/Editor/SeriesEditorFooterView.js b/src/UI/Series/Editor/SeriesEditorFooterView.js index 28a27bca4..3d8993ece 100644 --- a/src/UI/Series/Editor/SeriesEditorFooterView.js +++ b/src/UI/Series/Editor/SeriesEditorFooterView.js @@ -7,9 +7,9 @@ define( 'vent', 'Series/SeriesCollection', 'Quality/QualityProfileCollection', - 'AddSeries/RootFolders/Collection', + 'AddSeries/RootFolders/RootFolderCollection', 'Shared/Toolbar/ToolbarLayout', - 'AddSeries/RootFolders/Layout', + 'AddSeries/RootFolders/RootFolderLayout', 'Config' ], function (_, Marionette, From bcc854b78cd0ce20d16b45a3cdf389e5305a36ad Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 9 Feb 2014 12:31:57 -0800 Subject: [PATCH 41/48] Removed extraneous filtering code from series index --- src/UI/Series/Index/SeriesIndexLayout.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 5d732d484..bf3a52416 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -257,7 +257,6 @@ define( onShow: function () { this._showToolbar(); - this._renderView(); this._fetchCollection(); }, @@ -295,8 +294,6 @@ define( this.toolbar2.close(); } else { - this._resetFilter(); - this.seriesRegion.show(this.currentView); this._showToolbar(); @@ -314,18 +311,6 @@ define( this.seriesCollection.setFilterMode(mode); }, - _resetFilter: function () { - var key = this.seriesCollection.state.filterKey; - var value = this.seriesCollection.state.filterValue; - - this.seriesCollection.setFilter([ null, null ]); - this.seriesCollection.setFilter([ key, value ]); - }, - - _shadowTest: function () { - window.alert('added to shadow'); - }, - _showToolbar: function () { if (this.toolbar.currentView) { From 267c97052e545b66204db24d0c200cfa6ca490cf Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 9 Feb 2014 23:10:06 -0800 Subject: [PATCH 42/48] Close both toolbars --- src/UI/Series/Index/SeriesIndexLayout.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index bf3a52416..0896d35c3 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -290,6 +290,7 @@ define( if (SeriesCollection.length === 0) { this.seriesRegion.show(new EmptyView()); + this.toolbar.close(); this.toolbar2.close(); } @@ -317,6 +318,14 @@ define( return; } + this.toolbar2.show(new ToolbarLayout({ + right : + [ + this.filteringOptions + ], + context: this + })); + this.toolbar.show(new ToolbarLayout({ right : [ @@ -329,14 +338,6 @@ define( ], context: this })); - - this.toolbar2.show(new ToolbarLayout({ - right : - [ - this.filteringOptions - ], - context: this - })); }, _showFooter: function () { From 2d50957baad9a91ed6e377bd68aa9a3eadd4af7d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 01:25:06 -0800 Subject: [PATCH 43/48] Fixed: Fatal error will not be logged when browser is closed (linux/osx) --- .../Infrastructure/Connection.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs index f29063f7f..d2109e3bf 100644 --- a/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs +++ b/src/Microsoft.AspNet.SignalR.Core/Infrastructure/Connection.cs @@ -238,6 +238,12 @@ namespace Microsoft.AspNet.SignalR.Infrastructure if (command == null) { + var platform = (int)Environment.OSVersion.Platform; + if (platform == 4 || platform == 6 || platform == 128) + { + return; + } + throw new SerializationException("Couldn't parse message " + message.Value); } From d0a6daeb268c39fa1c825d561ed91e5fca3f5b77 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 01:49:06 -0800 Subject: [PATCH 44/48] Restart on linux/os x working Removed client messages for shutdown/restart since they were getting stuck --- .../EnvironmentInfo/AppFolderInfo.cs | 5 --- .../EnvironmentInfo/RuntimeInfo.cs | 5 +++ src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../Processes/INzbDroneProcessProvider.cs | 10 +++++ .../Lifecycle/Commands/RestartCommand.cs | 7 ---- .../Lifecycle/Commands/ShutdownCommand.cs | 7 ---- ...ifestyleService.cs => LifecycleService.cs} | 40 ++++++------------- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 +- src/NzbDrone.Host/Bootstrap.cs | 1 + src/NzbDrone.Host/MainAppContainerBuilder.cs | 1 - src/NzbDrone.Host/NzbDrone.Host.csproj | 6 +-- ...cessService.cs => SingleInstancePolicy.cs} | 16 +++++--- src/NzbDrone.Mono/NzbDrone.Mono.csproj | 1 + src/NzbDrone.Mono/NzbDroneProcessProvider.cs | 31 ++++++++++++++ src/NzbDrone.Windows/NzbDrone.Windows.csproj | 1 + .../NzbDroneProcessProvider.cs | 25 ++++++++++++ src/UI/System/SystemLayoutTemplate.html | 2 - 17 files changed, 100 insertions(+), 61 deletions(-) create mode 100644 src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs rename src/NzbDrone.Core/Lifecycle/{LifestyleService.cs => LifecycleService.cs} (58%) rename src/NzbDrone.Host/{NzbDroneProcessService.cs => SingleInstancePolicy.cs} (72%) create mode 100644 src/NzbDrone.Mono/NzbDroneProcessProvider.cs create mode 100644 src/NzbDrone.Windows/NzbDroneProcessProvider.cs diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs index 3bf7876f3..3a5bcd019 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs @@ -1,11 +1,6 @@ using System; using System.IO; using System.Reflection; -using System.Security.AccessControl; -using System.Security.Principal; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Instrumentation; namespace NzbDrone.Common.EnvironmentInfo { diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index aec416e61..314b7cb6a 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Security.Principal; using System.ServiceProcess; using NLog; @@ -15,6 +16,7 @@ namespace NzbDrone.Common.EnvironmentInfo bool IsWindowsService { get; } bool IsConsole { get; } bool IsRunning { get; set; } + string ExecutingApplication { get; } } public class RuntimeInfo : IRuntimeInfo @@ -30,6 +32,8 @@ namespace NzbDrone.Common.EnvironmentInfo OsInfo.IsWindows && serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; + + ExecutingApplication = Assembly.GetEntryAssembly().Location; } static RuntimeInfo() @@ -73,6 +77,7 @@ namespace NzbDrone.Common.EnvironmentInfo } public bool IsRunning { get; set; } + public string ExecutingApplication { get; private set; } public static bool IsProduction { get; private set; } diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 6ce3ba94c..9378ab2f6 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -101,6 +101,7 @@ + diff --git a/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs b/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs new file mode 100644 index 000000000..29f16b7fa --- /dev/null +++ b/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using NzbDrone.Common.Model; + +namespace NzbDrone.Common.Processes +{ + public interface INzbDroneProcessProvider + { + List GetNzbDroneProcesses(); + } +} diff --git a/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs index 6f0324c6a..82c20cc07 100644 --- a/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs +++ b/src/NzbDrone.Core/Lifecycle/Commands/RestartCommand.cs @@ -4,12 +4,5 @@ namespace NzbDrone.Core.Lifecycle.Commands { public class RestartCommand : Command { - public override bool SendUpdatesToClient - { - get - { - return true; - } - } } } diff --git a/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs index f1d20c46e..b0fffd8e5 100644 --- a/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs +++ b/src/NzbDrone.Core/Lifecycle/Commands/ShutdownCommand.cs @@ -4,12 +4,5 @@ namespace NzbDrone.Core.Lifecycle.Commands { public class ShutdownCommand : Command { - public override bool SendUpdatesToClient - { - get - { - return true; - } - } } } diff --git a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs similarity index 58% rename from src/NzbDrone.Core/Lifecycle/LifestyleService.cs rename to src/NzbDrone.Core/Lifecycle/LifecycleService.cs index f051e723b..25199b49f 100644 --- a/src/NzbDrone.Core/Lifecycle/LifestyleService.cs +++ b/src/NzbDrone.Core/Lifecycle/LifecycleService.cs @@ -1,4 +1,4 @@ -using System.IO; +using System; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; @@ -11,26 +11,23 @@ using IServiceProvider = NzbDrone.Common.IServiceProvider; namespace NzbDrone.Core.Lifecycle { - public class LifestyleService: IExecute, IExecute + public class LifecycleService: IExecute, IExecute { private readonly IEventAggregator _eventAggregator; private readonly IRuntimeInfo _runtimeInfo; - private readonly IAppFolderInfo _appFolderInfo; private readonly IServiceProvider _serviceProvider; private readonly IProcessProvider _processProvider; private readonly Logger _logger; - public LifestyleService(IEventAggregator eventAggregator, + public LifecycleService(IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, - IAppFolderInfo appFolderInfo, IServiceProvider serviceProvider, IProcessProvider processProvider, Logger logger) { _eventAggregator = eventAggregator; _runtimeInfo = runtimeInfo; - _appFolderInfo = appFolderInfo; _serviceProvider = serviceProvider; _processProvider = processProvider; _logger = logger; @@ -38,7 +35,7 @@ namespace NzbDrone.Core.Lifecycle public void Execute(ShutdownCommand message) { - _logger.ProgressInfo("Shutdown requested, goodbye."); + _logger.Info("Shutdown requested."); _eventAggregator.PublishEvent(new ApplicationShutdownRequested()); if (_runtimeInfo.IsWindowsService) @@ -49,7 +46,13 @@ namespace NzbDrone.Core.Lifecycle public void Execute(RestartCommand message) { - _logger.ProgressInfo("Restart requested, brb."); + _logger.Info("Restart requested."); + + if (OsInfo.IsLinux) + { + _processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, "--terminateexisting --nobrowser"); + } + _eventAggregator.PublishEvent(new ApplicationShutdownRequested(true)); if (_runtimeInfo.IsWindowsService) @@ -59,26 +62,7 @@ namespace NzbDrone.Core.Lifecycle else { - //TODO: move this to environment specific projects - if (OsInfo.IsWindows) - { - if (_runtimeInfo.IsConsole) - { - //Run console with switch - var path = Path.Combine(_appFolderInfo.StartUpFolder, - ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe"); - - _processProvider.SpawnNewProcess(path, "--terminateexisting --nobrowser"); - } - - else - { - var path = Path.Combine(_appFolderInfo.StartUpFolder, - ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe"); - - _processProvider.Start(path, "--terminateexisting --nobrowser"); - } - } + _processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, "--terminateexisting --nobrowser"); } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 1944dc15a..4c85322c3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -292,7 +292,7 @@ - + diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 2890d4ec3..1b0d9fa70 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -51,6 +51,7 @@ namespace NzbDrone.Host catch (TerminateApplicationException e) { Logger.Info(e.Message); + LogManager.Configuration = null; } } diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index ce7d53216..ea4ace057 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Nancy.Bootstrapper; using NzbDrone.Api; using NzbDrone.Common.Composition; diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 7a0f05c83..8ad545c0d 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -117,7 +117,7 @@ - + @@ -195,9 +195,7 @@ NzbDrone.SignalR - - - + diff --git a/src/NzbDrone.Host/NzbDroneProcessService.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs similarity index 72% rename from src/NzbDrone.Host/NzbDroneProcessService.cs rename to src/NzbDrone.Host/SingleInstancePolicy.cs index c13a46f5b..d88b11117 100644 --- a/src/NzbDrone.Host/NzbDroneProcessService.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -16,12 +16,17 @@ namespace NzbDrone.Host { private readonly IProcessProvider _processProvider; private readonly IBrowserService _browserService; + private readonly INzbDroneProcessProvider _nzbDroneProcessProvider; private readonly Logger _logger; - public SingleInstancePolicy(IProcessProvider processProvider, IBrowserService browserService, Logger logger) + public SingleInstancePolicy(IProcessProvider processProvider, + IBrowserService browserService, + INzbDroneProcessProvider nzbDroneProcessProvider, + Logger logger) { _processProvider = processProvider; _browserService = browserService; + _nzbDroneProcessProvider = nzbDroneProcessProvider; _logger = logger; } @@ -51,11 +56,10 @@ namespace NzbDrone.Host private List GetOtherNzbDroneProcessIds() { var currentId = _processProvider.GetCurrentProcess().Id; - var consoleIds = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) - .Select(c => c.Id); - var winformIds = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME).Select(c => c.Id); - - var otherProcesses = consoleIds.Union(winformIds).Except(new[] { currentId }).ToList(); + var otherProcesses = _nzbDroneProcessProvider.GetNzbDroneProcesses() + .Select(c => c.Id) + .Except(new[] {currentId}) + .ToList(); if (otherProcesses.Any()) { diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index 91d0efeb3..e342f9a16 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -70,6 +70,7 @@ + diff --git a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs new file mode 100644 index 000000000..3b2ff34d7 --- /dev/null +++ b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Model; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Mono +{ + public class NzbDroneProcessProvider : INzbDroneProcessProvider + { + private readonly IProcessProvider _processProvider; + + public NzbDroneProcessProvider(IProcessProvider processProvider) + { + _processProvider = processProvider; + } + + public List GetNzbDroneProcesses() + { + var monoProcesses = _processProvider.FindProcessByName("mono"); + + return monoProcesses.Where(c => + { + var processArgs = _processProvider.StartAndCapture("ps", String.Format("--pid {0} -o args=", c.Id)); + + return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME) || + p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)); + }).ToList(); + } + } +} diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index 607f09a4e..77e47f03c 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -63,6 +63,7 @@ + diff --git a/src/NzbDrone.Windows/NzbDroneProcessProvider.cs b/src/NzbDrone.Windows/NzbDroneProcessProvider.cs new file mode 100644 index 000000000..c75f4cf30 --- /dev/null +++ b/src/NzbDrone.Windows/NzbDroneProcessProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Model; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Windows +{ + public class NzbDroneProcessProvider : INzbDroneProcessProvider + { + private readonly IProcessProvider _processProvider; + + public NzbDroneProcessProvider(IProcessProvider processProvider) + { + _processProvider = processProvider; + } + + public List GetNzbDroneProcesses() + { + var consoleProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); + var winformProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); + + return consoleProcesses.Concat(winformProcesses).ToList(); + } + } +} diff --git a/src/UI/System/SystemLayoutTemplate.html b/src/UI/System/SystemLayoutTemplate.html index 3e5a10b8e..1ee3a2349 100644 --- a/src/UI/System/SystemLayoutTemplate.html +++ b/src/UI/System/SystemLayoutTemplate.html @@ -7,11 +7,9 @@ - {{#if_windows}} - {{/if_windows}}
      From 4d3217432c18c23ad3b46563eb8c3ebe7e1b3f90 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 11:40:13 -0800 Subject: [PATCH 45/48] Fixed some unit tests --- .../NzbDroneProcessServiceFixture.cs | 36 +++++-------------- .../EnvironmentInfo/RuntimeInfo.cs | 7 +++- .../Processes/INzbDroneProcessProvider.cs | 2 +- src/NzbDrone.Host/SingleInstancePolicy.cs | 2 +- .../NzbDrone.Mono.Test.csproj | 13 +++++++ .../ServiceFactoryFixture.cs | 31 ++++++++++++++++ src/NzbDrone.Mono.Test/packages.config | 1 + src/NzbDrone.Mono/NzbDroneProcessProvider.cs | 2 +- .../NzbDrone.Windows.Test.csproj | 13 +++++++ .../ServiceFactoryFixture.cs | 31 ++++++++++++++++ src/NzbDrone.Windows.Test/packages.config | 1 + .../NzbDroneProcessProvider.cs | 2 +- src/NzbDrone.sln | 3 ++ 13 files changed, 111 insertions(+), 33 deletions(-) create mode 100644 src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs create mode 100644 src/NzbDrone.Windows.Test/ServiceFactoryFixture.cs diff --git a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs index 0c792f0c3..faef69f54 100644 --- a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs +++ b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs @@ -23,11 +23,7 @@ namespace NzbDrone.App.Test [Test] public void should_continue_if_only_instance() { - Mocker.GetMock() - .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) - .Returns(new List()); - - Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + Mocker.GetMock().Setup(c => c.FindNzbDroneProcesses()) .Returns(new List { new ProcessInfo{Id = CURRENT_PROCESS_ID} @@ -36,29 +32,20 @@ namespace NzbDrone.App.Test Subject.PreventStartIfAlreadyRunning(); - Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Never()); - } [Test] public void should_enforce_if_another_console_is_running() { - Mocker.GetMock() - .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + Mocker.GetMock() + .Setup(c => c.FindNzbDroneProcesses()) .Returns(new List { - new ProcessInfo{Id = 10} - }); - - Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) - .Returns(new List - { + new ProcessInfo{Id = 10}, new ProcessInfo{Id = CURRENT_PROCESS_ID} }); - - Assert.Throws(() => Subject.PreventStartIfAlreadyRunning()); Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); ExceptionVerification.ExpectedWarns(1); @@ -67,22 +54,15 @@ namespace NzbDrone.App.Test [Test] public void should_return_false_if_another_gui_is_running() { - Mocker.GetMock() - .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + Mocker.GetMock() + .Setup(c => c.FindNzbDroneProcesses()) .Returns(new List { - new ProcessInfo{Id = CURRENT_PROCESS_ID} + new ProcessInfo{Id = CURRENT_PROCESS_ID}, + new ProcessInfo{Id = 10} }); - Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) - .Returns(new List - { - new ProcessInfo{Id = 10} - }); - - - Assert.Throws(() => Subject.PreventStartIfAlreadyRunning()); Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); ExceptionVerification.ExpectedWarns(1); diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 314b7cb6a..7f8ebedf3 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -33,7 +33,12 @@ namespace NzbDrone.Common.EnvironmentInfo serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; - ExecutingApplication = Assembly.GetEntryAssembly().Location; + var entry = Assembly.GetEntryAssembly(); + + if (entry != null) + { + ExecutingApplication = entry.Location; + } } static RuntimeInfo() diff --git a/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs b/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs index 29f16b7fa..71abd6d0e 100644 --- a/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/INzbDroneProcessProvider.cs @@ -5,6 +5,6 @@ namespace NzbDrone.Common.Processes { public interface INzbDroneProcessProvider { - List GetNzbDroneProcesses(); + List FindNzbDroneProcesses(); } } diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index d88b11117..2952997cb 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Host private List GetOtherNzbDroneProcessIds() { var currentId = _processProvider.GetCurrentProcess().Id; - var otherProcesses = _nzbDroneProcessProvider.GetNzbDroneProcesses() + var otherProcesses = _nzbDroneProcessProvider.FindNzbDroneProcesses() .Select(c => c.Id) .Except(new[] {currentId}) .ToList(); diff --git a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj index 4d31659ea..6b5b32c5b 100644 --- a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj +++ b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj @@ -50,6 +50,10 @@ MinimumRecommendedRules.ruleset + + False + ..\packages\FluentAssertions.2.1.0.0\lib\net40\FluentAssertions.dll + ..\packages\NUnit.2.6.2\lib\nunit.framework.dll @@ -66,6 +70,7 @@ + @@ -79,6 +84,14 @@ {f2be0fdf-6e47-4827-a420-dd4ef82407f8} NzbDrone.Common + + {ff5ee3b6-913b-47ce-9ceb-11c51b4e1205} + NzbDrone.Core + + + {95c11a9e-56ed-456a-8447-2c89c1139266} + NzbDrone.Host + {15ad7579-a314-4626-b556-663f51d97cd1} NzbDrone.Mono diff --git a/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs new file mode 100644 index 000000000..1d5e77f87 --- /dev/null +++ b/src/NzbDrone.Mono.Test/ServiceFactoryFixture.cs @@ -0,0 +1,31 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Host; +using NzbDrone.Test.Common; + +namespace NzbDrone.Mono.Test +{ + [TestFixture] + public class ServiceFactoryFixture : TestBase + { + [SetUp] + public void setup() + { + Mocker.SetConstant(MainAppContainerBuilder.BuildContainer(new StartupContext())); + } + + [Test] + public void event_handlers_should_be_unique() + { + var handlers = Subject.BuildAll>() + .Select(c => c.GetType().FullName); + + handlers.Should().OnlyHaveUniqueItems(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/packages.config b/src/NzbDrone.Mono.Test/packages.config index 5c3ca54dd..e6606e669 100644 --- a/src/NzbDrone.Mono.Test/packages.config +++ b/src/NzbDrone.Mono.Test/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs index 3b2ff34d7..147efb5b8 100644 --- a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs +++ b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Mono _processProvider = processProvider; } - public List GetNzbDroneProcesses() + public List FindNzbDroneProcesses() { var monoProcesses = _processProvider.FindProcessByName("mono"); diff --git a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj index 098c95b80..309005f99 100644 --- a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj +++ b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj @@ -50,6 +50,10 @@ MinimumRecommendedRules.ruleset + + False + ..\packages\FluentAssertions.2.1.0.0\lib\net40\FluentAssertions.dll + False ..\packages\NUnit.2.6.2\lib\nunit.framework.dll @@ -67,6 +71,7 @@ + @@ -77,6 +82,14 @@ {f2be0fdf-6e47-4827-a420-dd4ef82407f8} NzbDrone.Common + + {ff5ee3b6-913b-47ce-9ceb-11c51b4e1205} + NzbDrone.Core + + + {95c11a9e-56ed-456a-8447-2c89c1139266} + NzbDrone.Host + {caddfce0-7509-4430-8364-2074e1eefca2} NzbDrone.Test.Common diff --git a/src/NzbDrone.Windows.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Windows.Test/ServiceFactoryFixture.cs new file mode 100644 index 000000000..bde389da2 --- /dev/null +++ b/src/NzbDrone.Windows.Test/ServiceFactoryFixture.cs @@ -0,0 +1,31 @@ +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Host; +using NzbDrone.Test.Common; + +namespace NzbDrone.Windows.Test +{ + [TestFixture] + public class ServiceFactoryFixture : TestBase + { + [SetUp] + public void setup() + { + Mocker.SetConstant(MainAppContainerBuilder.BuildContainer(new StartupContext())); + } + + [Test] + public void event_handlers_should_be_unique() + { + var handlers = Subject.BuildAll>() + .Select(c => c.GetType().FullName); + + handlers.Should().OnlyHaveUniqueItems(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Windows.Test/packages.config b/src/NzbDrone.Windows.Test/packages.config index 5c3ca54dd..e6606e669 100644 --- a/src/NzbDrone.Windows.Test/packages.config +++ b/src/NzbDrone.Windows.Test/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/src/NzbDrone.Windows/NzbDroneProcessProvider.cs b/src/NzbDrone.Windows/NzbDroneProcessProvider.cs index c75f4cf30..1e18b6c50 100644 --- a/src/NzbDrone.Windows/NzbDroneProcessProvider.cs +++ b/src/NzbDrone.Windows/NzbDroneProcessProvider.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Windows _processProvider = processProvider; } - public List GetNzbDroneProcesses() + public List FindNzbDroneProcesses() { var consoleProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); var winformProcesses = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 6607c1c36..fa28f72be 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -73,6 +73,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" + ProjectSection(SolutionItems) = preProject + NzbDrone.Common.Test\ServiceFactoryFixture.cs = NzbDrone.Common.Test\ServiceFactoryFixture.cs + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" EndProject From f5d85b8f574dfcce2258acbf5d808954a75bad90 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 11:59:00 -0800 Subject: [PATCH 46/48] Mono NzbDrone processes are more restricted --- .../NzbDrone.Common.Test.csproj | 1 - .../ServiceFactoryFixture.cs | 30 ------------------- src/NzbDrone.Mono/NzbDroneProcessProvider.cs | 4 +-- 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 src/NzbDrone.Common.Test/ServiceFactoryFixture.cs diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index e59fb8bf9..80c3a159e 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -73,7 +73,6 @@ - diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs deleted file mode 100644 index 157d0893d..000000000 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.Host; -using NzbDrone.Test.Common; - -namespace NzbDrone.Common.Test -{ - [TestFixture] - public class ServiceFactoryFixture : TestBase - { - [SetUp] - public void setup() - { - Mocker.SetConstant(MainAppContainerBuilder.BuildContainer(new StartupContext())); - } - - [Test] - public void event_handlers_should_be_unique() - { - var handlers = Subject.BuildAll>() - .Select(c => c.GetType().FullName); - - handlers.Should().OnlyHaveUniqueItems(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs index 147efb5b8..630b6e665 100644 --- a/src/NzbDrone.Mono/NzbDroneProcessProvider.cs +++ b/src/NzbDrone.Mono/NzbDroneProcessProvider.cs @@ -23,8 +23,8 @@ namespace NzbDrone.Mono { var processArgs = _processProvider.StartAndCapture("ps", String.Format("--pid {0} -o args=", c.Id)); - return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME) || - p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)); + return processArgs.Standard.Any(p => p.Contains(ProcessProvider.NZB_DRONE_PROCESS_NAME + ".exe") || + p.Contains(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME + ".exe")); }).ToList(); } } From bc908e044062d9998f89e6edb44fd21015c39d7d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 12:24:59 -0800 Subject: [PATCH 47/48] Binding signalr to cloned series collection --- .../EnvironmentInfo/RuntimeInfo.cs | 1 + src/UI/Series/Index/SeriesIndexLayout.js | 15 ++++++--------- src/UI/Series/SeriesCollection.js | 5 ++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 7f8ebedf3..82369c35b 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Common.EnvironmentInfo serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) && serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending; + //Guarded to avoid issues when running in a non-managed process var entry = Assembly.GetEntryAssembly(); if (entry != null) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 0896d35c3..bfd37e8e7 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -17,7 +17,8 @@ define( 'Cells/SeriesStatusCell', 'Series/Index/FooterView', 'Series/Index/FooterModel', - 'Shared/Toolbar/ToolbarLayout' + 'Shared/Toolbar/ToolbarLayout', + 'Mixins/backbone.signalr.mixin' ], function (_, Marionette, Backgrid, @@ -130,27 +131,23 @@ define( initialize: function () { this.seriesCollection = SeriesCollection.clone(); + this.seriesCollection.shadowCollection.bindSignalR(); - this.listenTo(SeriesCollection, 'sync', function (model, collection, options) { - this.seriesCollection.shadowCollection.add(model, options); + this.listenTo(this.seriesCollection.shadowCollection, 'sync', function (model, collection, options) { this.seriesCollection.fullCollection.resetFiltered(); this._renderView(); }); - this.listenTo(SeriesCollection, 'add', function (model, collection, options) { - this.seriesCollection.shadowCollection.add(model, options); + this.listenTo(this.seriesCollection.shadowCollection, 'add', function (model, collection, options) { this.seriesCollection.fullCollection.resetFiltered(); this._renderView(); }); - this.listenTo(SeriesCollection, 'remove', function (model, collection, options) { - this.seriesCollection.shadowCollection.remove(model, options); + this.listenTo(this.seriesCollection.shadowCollection, 'remove', function (model, collection, options) { this.seriesCollection.fullCollection.resetFiltered(); this._renderView(); }); - - this.sortingOptions = { type : 'sorting', storeState : false, diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index bbd70e940..4f00e6c42 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -8,8 +8,7 @@ define( 'api!series', 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection', - 'moment', - 'Mixins/backbone.signalr.mixin' + 'moment' ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsPersistedStateCollection, Moment) { var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', @@ -73,5 +72,5 @@ define( var MixedIn = AsPersistedStateCollection.call(FilteredCollection); var collection = new MixedIn(SeriesData, { full: true }); - return collection.bindSignalR(); + return collection; }); From 3d3390187e03bbee985b094e367dc47d6337b778 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Feb 2014 17:09:31 -0800 Subject: [PATCH 48/48] New: Optionally disable notifications for upgraded episode files --- .../Notifications/NotificationResource.cs | 1 + .../038_add_on_upgrade_to_notifications.cs | 16 +++++++++ .../Notifications/NotificationDefinition.cs | 1 + .../Notifications/NotificationFactory.cs | 6 ++++ .../Notifications/NotificationService.cs | 5 +++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/UI/Settings/Notifications/AddItemView.js | 9 ++--- .../Notifications/NotificationEditView.js | 24 ++++++++++++- .../NotificationEditViewTemplate.html | 34 +++++++++++++++---- src/UI/Settings/SettingsLayout.js | 2 +- 10 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs diff --git a/src/NzbDrone.Api/Notifications/NotificationResource.cs b/src/NzbDrone.Api/Notifications/NotificationResource.cs index 54ffe720a..51c7fb7df 100644 --- a/src/NzbDrone.Api/Notifications/NotificationResource.cs +++ b/src/NzbDrone.Api/Notifications/NotificationResource.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Api.Notifications public String Link { get; set; } public Boolean OnGrab { get; set; } public Boolean OnDownload { get; set; } + public Boolean OnUpgrade { get; set; } public String TestCommand { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs b/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs new file mode 100644 index 000000000..f5cae2ba0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/038_add_on_upgrade_to_notifications.cs @@ -0,0 +1,16 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(38)] + public class add_on_upgrade_to_notifications : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Notifications").AddColumn("OnUpgrade").AsBoolean().Nullable(); + + Execute.Sql("UPDATE Notifications SET OnUpgrade = OnDownload"); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index a48d2b28d..b1143db3f 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -7,5 +7,6 @@ namespace NzbDrone.Core.Notifications { public Boolean OnGrab { get; set; } public Boolean OnDownload { get; set; } + public Boolean OnUpgrade { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs index 54037acef..4a4fc9bc2 100644 --- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs +++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Notifications { List OnGrabEnabled(); List OnDownloadEnabled(); + List OnUpgradeEnabled(); } public class NotificationFactory : ProviderFactory, INotificationFactory @@ -28,5 +29,10 @@ namespace NzbDrone.Core.Notifications { return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnDownload).ToList(); } + + public List OnUpgradeEnabled() + { + return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnUpgrade).ToList(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 768c3fda3..e52dd1fac 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -80,6 +80,11 @@ namespace NzbDrone.Core.Notifications { try { + if (downloadMessage.OldFiles.Any() && !((NotificationDefinition) notification.Definition).OnUpgrade) + { + continue; + } + notification.OnDownload(downloadMessage); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4c85322c3..c09e55f48 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -194,6 +194,7 @@ + diff --git a/src/UI/Settings/Notifications/AddItemView.js b/src/UI/Settings/Notifications/AddItemView.js index c1264354b..2c031b3e0 100644 --- a/src/UI/Settings/Notifications/AddItemView.js +++ b/src/UI/Settings/Notifications/AddItemView.js @@ -24,10 +24,11 @@ define([ } this.model.set({ - id: undefined, - name: this.model.get('implementationName'), - onGrab: true, - onDownload: true + id : undefined, + name : this.model.get('implementationName'), + onGrab : true, + onDownload : true, + onUpgrade : true }); var editView = new EditView({ model: this.model, notificationCollection: this.notificationCollection }); diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js index 92a6f8af1..03de47197 100644 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ b/src/UI/Settings/Notifications/NotificationEditView.js @@ -16,18 +16,28 @@ define( var model = Marionette.ItemView.extend({ template: 'Settings/Notifications/NotificationEditViewTemplate', + ui: { + onDownloadToggle: '.x-on-download', + onUpgradeSection: '.x-on-upgrade' + }, + events: { 'click .x-save' : '_saveNotification', 'click .x-save-and-add': '_saveAndAddNotification', 'click .x-delete' : '_deleteNotification', 'click .x-back' : '_back', - 'click .x-test' : '_test' + 'click .x-test' : '_test', + 'change .x-on-download': '_onDownloadChanged' }, initialize: function (options) { this.notificationCollection = options.notificationCollection; }, + onRender: function () { + this._onDownloadChanged(); + }, + _saveNotification: function () { var self = this; var promise = this.model.saveSettings(); @@ -71,6 +81,18 @@ define( }); CommandController.Execute(testCommand, properties); + }, + + _onDownloadChanged: function () { + var checked = this.ui.onDownloadToggle.prop('checked'); + + if (checked) { + this.ui.onUpgradeSection.show(); + } + + else { + this.ui.onUpgradeSection.hide(); + } } }); diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html index c785fe9c1..e6abfa829 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html @@ -30,9 +30,9 @@
      - - - + + +
      @@ -41,7 +41,7 @@
      + +
      + + +
      +
      diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index db90a0a7d..2770feee5 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -66,7 +66,7 @@ define( 'click .x-notifications-tab' : '_showNotifications', 'click .x-general-tab' : '_showGeneral', 'click .x-save-settings' : '_save', - 'change .x-advanced-settings' : '_toggleAdvancedSettings' + 'change .x-advanced-settings' : '_toggleAdvancedSettings' }, initialize: function (options) {