diff --git a/src/Marr.Data/EntityGraph.cs b/src/Marr.Data/EntityGraph.cs
index aee376b61..72d28dcdf 100644
--- a/src/Marr.Data/EntityGraph.cs
+++ b/src/Marr.Data/EntityGraph.cs
@@ -160,6 +160,14 @@ namespace Marr.Data
get { return _children; }
}
+ ///
+ /// Adds an Child in the graph for LazyLoaded property.
+ ///
+ public void AddLazyRelationship(Relationship childRelationship)
+ {
+ _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType.GetGenericArguments()[0], this, childRelationship));
+ }
+
///
/// Adds an entity to the appropriate place in the object graph.
///
@@ -182,7 +190,10 @@ namespace Marr.Data
}
else // RelationTypes.One
{
- _relationship.Setter(_parent._entity, entityInstance);
+ if (_relationship.IsLazyLoaded)
+ _relationship.Setter(_parent._entity, Activator.CreateInstance(_relationship.MemberType, entityInstance));
+ else
+ _relationship.Setter(_parent._entity, entityInstance);
}
EntityReference entityRef = new EntityReference(entityInstance);
diff --git a/src/Marr.Data/QGen/QueryBuilder.cs b/src/Marr.Data/QGen/QueryBuilder.cs
index cd71c17bd..ba135ac07 100644
--- a/src/Marr.Data/QGen/QueryBuilder.cs
+++ b/src/Marr.Data/QGen/QueryBuilder.cs
@@ -551,6 +551,23 @@ namespace Marr.Data.QGen
return Join(joinType, rightMember, filterExpression);
}
+ public virtual QueryBuilder Join(JoinType joinType, Expression>> rightEntity, Expression> filterExpression)
+ {
+ _isJoin = true;
+ MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member;
+
+ foreach (var item in EntGraph)
+ {
+ if (item.EntityType == typeof(TLeft))
+ {
+ var relationship = item.Relationships.Single(v => v.Member == rightMember);
+ item.AddLazyRelationship(relationship);
+ }
+ }
+
+ return Join(joinType, rightMember, filterExpression);
+ }
+
public virtual QueryBuilder Join(JoinType joinType, MemberInfo rightMember, Expression> filterExpression)
{
_isJoin = true;
diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj
index 9ca8abe50..3305a880d 100644
--- a/src/NzbDrone.Api/NzbDrone.Api.csproj
+++ b/src/NzbDrone.Api/NzbDrone.Api.csproj
@@ -150,7 +150,9 @@
-
+
+
+
diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs
new file mode 100644
index 000000000..ee0b9f219
--- /dev/null
+++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs
@@ -0,0 +1,48 @@
+using System.Linq;
+using NzbDrone.Api.Episodes;
+using NzbDrone.Api.Extensions;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Api.Wanted
+{
+ public class CutoffModule : NzbDroneRestModule
+ {
+ private readonly IEpisodeCutoffService _episodeCutoffService;
+ private readonly SeriesRepository _seriesRepository;
+
+ public CutoffModule(IEpisodeCutoffService episodeCutoffService, SeriesRepository seriesRepository)
+ :base("wanted/cutoff")
+ {
+ _episodeCutoffService = episodeCutoffService;
+ _seriesRepository = seriesRepository;
+ GetResourcePaged = GetCutoffUnmetEpisodes;
+ }
+
+ private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource)
+ {
+ var pagingSpec = new PagingSpec
+ {
+ Page = pagingResource.Page,
+ PageSize = pagingResource.PageSize,
+ SortKey = pagingResource.SortKey,
+ SortDirection = pagingResource.SortDirection
+ };
+
+ if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false")
+ {
+ pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false;
+ }
+ else
+ {
+ pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true;
+ }
+
+ PagingResource resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec);
+
+ resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList();
+
+ return resource;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs b/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs
new file mode 100644
index 000000000..1fe0bb6ca
--- /dev/null
+++ b/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text;
+using Nancy;
+
+namespace NzbDrone.Api.Wanted
+{
+ class LegacyMissingModule : NzbDroneApiModule
+ {
+ public LegacyMissingModule() : base("missing")
+ {
+ Get["/"] = x =>
+ {
+ string queryString = ConvertQueryParams(Request.Query);
+ var url = String.Format("/api/wanted/missing?{0}", queryString);
+
+ return Response.AsRedirect(url);
+ };
+ }
+
+ private string ConvertQueryParams(DynamicDictionary query)
+ {
+ var sb = new StringBuilder();
+
+ foreach (var key in query)
+ {
+ var value = query[key];
+
+ sb.AppendFormat("&{0}={1}", key, value);
+ }
+
+ return sb.ToString().Trim('&');
+ }
+ }
+}
diff --git a/src/NzbDrone.Api/Missing/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs
similarity index 67%
rename from src/NzbDrone.Api/Missing/MissingModule.cs
rename to src/NzbDrone.Api/Wanted/MissingModule.cs
index 968da7aab..dd4d97f69 100644
--- a/src/NzbDrone.Api/Missing/MissingModule.cs
+++ b/src/NzbDrone.Api/Wanted/MissingModule.cs
@@ -4,7 +4,7 @@ using NzbDrone.Api.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Tv;
-namespace NzbDrone.Api.Missing
+namespace NzbDrone.Api.Wanted
{
public class MissingModule : NzbDroneRestModule
{
@@ -12,7 +12,7 @@ namespace NzbDrone.Api.Missing
private readonly SeriesRepository _seriesRepository;
public MissingModule(IEpisodeService episodeService, SeriesRepository seriesRepository)
- :base("missing")
+ :base("wanted/missing")
{
_episodeService = episodeService;
_seriesRepository = seriesRepository;
@@ -29,7 +29,17 @@ namespace NzbDrone.Api.Missing
SortDirection = pagingResource.SortDirection
};
- var resource = ApplyToPage(_episodeService.EpisodesWithoutFiles, pagingSpec);
+ if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false")
+ {
+ pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false;
+ }
+ else
+ {
+ pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true;
+ }
+
+ PagingResource resource = ApplyToPage(v => _episodeService.EpisodesWithoutFiles(v), pagingSpec);
+
resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList();
return resource;
diff --git a/src/NzbDrone.Automation.Test/MainPagesTest.cs b/src/NzbDrone.Automation.Test/MainPagesTest.cs
index 0b20f3c35..f10969108 100644
--- a/src/NzbDrone.Automation.Test/MainPagesTest.cs
+++ b/src/NzbDrone.Automation.Test/MainPagesTest.cs
@@ -45,12 +45,12 @@ namespace NzbDrone.Automation.Test
}
[Test]
- public void missing_page()
+ public void wanted_page()
{
- page.MissingNavIcon.Click();
+ page.WantedNavIcon.Click();
page.WaitForNoSpinner();
- page.FindByClass("iv-missing-missinglayout").Should().NotBeNull();
+ page.FindByClass("iv-wanted-missing-missinglayout").Should().NotBeNull();
}
[Test]
diff --git a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs
index 3ed74c8fb..6ba89f5ea 100644
--- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs
+++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs
@@ -72,11 +72,11 @@ namespace NzbDrone.Automation.Test.PageModel
}
}
- public IWebElement MissingNavIcon
+ public IWebElement WantedNavIcon
{
get
{
- return Find(By.LinkText("Missing"));
+ return Find(By.LinkText("Wanted"));
}
}
diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs
new file mode 100644
index 000000000..32aee7226
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs
@@ -0,0 +1,105 @@
+using FizzWare.NBuilder;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.MediaFiles;
+
+namespace NzbDrone.Core.Test.Datastore
+{
+
+ [TestFixture]
+ public class MarrDataLazyLoadingFixture : DbTest
+ {
+ [SetUp]
+ public void Setup()
+ {
+ var qualityProfile = new NzbDrone.Core.Qualities.QualityProfile
+ {
+ Name = "Test",
+ Cutoff = Quality.WEBDL720p,
+ Items = NzbDrone.Core.Test.Qualities.QualityFixture.GetDefaultQualities()
+ };
+
+
+ qualityProfile = Db.Insert(qualityProfile);
+
+ var series = Builder.CreateListOfSize(1)
+ .All()
+ .With(v => v.QualityProfileId = qualityProfile.Id)
+ .BuildListOfNew();
+
+ Db.InsertMany(series);
+
+ var episodeFiles = Builder.CreateListOfSize(1)
+ .All()
+ .With(v => v.SeriesId = series[0].Id)
+ .BuildListOfNew();
+
+ Db.InsertMany(episodeFiles);
+
+ var episodes = Builder.CreateListOfSize(10)
+ .All()
+ .With(v => v.Monitored = true)
+ .With(v => v.EpisodeFileId = episodeFiles[0].Id)
+ .With(v => v.SeriesId = series[0].Id)
+ .BuildListOfNew();
+
+ Db.InsertMany(episodes);
+ }
+
+ [Test]
+ public void should_lazy_load_qualityprofile_if_not_joined()
+ {
+ var db = Mocker.Resolve();
+ var DataMapper = db.GetDataMapper();
+
+ var episodes = DataMapper.Query()
+ .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id)
+ .ToList();
+
+ foreach (var episode in episodes)
+ {
+ Assert.IsNotNull(episode.Series);
+ Assert.IsFalse(episode.Series.QualityProfile.IsLoaded);
+ }
+ }
+
+ [Test]
+ public void should_explicit_load_episodefile_if_joined()
+ {
+ var db = Mocker.Resolve();
+ var DataMapper = db.GetDataMapper();
+
+ var episodes = DataMapper.Query()
+ .Join(Marr.Data.QGen.JoinType.Inner, v => v.EpisodeFile, (l, r) => l.EpisodeFileId == r.Id)
+ .ToList();
+
+ foreach (var episode in episodes)
+ {
+ Assert.IsNull(episode.Series);
+ Assert.IsTrue(episode.EpisodeFile.IsLoaded);
+ }
+ }
+
+ [Test]
+ public void should_explicit_load_qualityprofile_if_joined()
+ {
+ var db = Mocker.Resolve();
+ var DataMapper = db.GetDataMapper();
+
+ var episodes = DataMapper.Query()
+ .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id)
+ .Join(Marr.Data.QGen.JoinType.Inner, v => v.QualityProfile, (l, r) => l.QualityProfileId == r.Id)
+ .ToList();
+
+ foreach (var episode in episodes)
+ {
+ Assert.IsNotNull(episode.Series);
+ Assert.IsTrue(episode.Series.QualityProfile.IsLoaded);
+ }
+ }
+
+ }
+}
\ 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 fe5a842f8..3b03dfe3e 100644
--- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -104,6 +104,7 @@
+
@@ -211,6 +212,7 @@
+
diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs
new file mode 100644
index 000000000..5a3f2e1fc
--- /dev/null
+++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.MediaFiles;
+
+namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
+{
+ [TestFixture]
+ public class EpisodesWhereCutoffUnmetFixture : DbTest
+ {
+ private Series _monitoredSeries;
+ private Series _unmonitoredSeries;
+ private PagingSpec _pagingSpec;
+ private List _qualitiesBelowCutoff;
+
+ [SetUp]
+ public void Setup()
+ {
+ var qualityProfile = new QualityProfile
+ {
+ Id = 1,
+ Cutoff = Quality.WEBDL480p,
+ Items = new List
+ {
+ new QualityProfileItem { Allowed = true, Quality = Quality.SDTV },
+ new QualityProfileItem { Allowed = true, Quality = Quality.WEBDL480p },
+ new QualityProfileItem { Allowed = true, Quality = Quality.RAWHD }
+ }
+ };
+
+ _monitoredSeries = Builder.CreateNew()
+ .With(s => s.TvRageId = RandomNumber)
+ .With(s => s.Runtime = 30)
+ .With(s => s.Monitored = true)
+ .With(s => s.TitleSlug = "Title3")
+ .With(s => s.Id = qualityProfile.Id)
+ .BuildNew();
+
+ _unmonitoredSeries = Builder.CreateNew()
+ .With(s => s.TvdbId = RandomNumber)
+ .With(s => s.Runtime = 30)
+ .With(s => s.Monitored = false)
+ .With(s => s.TitleSlug = "Title2")
+ .With(s => s.Id = qualityProfile.Id)
+ .BuildNew();
+
+ _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id;
+ _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id;
+
+ _pagingSpec = new PagingSpec
+ {
+ Page = 1,
+ PageSize = 10,
+ SortKey = "AirDate",
+ SortDirection = SortDirection.Ascending
+ };
+
+ _qualitiesBelowCutoff = new List
+ {
+ new QualitiesBelowCutoff(qualityProfile.Id, new[] {Quality.SDTV.Id})
+ };
+
+ var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL480p } };
+ var qualityUnmet = new EpisodeFile { Path = "b", Quality = new QualityModel { Quality = Quality.SDTV } };
+ var qualityRawHD = new EpisodeFile { Path = "c", Quality = new QualityModel { Quality = Quality.RAWHD } };
+
+ MediaFileRepository fileRepository = Mocker.Resolve();
+
+ qualityMet = fileRepository.Insert(qualityMet);
+ qualityUnmet = fileRepository.Insert(qualityUnmet);
+ qualityRawHD = fileRepository.Insert(qualityRawHD);
+
+ var monitoredSeriesEpisodes = Builder.CreateListOfSize(4)
+ .All()
+ .With(e => e.Id = 0)
+ .With(e => e.SeriesId = _monitoredSeries.Id)
+ .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5))
+ .With(e => e.Monitored = true)
+ .With(e => e.EpisodeFileId = qualityUnmet.Id)
+ .TheFirst(1)
+ .With(e => e.Monitored = false)
+ .With(e => e.EpisodeFileId = qualityMet.Id)
+ .TheNext(1)
+ .With(e => e.EpisodeFileId = qualityRawHD.Id)
+ .TheLast(1)
+ .With(e => e.SeasonNumber = 0)
+ .Build();
+
+ var unmonitoredSeriesEpisodes = Builder.CreateListOfSize(3)
+ .All()
+ .With(e => e.Id = 0)
+ .With(e => e.SeriesId = _unmonitoredSeries.Id)
+ .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5))
+ .With(e => e.Monitored = true)
+ .With(e => e.EpisodeFileId = qualityUnmet.Id)
+ .TheFirst(1)
+ .With(e => e.Monitored = false)
+ .With(e => e.EpisodeFileId = qualityMet.Id)
+ .TheLast(1)
+ .With(e => e.SeasonNumber = 0)
+ .Build();
+
+
+ var unairedEpisodes = Builder.CreateListOfSize(1)
+ .All()
+ .With(e => e.Id = 0)
+ .With(e => e.SeriesId = _monitoredSeries.Id)
+ .With(e => e.AirDateUtc = DateTime.Now.AddDays(5))
+ .With(e => e.Monitored = true)
+ .With(e => e.EpisodeFileId = qualityUnmet.Id)
+ .Build();
+
+ Db.InsertMany(monitoredSeriesEpisodes);
+ Db.InsertMany(unmonitoredSeriesEpisodes);
+ Db.InsertMany(unairedEpisodes);
+ }
+
+ private void GivenMonitoredFilterExpression()
+ {
+ _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true;
+ }
+
+ private void GivenUnmonitoredFilterExpression()
+ {
+ _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false;
+ }
+
+ [Test]
+ public void should_include_episodes_where_cutoff_has_not_be_met()
+ {
+ GivenMonitoredFilterExpression();
+
+ var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false);
+
+ spec.Records.Should().HaveCount(1);
+ spec.Records.Should().OnlyContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.SDTV);
+ }
+
+ [Test]
+ public void should_only_contain_monitored_episodes()
+ {
+ GivenMonitoredFilterExpression();
+
+ var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false);
+
+ spec.Records.Should().HaveCount(1);
+ spec.Records.Should().OnlyContain(e => e.Monitored);
+ }
+
+ [Test]
+ public void should_only_contain_episode_with_monitored_series()
+ {
+ GivenMonitoredFilterExpression();
+
+ var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false);
+
+ spec.Records.Should().HaveCount(1);
+ spec.Records.Should().OnlyContain(e => e.Series.Monitored);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
index e59c67dec..5f7afc669 100644
--- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
+++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs
@@ -72,13 +72,36 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
.Build();
+ var unairedEpisodes = Builder.CreateListOfSize(1)
+ .All()
+ .With(e => e.Id = 0)
+ .With(e => e.SeriesId = _monitoredSeries.Id)
+ .With(e => e.EpisodeFileId = 0)
+ .With(e => e.AirDateUtc = DateTime.Now.AddDays(5))
+ .With(e => e.Monitored = true)
+ .Build();
+
+
Db.InsertMany(monitoredSeriesEpisodes);
Db.InsertMany(unmonitoredSeriesEpisodes);
+ Db.InsertMany(unairedEpisodes);
+ }
+
+ private void GivenMonitoredFilterExpression()
+ {
+ _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true;
+ }
+
+ private void GivenUnmonitoredFilterExpression()
+ {
+ _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false;
}
[Test]
public void should_get_monitored_episodes()
{
+ GivenMonitoredFilterExpression();
+
var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false);
episodes.Records.Should().HaveCount(1);
@@ -96,6 +119,8 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
[Test]
public void should_not_include_unmonitored_episodes()
{
+ GivenMonitoredFilterExpression();
+
var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false);
episodes.Records.Should().NotContain(e => e.Monitored == false);
@@ -104,17 +129,19 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests
[Test]
public void should_not_contain_unmonitored_series()
{
+ GivenMonitoredFilterExpression();
+
var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false);
episodes.Records.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id);
}
[Test]
- public void should_have_count_of_one()
+ public void should_not_return_unaired()
{
var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false);
- episodes.TotalRecords.Should().Be(1);
+ episodes.TotalRecords.Should().Be(4);
}
}
}
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index 0c22a0648..e6194a412 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -512,6 +512,7 @@
+
@@ -536,6 +537,7 @@
+
diff --git a/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs b/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs
new file mode 100644
index 000000000..7d1d2c498
--- /dev/null
+++ b/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+
+namespace NzbDrone.Core.Qualities
+{
+ public class QualitiesBelowCutoff
+ {
+ public Int32 ProfileId { get; set; }
+ public IEnumerable QualityIds { get; set; }
+
+ public QualitiesBelowCutoff(int profileId, IEnumerable qualityIds)
+ {
+ ProfileId = profileId;
+ QualityIds = qualityIds;
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs
new file mode 100644
index 000000000..f88dfc02e
--- /dev/null
+++ b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+using NLog;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Qualities;
+
+namespace NzbDrone.Core.Tv
+{
+ public interface IEpisodeCutoffService
+ {
+ PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec);
+ }
+
+ public class EpisodeCutoffService : IEpisodeCutoffService
+ {
+ private readonly IEpisodeRepository _episodeRepository;
+ private readonly IQualityProfileService _qualityProfileService;
+ private readonly Logger _logger;
+
+ public EpisodeCutoffService(IEpisodeRepository episodeRepository, IQualityProfileService qualityProfileService, Logger logger)
+ {
+ _episodeRepository = episodeRepository;
+ _qualityProfileService = qualityProfileService;
+ _logger = logger;
+ }
+
+ public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec)
+ {
+ var qualitiesBelowCutoff = new List();
+ var qualityProfiles = _qualityProfileService.All();
+
+ //Get all items less than the cutoff
+ foreach (var qualityProfile in qualityProfiles)
+ {
+ var cutoffIndex = qualityProfile.Items.FindIndex(v => v.Quality == qualityProfile.Cutoff);
+ var belowCutoff = qualityProfile.Items.Take(cutoffIndex).ToList();
+
+ if (belowCutoff.Any())
+ {
+ qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(qualityProfile.Id, belowCutoff.Select(i => i.Quality.Id)));
+ }
+ }
+
+ return _episodeRepository.EpisodesWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, false);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs
index dcbe99e1e..3324ec189 100644
--- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs
+++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs
@@ -4,7 +4,8 @@ using System.Linq;
using Marr.Data.QGen;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
-
+using NzbDrone.Core.MediaFiles;
+using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Tv
{
@@ -18,6 +19,7 @@ namespace NzbDrone.Core.Tv
List GetEpisodes(int seriesId, int seasonNumber);
List GetEpisodeByFileId(int fileId);
PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials);
+ PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, bool includeSpecials);
Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
List EpisodesBetweenDates(DateTime startDate, DateTime endDate);
void SetMonitoredFlat(Episode episode, bool monitored);
@@ -91,8 +93,24 @@ namespace NzbDrone.Core.Tv
startingSeasonNumber = 0;
}
- pagingSpec.Records = GetEpisodesWithoutFilesQuery(pagingSpec, currentTime, startingSeasonNumber).ToList();
- pagingSpec.TotalRecords = GetEpisodesWithoutFilesQuery(pagingSpec, currentTime, startingSeasonNumber).GetRowCount();
+ pagingSpec.TotalRecords = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).GetRowCount();
+ pagingSpec.Records = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).ToList();
+
+ return pagingSpec;
+ }
+
+ public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, bool includeSpecials)
+ {
+ var currentTime = DateTime.UtcNow;
+ var startingSeasonNumber = 1;
+
+ if (includeSpecials)
+ {
+ startingSeasonNumber = 0;
+ }
+
+ pagingSpec.TotalRecords = EpisodesWhereCutoffUnmetQuery(pagingSpec, currentTime, qualitiesBelowCutoff, startingSeasonNumber).GetRowCount();
+ pagingSpec.Records = EpisodesWhereCutoffUnmetQuery(pagingSpec, currentTime, qualitiesBelowCutoff, startingSeasonNumber).ToList();
return pagingSpec;
}
@@ -142,17 +160,45 @@ namespace NzbDrone.Core.Tv
SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId);
}
- private SortBuilder GetEpisodesWithoutFilesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber)
+ private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber)
{
return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
- .Where(e => e.EpisodeFileId == 0)
- .AndWhere(e => e.SeasonNumber >= startingSeasonNumber)
- .AndWhere(e => e.AirDateUtc <= currentTime)
- .AndWhere(e => e.Monitored)
- .AndWhere(e => e.Series.Monitored)
- .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
- .Skip(pagingSpec.PagingOffset())
- .Take(pagingSpec.PageSize);
+ .Where(pagingSpec.FilterExpression)
+ .AndWhere(e => e.EpisodeFileId == 0)
+ .AndWhere(e => e.SeasonNumber >= startingSeasonNumber)
+ .AndWhere(e => e.AirDateUtc <= currentTime)
+ .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
+ .Skip(pagingSpec.PagingOffset())
+ .Take(pagingSpec.PageSize);
+ }
+
+ private SortBuilder EpisodesWhereCutoffUnmetQuery(PagingSpec pagingSpec, DateTime currentTime, List qualitiesBelowCutoff, int startingSeasonNumber)
+ {
+ return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
+ .Join(JoinType.Left, e => e.EpisodeFile, (e, s) => e.EpisodeFileId == s.Id)
+ .Where(pagingSpec.FilterExpression)
+ .AndWhere(e => e.EpisodeFileId != 0)
+ .AndWhere(e => e.SeasonNumber >= startingSeasonNumber)
+ .AndWhere(e => e.AirDateUtc <= currentTime)
+ .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff))
+ .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection())
+ .Skip(pagingSpec.PagingOffset())
+ .Take(pagingSpec.PageSize);
+ }
+
+ private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff)
+ {
+ var clauses = new List();
+
+ foreach (var profile in qualitiesBelowCutoff)
+ {
+ foreach (var belowCutoff in profile.QualityIds)
+ {
+ clauses.Add(String.Format("([t1].[QualityProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
+ }
+ }
+
+ return String.Format("({0})", String.Join(" OR ", clauses));
}
}
}
diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs
index df27033a9..20a575884 100644
--- a/src/NzbDrone.Core/Tv/EpisodeService.cs
+++ b/src/NzbDrone.Core/Tv/EpisodeService.cs
@@ -7,6 +7,7 @@ using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Events;
+using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Tv
{
@@ -40,12 +41,14 @@ namespace NzbDrone.Core.Tv
{
private readonly IEpisodeRepository _episodeRepository;
+ private readonly IQualityProfileRepository _qualityProfileRepository;
private readonly IConfigService _configService;
private readonly Logger _logger;
- public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger)
+ public EpisodeService(IEpisodeRepository episodeRepository, IQualityProfileRepository qualityProfileRepository, IConfigService configService, Logger logger)
{
_episodeRepository = episodeRepository;
+ _qualityProfileRepository = qualityProfileRepository;
_configService = configService;
_logger = logger;
}
@@ -88,7 +91,7 @@ namespace NzbDrone.Core.Tv
{
return _episodeRepository.GetEpisodes(seriesId, seasonNumber);
}
-
+
public Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle)
{
// TODO: can replace this search mechanism with something smarter/faster/better
diff --git a/src/UI/.idea/runConfigurations/Debug___Chrome.xml b/src/UI/.idea/runConfigurations/Debug___Chrome.xml
index 82eb4863d..47bd06dc9 100644
--- a/src/UI/.idea/runConfigurations/Debug___Chrome.xml
+++ b/src/UI/.idea/runConfigurations/Debug___Chrome.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/src/UI/.idea/runConfigurations/Debug___Firefox.xml b/src/UI/.idea/runConfigurations/Debug___Firefox.xml
index 2e020afbc..d9e99acc3 100644
--- a/src/UI/.idea/runConfigurations/Debug___Firefox.xml
+++ b/src/UI/.idea/runConfigurations/Debug___Firefox.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js
index d8ab15259..c48fffcf1 100644
--- a/src/UI/Cells/EpisodeStatusCell.js
+++ b/src/UI/Cells/EpisodeStatusCell.js
@@ -3,11 +3,12 @@
define(
[
'reqres',
+ 'backbone',
'Cells/NzbDroneCell',
'History/Queue/QueueCollection',
'moment',
'Shared/FormatHelpers'
- ], function (reqres, NzbDroneCell, QueueCollection, Moment, FormatHelpers) {
+ ], function (reqres, Backbone, NzbDroneCell, QueueCollection, Moment, FormatHelpers) {
return NzbDroneCell.extend({
className: 'episode-status-cell',
@@ -31,8 +32,16 @@ define(
var hasAired = Moment(this.model.get('airDateUtc')).isBefore(Moment());
var hasFile = this.model.get('hasFile');
- if (hasFile && reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
- var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId'));
+ if (hasFile) {
+ var episodeFile;
+
+ if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
+ episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId'));
+ }
+
+ else {
+ episodeFile = new Backbone.Model(this.model.get('episodeFile'));
+ }
this.listenTo(episodeFile, 'change', this._refresh);
diff --git a/src/UI/Controller.js b/src/UI/Controller.js
index db0b13e2b..19d7c3760 100644
--- a/src/UI/Controller.js
+++ b/src/UI/Controller.js
@@ -7,7 +7,7 @@ define(
'History/HistoryLayout',
'Settings/SettingsLayout',
'AddSeries/AddSeriesLayout',
- 'Missing/MissingLayout',
+ 'Wanted/WantedLayout',
'Calendar/CalendarLayout',
'Release/ReleaseLayout',
'System/SystemLayout',
@@ -20,7 +20,7 @@ define(
HistoryLayout,
SettingsLayout,
AddSeriesLayout,
- MissingLayout,
+ WantedLayout,
CalendarLayout,
ReleaseLayout,
SystemLayout,
@@ -44,10 +44,10 @@ define(
this.showMainRegion(new SettingsLayout({ action: action }));
},
- missing: function () {
- this.setTitle('Missing');
+ wanted: function (action) {
+ this.setTitle('Wanted');
- this.showMainRegion(new MissingLayout());
+ this.showMainRegion(new WantedLayout({ action: action }));
},
history: function (action) {
diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js
index 2a0e17991..469059cfc 100644
--- a/src/UI/Mixins/AsFilteredCollection.js
+++ b/src/UI/Mixins/AsFilteredCollection.js
@@ -24,7 +24,7 @@ define(
};
this.prototype.setFilterMode = function(mode, options) {
- this.setFilter(this.filterModes[mode], options);
+ return this.setFilter(this.filterModes[mode], options);
};
var originalMakeFullCollection = this.prototype._makeFullCollection;
diff --git a/src/UI/Navbar/NavbarTemplate.html b/src/UI/Navbar/NavbarTemplate.html
index 19284def2..fdc1128b7 100644
--- a/src/UI/Navbar/NavbarTemplate.html
+++ b/src/UI/Navbar/NavbarTemplate.html
@@ -29,10 +29,10 @@
-
+
- Missing
+ Wanted
diff --git a/src/UI/Router.js b/src/UI/Router.js
index f2927787a..6c268a7ef 100644
--- a/src/UI/Router.js
+++ b/src/UI/Router.js
@@ -14,7 +14,8 @@ define(
'calendar' : 'calendar',
'settings' : 'settings',
'settings/:action(/:query)' : 'settings',
- 'missing' : 'missing',
+ 'wanted' : 'wanted',
+ 'wanted/:action' : 'wanted',
'history' : 'history',
'history/:action' : 'history',
'rss' : 'rss',
diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js
index 240cd1445..c6b66abc8 100644
--- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js
+++ b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js
@@ -16,13 +16,18 @@ define(
initialize: function (options) {
this.menu = options.menu;
- if (this.menu.storeState) {
- this.setActive();
- }
+ this.setActive();
},
setActive: function () {
- var storedKey = Config.getValue(this.menu.menuKey, this.menu.defaultAction);
+ var storedKey = this.menu.defaultAction;
+
+ if (this.menu.storeState) {
+ storedKey = Config.getValue(this.menu.menuKey, storedKey);
+ }
+
+ if (!storedKey)
+ return;
this.collection.each(function (model) {
if (model.get('key').toLocaleLowerCase() === storedKey.toLowerCase()) {
diff --git a/src/UI/Missing/ControlsColumnTemplate.html b/src/UI/Wanted/ControlsColumnTemplate.html
similarity index 100%
rename from src/UI/Missing/ControlsColumnTemplate.html
rename to src/UI/Wanted/ControlsColumnTemplate.html
diff --git a/src/UI/Missing/MissingCollection.js b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js
similarity index 60%
rename from src/UI/Missing/MissingCollection.js
rename to src/UI/Wanted/Cutoff/CutoffUnmetCollection.js
index d58b6d133..a42c12dba 100644
--- a/src/UI/Missing/MissingCollection.js
+++ b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js
@@ -1,19 +1,21 @@
'use strict';
define(
[
+ 'underscore',
'Series/EpisodeModel',
'backbone.pageable',
+ 'Mixins/AsFilteredCollection',
'Mixins/AsPersistedStateCollection'
- ], function (EpisodeModel, PagableCollection, AsPersistedStateCollection) {
+ ], function (_, EpisodeModel, PagableCollection, AsFilteredCollection, AsPersistedStateCollection) {
var collection = PagableCollection.extend({
- url : window.NzbDrone.ApiRoot + '/missing',
+ url : window.NzbDrone.ApiRoot + '/wanted/cutoff',
model: EpisodeModel,
- tableName: 'missing',
+ tableName: 'wanted.cutoff',
state: {
- pageSize: 15,
- sortKey : 'airDateUtc',
- order : 1
+ pageSize : 15,
+ sortKey : 'airDateUtc',
+ order : 1
},
queryParams: {
@@ -27,6 +29,12 @@ define(
'1' : 'desc'
}
},
+
+ // Filter Modes
+ filterModes: {
+ 'monitored' : ['monitored', 'true'],
+ 'unmonitored' : ['monitored', 'false'],
+ },
parseState: function (resp) {
return {totalRecords: resp.totalRecords};
@@ -41,5 +49,6 @@ define(
}
});
+ collection = AsFilteredCollection.call(collection);
return AsPersistedStateCollection.call(collection);
});
diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js
new file mode 100644
index 000000000..aeca14fbb
--- /dev/null
+++ b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js
@@ -0,0 +1,206 @@
+'use strict';
+define(
+ [
+ 'underscore',
+ 'marionette',
+ 'backgrid',
+ 'Wanted/Cutoff/CutoffUnmetCollection',
+ 'Cells/SeriesTitleCell',
+ 'Cells/EpisodeNumberCell',
+ 'Cells/EpisodeTitleCell',
+ 'Cells/RelativeDateCell',
+ 'Cells/EpisodeStatusCell',
+ 'Shared/Grid/Pager',
+ 'Shared/Toolbar/ToolbarLayout',
+ 'Shared/LoadingView',
+ 'Shared/Messenger',
+ 'Commands/CommandController',
+ 'backgrid.selectall'
+ ], function (_,
+ Marionette,
+ Backgrid,
+ CutoffUnmetCollection,
+ SeriesTitleCell,
+ EpisodeNumberCell,
+ EpisodeTitleCell,
+ RelativeDateCell,
+ EpisodeStatusCell,
+ GridPager,
+ ToolbarLayout,
+ LoadingView,
+ Messenger,
+ CommandController) {
+ return Marionette.Layout.extend({
+ template: 'Wanted/Cutoff/CutoffUnmetLayoutTemplate',
+
+ regions: {
+ missing: '#x-missing',
+ toolbar: '#x-toolbar',
+ pager : '#x-pager'
+ },
+
+ ui: {
+ searchSelectedButton: '.btn i.icon-search'
+ },
+
+ columns:
+ [
+ {
+ name : '',
+ cell : 'select-row',
+ headerCell: 'select-all',
+ sortable : false
+ },
+ {
+ name : 'series',
+ label : 'Series Title',
+ sortable : false,
+ cell : SeriesTitleCell
+ },
+ {
+ name : 'this',
+ label : 'Episode',
+ sortable : false,
+ cell : EpisodeNumberCell
+ },
+ {
+ name : 'this',
+ label : 'Episode Title',
+ sortable : false,
+ cell : EpisodeTitleCell,
+ },
+ {
+ name : 'airDateUtc',
+ label : 'Air Date',
+ cell : RelativeDateCell
+ },
+ {
+ name : 'status',
+ label : 'Status',
+ cell : EpisodeStatusCell,
+ sortable: false
+ }
+ ],
+
+ initialize: function () {
+ this.collection = new CutoffUnmetCollection();
+
+ this.listenTo(this.collection, 'sync', this._showTable);
+ },
+
+ onShow: function () {
+ this.missing.show(new LoadingView());
+ this._showToolbar();
+ this.collection.fetch();
+ },
+
+ _showTable: function () {
+ this.missingGrid = new Backgrid.Grid({
+ columns : this.columns,
+ collection: this.collection,
+ className : 'table table-hover'
+ });
+
+ this.missing.show(this.missingGrid);
+
+ this.pager.show(new GridPager({
+ columns : this.columns,
+ collection: this.collection
+ }));
+ },
+
+ _showToolbar: function () {
+ var leftSideButtons = {
+ type : 'default',
+ storeState: false,
+ items :
+ [
+ {
+ title: 'Search Selected',
+ icon : 'icon-search',
+ callback: this._searchSelected,
+ ownerContext: this
+ },
+ {
+ title: 'Season Pass',
+ icon : 'icon-bookmark',
+ route: 'seasonpass'
+ }
+ ]
+ };
+
+ var filterOptions = {
+ type : 'radio',
+ storeState : false,
+ menuKey : 'wanted.filterMode',
+ defaultAction : 'monitored',
+ items :
+ [
+ {
+ key : 'monitored',
+ title : '',
+ tooltip : 'Monitored Only',
+ icon : 'icon-nd-monitored',
+ callback : this._setFilter
+ },
+ {
+ key : 'unmonitored',
+ title : '',
+ tooltip : 'Unmonitored Only',
+ icon : 'icon-nd-unmonitored',
+ callback : this._setFilter
+ },
+ ]
+ };
+
+ this.toolbar.show(new ToolbarLayout({
+ left :
+ [
+ leftSideButtons
+ ],
+ right :
+ [
+ filterOptions
+ ],
+ context: this
+ }));
+
+ CommandController.bindToCommand({
+ element: this.$('.x-toolbar-left-1 .btn i.icon-search'),
+ command: {
+ name: 'episodeSearch'
+ }
+ });
+ },
+
+ _setFilter: function(buttonContext) {
+ var mode = buttonContext.model.get('key');
+
+ this.collection.state.currentPage = 1;
+ var promise = this.collection.setFilterMode(mode);
+
+ if (buttonContext)
+ buttonContext.ui.icon.spinForPromise(promise);
+ },
+
+ _searchSelected: function () {
+ var selected = this.missingGrid.getSelectedModels();
+
+ if (selected.length === 0) {
+ Messenger.show({
+ type: 'error',
+ message: 'No episodes selected'
+ });
+
+ return;
+ }
+
+ var ids = _.pluck(selected, 'id');
+
+ CommandController.Execute('episodeSearch', {
+ name : 'episodeSearch',
+ episodeIds: ids
+ });
+ }
+ });
+ });
diff --git a/src/UI/Missing/MissingLayoutTemplate.html b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html
similarity index 100%
rename from src/UI/Missing/MissingLayoutTemplate.html
rename to src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html
diff --git a/src/UI/Wanted/Missing/MissingCollection.js b/src/UI/Wanted/Missing/MissingCollection.js
new file mode 100644
index 000000000..61564359f
--- /dev/null
+++ b/src/UI/Wanted/Missing/MissingCollection.js
@@ -0,0 +1,54 @@
+'use strict';
+define(
+ [
+ 'underscore',
+ 'Series/EpisodeModel',
+ 'backbone.pageable',
+ 'Mixins/AsFilteredCollection',
+ 'Mixins/AsPersistedStateCollection'
+ ], function (_, EpisodeModel, PagableCollection, AsFilteredCollection, AsPersistedStateCollection) {
+ var collection = PagableCollection.extend({
+ url : window.NzbDrone.ApiRoot + '/wanted/missing',
+ model: EpisodeModel,
+ tableName: 'wanted.missing',
+
+ state: {
+ pageSize : 15,
+ sortKey : 'airDateUtc',
+ order : 1
+ },
+
+ queryParams: {
+ totalPages : null,
+ totalRecords: null,
+ pageSize : 'pageSize',
+ sortKey : 'sortKey',
+ order : 'sortDir',
+ directions : {
+ '-1': 'asc',
+ '1' : 'desc'
+ }
+ },
+
+ // Filter Modes
+ filterModes: {
+ 'monitored' : ['monitored', 'true'],
+ 'unmonitored' : ['monitored', 'false'],
+ },
+
+ parseState: function (resp) {
+ return {totalRecords: resp.totalRecords};
+ },
+
+ parseRecords: function (resp) {
+ if (resp) {
+ return resp.records;
+ }
+
+ return resp;
+ }
+ });
+
+ collection = AsFilteredCollection.call(collection);
+ return AsPersistedStateCollection.call(collection);
+ });
diff --git a/src/UI/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js
similarity index 67%
rename from src/UI/Missing/MissingLayout.js
rename to src/UI/Wanted/Missing/MissingLayout.js
index 7240277f5..bd8f20843 100644
--- a/src/UI/Missing/MissingLayout.js
+++ b/src/UI/Wanted/Missing/MissingLayout.js
@@ -4,11 +4,12 @@ define(
'underscore',
'marionette',
'backgrid',
- 'Missing/MissingCollection',
+ 'Wanted/Missing/MissingCollection',
'Cells/SeriesTitleCell',
'Cells/EpisodeNumberCell',
'Cells/EpisodeTitleCell',
'Cells/RelativeDateCell',
+ 'Cells/EpisodeStatusCell',
'Shared/Grid/Pager',
'Shared/Toolbar/ToolbarLayout',
'Shared/LoadingView',
@@ -23,13 +24,14 @@ define(
EpisodeNumberCell,
EpisodeTitleCell,
RelativeDateCell,
+ EpisodeStatusCell,
GridPager,
ToolbarLayout,
LoadingView,
Messenger,
CommandController) {
return Marionette.Layout.extend({
- template: 'Missing/MissingLayoutTemplate',
+ template: 'Wanted/Missing/MissingLayoutTemplate',
regions: {
missing: '#x-missing',
@@ -52,25 +54,31 @@ define(
{
name : 'series',
label : 'Series Title',
- sortable: false,
+ sortable : false,
cell : SeriesTitleCell
},
{
name : 'this',
label : 'Episode',
- sortable: false,
+ sortable : false,
cell : EpisodeNumberCell
},
{
name : 'this',
label : 'Episode Title',
- sortable: false,
- cell : EpisodeTitleCell
+ sortable : false,
+ cell : EpisodeTitleCell,
},
{
- name : 'airDateUtc',
- label: 'Air Date',
- cell : RelativeDateCell
+ name : 'airDateUtc',
+ label : 'Air Date',
+ cell : RelativeDateCell
+ },
+ {
+ name : 'status',
+ label : 'Status',
+ cell : EpisodeStatusCell,
+ sortable: false
}
],
@@ -82,8 +90,8 @@ define(
onShow: function () {
this.missing.show(new LoadingView());
- this.collection.fetch();
this._showToolbar();
+ this.collection.fetch();
},
_showTable: function () {
@@ -120,12 +128,40 @@ define(
}
]
};
+
+ var filterOptions = {
+ type : 'radio',
+ storeState : false,
+ menuKey : 'wanted.filterMode',
+ defaultAction : 'monitored',
+ items :
+ [
+ {
+ key : 'monitored',
+ title : '',
+ tooltip : 'Monitored Only',
+ icon : 'icon-nd-monitored',
+ callback : this._setFilter
+ },
+ {
+ key : 'unmonitored',
+ title : '',
+ tooltip : 'Unmonitored Only',
+ icon : 'icon-nd-unmonitored',
+ callback : this._setFilter
+ },
+ ]
+ };
this.toolbar.show(new ToolbarLayout({
left :
[
leftSideButtons
],
+ right :
+ [
+ filterOptions
+ ],
context: this
}));
@@ -136,6 +172,16 @@ define(
}
});
},
+
+ _setFilter: function(buttonContext) {
+ var mode = buttonContext.model.get('key');
+
+ this.collection.state.currentPage = 1;
+ var promise = this.collection.setFilterMode(mode);
+
+ if (buttonContext)
+ buttonContext.ui.icon.spinForPromise(promise);
+ },
_searchSelected: function () {
var selected = this.missingGrid.getSelectedModels();
diff --git a/src/UI/Wanted/Missing/MissingLayoutTemplate.html b/src/UI/Wanted/Missing/MissingLayoutTemplate.html
new file mode 100644
index 000000000..958d5aa5e
--- /dev/null
+++ b/src/UI/Wanted/Missing/MissingLayoutTemplate.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/UI/Wanted/WantedLayout.js b/src/UI/Wanted/WantedLayout.js
new file mode 100644
index 000000000..bca0b3435
--- /dev/null
+++ b/src/UI/Wanted/WantedLayout.js
@@ -0,0 +1,69 @@
+'use strict';
+define(
+ [
+ 'marionette',
+ 'backbone',
+ 'backgrid',
+ 'Wanted/Missing/MissingLayout',
+ 'Wanted/Cutoff/CutoffUnmetLayout'
+ ], function (Marionette, Backbone, Backgrid, MissingLayout, CutoffUnmetLayout) {
+ return Marionette.Layout.extend({
+ template: 'Wanted/WantedLayoutTemplate',
+
+ regions: {
+ content : '#content'
+ //missing : '#missing',
+ //cutoff : '#cutoff'
+ },
+
+ ui: {
+ missingTab : '.x-missing-tab',
+ cutoffTab : '.x-cutoff-tab'
+ },
+
+ events: {
+ 'click .x-missing-tab' : '_showMissing',
+ 'click .x-cutoff-tab' : '_showCutoffUnmet'
+ },
+
+ initialize: function (options) {
+ if (options.action) {
+ this.action = options.action.toLowerCase();
+ }
+ },
+
+ onShow: function () {
+ switch (this.action) {
+ case 'cutoff':
+ this._showCutoffUnmet();
+ break;
+ default:
+ this._showMissing();
+ }
+ },
+
+ _navigate: function (route) {
+ Backbone.history.navigate(route);
+ },
+
+ _showMissing: function (e) {
+ if (e) {
+ e.preventDefault();
+ }
+
+ this.content.show(new MissingLayout());
+ this.ui.missingTab.tab('show');
+ this._navigate('/wanted/missing');
+ },
+
+ _showCutoffUnmet: function (e) {
+ if (e) {
+ e.preventDefault();
+ }
+
+ this.content.show(new CutoffUnmetLayout());
+ this.ui.cutoffTab.tab('show');
+ this._navigate('/wanted/cutoff');
+ }
+ });
+ });
diff --git a/src/UI/Wanted/WantedLayoutTemplate.html b/src/UI/Wanted/WantedLayoutTemplate.html
new file mode 100644
index 000000000..6665fb3d1
--- /dev/null
+++ b/src/UI/Wanted/WantedLayoutTemplate.html
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file