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