From 6dd85a5af931509b950ac418781fcb1521d2b08d Mon Sep 17 00:00:00 2001 From: jbstark <33739840+jbstark@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:57:22 -0700 Subject: [PATCH] New: 'Seasons Monitored Status' Custom Filter to replace 'Has Unmonitored Season' Closes #6896 --- .../Filter/Builder/FilterBuilderRow.js | 4 + ...onsMonitoredStatusFilterBuilderRowValue.js | 35 ++ .../Helpers/Props/filterBuilderValueTypes.js | 1 + frontend/src/Store/Actions/seriesActions.js | 37 +- ...210_add_monitored_seasons_filterFixture.cs | 372 ++++++++++++++++++ .../210_add_monitored_seasons_filter.cs | 76 ++++ src/NzbDrone.Core/Localization/Core/en.json | 4 + 7 files changed, 517 insertions(+), 12 deletions(-) create mode 100644 frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 46a38a258..e12f8c40f 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -13,6 +13,7 @@ import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import SeasonsMonitoredStatusFilterBuilderRowValue from './SeasonsMonitoredStatusFilterBuilderRowValue'; import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue'; @@ -79,6 +80,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.QUALITY_PROFILE: return QualityProfileFilterBuilderRowValueConnector; + case filterBuilderValueTypes.SEASONS_MONITORED_STATUS: + return SeasonsMonitoredStatusFilterBuilderRowValue; + case filterBuilderValueTypes.SERIES: return SeriesFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js new file mode 100644 index 000000000..b84260e3c --- /dev/null +++ b/frontend/src/Components/Filter/Builder/SeasonsMonitoredStatusFilterBuilderRowValue.js @@ -0,0 +1,35 @@ +import React from 'react'; +import translate from 'Utilities/String/translate'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +const seasonsMonitoredStatusList = [ + { + id: 'all', + get name() { + return translate('SeasonsMonitoredAll'); + } + }, + { + id: 'partial', + get name() { + return translate('SeasonsMonitoredPartial'); + } + }, + { + id: 'none', + get name() { + return translate('SeasonsMonitoredNone'); + } + } +]; + +function SeasonsMonitoredStatusFilterBuilderRowValue(props) { + return ( + + ); +} + +export default SeasonsMonitoredStatusFilterBuilderRowValue; diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 1f4227779..d9a5d58c7 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -8,6 +8,7 @@ export const LANGUAGE = 'language'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; +export const SEASONS_MONITORED_STATUS = 'seasonsMonitoredStatus'; export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; export const SERIES_TYPES = 'seriesType'; diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js index 3aa9b7237..c18104065 100644 --- a/frontend/src/Store/Actions/seriesActions.js +++ b/frontend/src/Store/Actions/seriesActions.js @@ -202,20 +202,33 @@ export const filterPredicates = { return predicate(hasMissingSeason, filterValue); }, - hasUnmonitoredSeason: function(item, filterValue, type) { + seasonsMonitoredStatus: function(item, filterValue, type) { const predicate = filterTypePredicates[type]; const { seasons = [] } = item; - const hasUnmonitoredSeason = seasons.some((season) => { - const { - seasonNumber, - monitored - } = season; + const { monitoredCount, unmonitoredCount } = seasons.reduce((acc, { seasonNumber, monitored }) => { + if (seasonNumber <= 0) { + return acc; + } - return seasonNumber > 0 && !monitored; - }); + if (monitored) { + acc.monitoredCount++; + } else { + acc.unmonitoredCount++; + } - return predicate(hasUnmonitoredSeason, filterValue); + return acc; + }, { monitoredCount: 0, unmonitoredCount: 0 }); + + let seasonsMonitoredStatus = 'partial'; + + if (monitoredCount === 0) { + seasonsMonitoredStatus = 'none'; + } else if (unmonitoredCount === 0) { + seasonsMonitoredStatus = 'all'; + } + + return predicate(seasonsMonitoredStatus, filterValue); } }; @@ -383,10 +396,10 @@ export const filterBuilderProps = [ valueType: filterBuilderValueTypes.BOOL }, { - name: 'hasUnmonitoredSeason', - label: () => translate('HasUnmonitoredSeason'), + name: 'seasonsMonitoredStatus', + label: () => translate('SeasonsMonitoredStatus'), type: filterBuilderTypes.EXACT, - valueType: filterBuilderValueTypes.BOOL + valueType: filterBuilderValueTypes.SEASONS_MONITORED_STATUS }, { name: 'year', diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs new file mode 100644 index 000000000..94bd7fb68 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/210_add_monitored_seasons_filterFixture.cs @@ -0,0 +1,372 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class add_monitored_seasons_filterFixture : MigrationTest + { + [Test] + public void equal_both_becomes_equal_every_option() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { true, false }, + type = "equal" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all", "partial", "none" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void notEqual_both_becomes_notEqual_every_option() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { true, false }, + type = "notEqual" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all", "partial", "none" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void equal_true_becomes_notEqual_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { true }, + type = "equal" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void notEqual_true_becomes_equal_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { true }, + type = "notEqual" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void equal_false_becomes_equal_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { false }, + type = "equal" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all" }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void notEqual_false_becomes_notEqual_all() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { false }, + type = "notEqual" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { "all" }); + filters[0].type.Should().Be("notEqual"); + } + + [Test] + public void missing_hasUnmonitored_unchanged() + { + var filter = new FilterSettings210 + { + key = "monitored", + value = new List { false }, + type = "equal" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("monitored"); + filters[0].value.Should().BeEquivalentTo(new List { false }); + filters[0].type.Should().Be("equal"); + } + + [Test] + public void has_hasUnmonitored_not_in_first_entry() + { + var filter1 = new FilterSettings210 + { + key = "monitored", + value = new List { false }, + type = "equal" + }; + var filter2 = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { true }, + type = "equal" + }; + + var filtersJson = new List { filter1, filter2 }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("monitored"); + filters[0].value.Should().BeEquivalentTo(new List { false }); + filters[0].type.Should().Be("equal"); + filters[1].key.Should().Be("seasonsMonitoredStatus"); + filters[1].value.Should().BeEquivalentTo(new List { "all" }); + filters[1].type.Should().Be("notEqual"); + } + + [Test] + public void has_umonitored_is_empty() + { + var filter = new FilterSettings210 + { + key = "hasUnmonitoredSeason", + value = new List { }, + type = "equal" + }; + + var filtersJson = new List { filter }; + + var filtersString = filtersJson.ToJson(); + + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("CustomFilters").Row(new + { + Id = 1, + Type = "series", + Label = "Is Both", + Filters = filtersString + }); + }); + + var items = db.Query("SELECT * FROM \"CustomFilters\""); + + items.Should().HaveCount(1); + items.First().Type.Should().Be("series"); + items.First().Label.Should().Be("Is Both"); + + var filters = JsonConvert.DeserializeObject>(items.First().Filters); + filters[0].key.Should().Be("seasonsMonitoredStatus"); + filters[0].value.Should().BeEquivalentTo(new List { }); + filters[0].type.Should().Be("equal"); + } + } + + public class FilterDefinition210 + { + public int Id { get; set; } + public string Type { get; set; } + public string Label { get; set; } + public string Filters { get; set; } + } + + public class FilterSettings210 + { + public string key { get; set; } + public List value { get; set; } + public string type { get; set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs b/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs new file mode 100644 index 000000000..ad07f3a93 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/210_add_monitored_seasons_filter.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Data; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(210)] + public class add_monitored_seasons_filter : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeHasUnmonitoredSeason); + } + + private void ChangeHasUnmonitoredSeason(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + using (var getUnmonitoredSeasonFilter = conn.CreateCommand()) + { + getUnmonitoredSeasonFilter.Transaction = tran; + getUnmonitoredSeasonFilter.CommandText = "SELECT \"Id\", \"Filters\" FROM \"CustomFilters\" WHERE \"Type\" = 'series'"; + + using (var reader = getUnmonitoredSeasonFilter.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var filters = JArray.Parse(reader.GetString(1)); + + foreach (var filter in filters) + { + if (filter["key"]?.ToString() == "hasUnmonitoredSeason") + { + var value = filter["value"].ToString(); + var type = filter["type"].ToString(); + + filter["key"] = "seasonsMonitoredStatus"; + + if (value.Contains("true") && value.Contains("false")) + { + filter["value"] = new JArray("all", "partial", "none"); + } + else if (value.Contains("true")) + { + filter["type"] = type == "equal" ? "notEqual" : "equal"; + filter["value"] = new JArray("all"); + } + else if (value.Contains("false")) + { + filter["value"] = new JArray("all"); + } + else + { + filter["value"] = new JArray(); + } + } + } + + updated.Add(new + { + Filters = filters.ToJson(), + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"CustomFilters\" SET \"Filters\" = @Filters WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index cafc59138..5da122a3e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1784,6 +1784,10 @@ "SeasonPremiere": "Season Premiere", "SeasonPremieresOnly": "Season Premieres Only", "Seasons": "Seasons", + "SeasonsMonitoredAll": "All", + "SeasonsMonitoredPartial": "Partial", + "SeasonsMonitoredNone": "None", + "SeasonsMonitoredStatus": "Seasons Monitored", "SecretToken": "Secret Token", "Security": "Security", "Seeders": "Seeders",