Skip to content

Commit df57e63

Browse files
authored
MMI-3365 Fix duplicate series (#2520)
1 parent 7f5f171 commit df57e63

File tree

5 files changed

+8006
-27
lines changed

5 files changed

+8006
-27
lines changed

app/editor/src/features/content/form/ContentForm.tsx

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -121,27 +121,55 @@ const ContentForm: React.FC<IContentFormProps> = ({
121121
const [seriesOptions, setSeriesOptions] = React.useState<IOptionItem[]>([]);
122122
const [seriesOtherOptions, setSeriesOtherOptions] = React.useState<IOptionItem[]>([]);
123123
const [seriesOtherCreated, setSeriesOtherCreated] = React.useState<string>('');
124+
const [contentPrepTime, setContentPrepTime] = React.useState<string>('');
124125

125126
const contentId = parseInt(id ?? '0');
126127
const urlParams = new URLSearchParams(window.location.search);
127128
const showPostedOn = ['', 'true'].includes(urlParams.get('showPostedOn') ?? 'false');
128129

130+
// Function to filter series based on source and other criteria
131+
const filterSeries = React.useCallback(
132+
(sourceId: number | '', seriesId: number | '', isOther: boolean = false) => {
133+
return series.filter(
134+
(f) =>
135+
f.isOther === isOther &&
136+
((!!seriesId && f.id === seriesId) || // Include current selected
137+
(f.isEnabled && (!sourceId || !f.sourceId || f.sourceId === sourceId))),
138+
);
139+
},
140+
[series],
141+
);
142+
143+
// Function to create series options based on source and other criteria
144+
const filterSeriesOptions = React.useCallback(
145+
(sourceId: number | '', seriesId: number | '') => {
146+
const options = filterSeries(sourceId, seriesId, false).map(
147+
(m) => new OptionItem<number | ''>(m.name, m.id, !m.isEnabled),
148+
);
149+
setSeriesOptions(options);
150+
},
151+
[filterSeries],
152+
);
153+
154+
// Function to create other series options based on source and other criteria
155+
const filterSeriesOtherOptions = React.useCallback(
156+
(sourceId: number | '', seriesId: number | '') => {
157+
let options = filterSeries(sourceId, seriesId, true).map(
158+
(m) => new OptionItem<number | ''>(m.name, m.id, !m.isEnabled),
159+
);
160+
if (seriesOtherCreated) options = options.concat(new OptionItem(seriesOtherCreated, ''));
161+
setSeriesOtherOptions(options);
162+
},
163+
[filterSeries, seriesOtherCreated],
164+
);
165+
129166
React.useEffect(() => {
130-
setSeriesOptions(
131-
series.filter((f) => !f.isOther).map((m: any) => new OptionItem(m.name, m.id, !m.isEnabled)),
132-
);
133-
}, [series]);
167+
filterSeriesOptions(form.sourceId, form.seriesId);
168+
}, [filterSeriesOptions, form.seriesId, form.sourceId]);
134169

135170
React.useEffect(() => {
136-
// create a list of "Other Series" options
137-
// and concat the created value if necessary
138-
let filteredSeriesOptions = series
139-
.filter((f) => f.isOther)
140-
.map((m: any) => new OptionItem(m.name, m.id, !m.isEnabled));
141-
if (seriesOtherCreated)
142-
filteredSeriesOptions = filteredSeriesOptions.concat(new OptionItem(seriesOtherCreated, ''));
143-
setSeriesOtherOptions(filteredSeriesOptions);
144-
}, [series, seriesOtherCreated]);
171+
filterSeriesOtherOptions(form.sourceId, form.seriesId);
172+
}, [filterSeriesOtherOptions, form.sourceId, form.seriesId]);
145173

146174
React.useEffect(() => {
147175
if (contentId > 0 && contentId !== form.id) {
@@ -152,7 +180,11 @@ const ContentForm: React.FC<IContentFormProps> = ({
152180
React.useEffect(() => {
153181
setAvStream();
154182
}, [setAvStream]);
155-
const [contentPrepTime, setContentPrepTime] = React.useState<string>('');
183+
184+
const onPrepTimeChanged = React.useCallback((value: string) => {
185+
setContentPrepTime(value);
186+
}, []);
187+
156188
return (
157189
<styled.ContentForm className="content-form fvh" ref={refForm}>
158190
<FormPage className="fvh">
@@ -171,10 +203,6 @@ const ContentForm: React.FC<IContentFormProps> = ({
171203
const source = sources.find((s) => s.id === props.values.sourceId);
172204
const program = series.find((s) => s.id === props.values.seriesId);
173205

174-
const onPrepTimeChanged = (value: string) => {
175-
setContentPrepTime(value);
176-
};
177-
178206
return (
179207
<Col className="content-col fvh">
180208
<ContentFormToolBar
@@ -241,6 +269,14 @@ const ContentForm: React.FC<IContentFormProps> = ({
241269
if (!!source?.mediaTypeId)
242270
props.setFieldValue('mediaTypeId', source.mediaTypeId);
243271
}
272+
filterSeriesOptions(
273+
newValue?.value ?? '',
274+
props.values.seriesId,
275+
);
276+
filterSeriesOtherOptions(
277+
newValue?.value ?? '',
278+
props.values.seriesId,
279+
);
244280
}}
245281
options={filterEnabledOptions(
246282
sourceOptions,
@@ -628,10 +664,7 @@ const ContentForm: React.FC<IContentFormProps> = ({
628664
(s: any) => s.value === props.values.seriesId,
629665
) ?? ''
630666
}
631-
options={filterEnabledOptions(
632-
seriesOptions,
633-
props.values.seriesId,
634-
)}
667+
options={seriesOptions}
635668
isDisabled={!!props.values.otherSeries}
636669
onChange={(e) => {
637670
props.setFieldValue('otherSeries', '');
@@ -646,8 +679,13 @@ const ContentForm: React.FC<IContentFormProps> = ({
646679
options={seriesOtherOptions}
647680
onChange={(e: any) => {
648681
let foundSeries: ISeriesModel | undefined;
649-
foundSeries = series.find((c) => c.id === e.value);
650-
if (!!foundSeries && foundSeries.isOther) {
682+
foundSeries = series.find((c) => c.id === e?.value);
683+
if (e?.value == null || e?.value === '') {
684+
// Clear selected value
685+
props.setFieldValue('otherSeries', '');
686+
props.setFieldValue('seriesId', '');
687+
setSeriesOtherCreated('');
688+
} else if (!!foundSeries && foundSeries.isOther) {
651689
// this is a known "Other Series"
652690
props.setFieldValue('otherSeries', '');
653691
props.setFieldValue('seriesId', foundSeries.id);
@@ -657,10 +695,11 @@ const ContentForm: React.FC<IContentFormProps> = ({
657695
onCreateOption={(inputValue: string) => {
658696
// this is a "created" option, but we need to check if
659697
// it's a duplicate of an existing option this is !isOther
698+
const value = inputValue.trim();
660699
let foundSeries: ISeriesModel | undefined = series.find(
661700
(s) =>
662701
s.name.toLocaleLowerCase() ===
663-
inputValue.toLocaleLowerCase(),
702+
value.toLocaleLowerCase(),
664703
);
665704
if (!!foundSeries) {
666705
// this is an existing series - not isOther
@@ -669,10 +708,10 @@ const ContentForm: React.FC<IContentFormProps> = ({
669708
setSeriesOtherCreated('');
670709
} else {
671710
// this is a new "other" series
672-
props.setFieldValue('otherSeries', inputValue);
711+
props.setFieldValue('otherSeries', value);
673712
props.setFieldValue('seriesId', undefined);
674713
// save the new created series/program name
675-
setSeriesOtherCreated(inputValue);
714+
setSeriesOtherCreated(value);
676715
}
677716
}}
678717
value={

libs/net/dal/Configuration/SeriesConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ public override void Configure(EntityTypeBuilder<Series> builder)
1313
builder.Property(m => m.AutoTranscribe).IsRequired();
1414
builder.Property(m => m.UseInTopics).IsRequired();
1515
builder.Property(m => m.IsCBRASource).IsRequired();
16+
builder.Property(m => m.IsOther).IsRequired();
1617

1718
builder.HasOne(m => m.Source).WithMany(m => m.Series).HasForeignKey(m => m.SourceId).OnDelete(DeleteBehavior.Cascade);
1819
builder.HasMany(m => m.MediaTypeSearchMappings).WithMany(m => m.SeriesSearchMappings).UsingEntity<SeriesMediaTypeSearchMapping>();
1920

21+
// This has been applied through a script to ensure unique value handle NULL correctly.
2022
// CREATE UNIQUE INDEX "IX_series_name" ON public."series"
2123
// ("name") WHERE "source_id" IS NULL;
2224

25+
// This has been applied through a script to ensure unique value handle NULL correctly.
2326
// CREATE UNIQUE INDEX "IX_source_id_name" ON public."series"
2427
// ("source_id", "name") WHERE "source_id" IS NOT NULL;
2528

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
DO $$
2+
BEGIN
3+
4+
-- Step 1: Update the boolean fields on the surviving records (oldest ID per duplicate group)
5+
WITH groups AS (
6+
SELECT
7+
TRIM(name) AS trimmed_name,
8+
source_id,
9+
MIN(id) AS survivor_id,
10+
BOOL_OR(auto_transcribe) AS new_auto_transcribe, -- OR: true if any is true
11+
BOOL_OR(use_in_topics) AS new_use_in_topics,
12+
BOOL_OR(is_cbra_source) AS new_is_cbra_source,
13+
BOOL_AND(is_other) AS new_is_other, -- AND: true only if all are true
14+
BOOL_OR(is_enabled) AS new_is_enabled -- OR: true if any is true
15+
FROM series
16+
GROUP BY TRIM(name), source_id
17+
HAVING COUNT(*) > 1
18+
)
19+
UPDATE series s
20+
SET
21+
auto_transcribe = g.new_auto_transcribe,
22+
use_in_topics = g.new_use_in_topics,
23+
is_cbra_source = g.new_is_cbra_source,
24+
is_other = g.new_is_other,
25+
is_enabled = g.new_is_enabled
26+
FROM groups g
27+
WHERE s.id = g.survivor_id;
28+
29+
-- Step 2: Create temp table for mappings
30+
CREATE TEMP TABLE mappings (old_id int, new_id int);
31+
32+
INSERT INTO mappings (old_id, new_id)
33+
WITH groups AS (
34+
SELECT
35+
TRIM(name) AS trimmed_name,
36+
source_id,
37+
MIN(id) AS survivor_id,
38+
ARRAY_AGG(id ORDER BY id) AS all_ids
39+
FROM series
40+
GROUP BY TRIM(name), source_id
41+
HAVING COUNT(*) > 1
42+
),
43+
all_duplicates AS (
44+
SELECT
45+
survivor_id,
46+
unnest(all_ids) AS id
47+
FROM groups
48+
)
49+
SELECT
50+
id,
51+
survivor_id
52+
FROM all_duplicates
53+
WHERE id != survivor_id;
54+
55+
-- Step 3: Update foreign keys in related tables
56+
UPDATE content c
57+
SET series_id = m.new_id
58+
FROM mappings m
59+
WHERE c.series_id = m.old_id;
60+
61+
UPDATE series_media_type_search_mapping sm
62+
SET series_id = m.new_id
63+
FROM mappings m
64+
WHERE sm.series_id = m.old_id;
65+
66+
UPDATE av_overview_section aos
67+
SET series_id = m.new_id
68+
FROM mappings m
69+
WHERE aos.series_id = m.old_id;
70+
71+
UPDATE av_overview_template_section aots
72+
SET series_id = m.new_id
73+
FROM mappings m
74+
WHERE aots.series_id = m.old_id;
75+
76+
UPDATE topic_score_rule tsr
77+
SET series_id = m.new_id
78+
FROM mappings m
79+
WHERE tsr.series_id = m.old_id;
80+
81+
-- Step 4: Delete the duplicate series records
82+
DELETE FROM series s
83+
USING mappings m
84+
WHERE s.id = m.old_id;
85+
86+
-- Clean up temp table
87+
DROP TABLE mappings;
88+
89+
END $$;

0 commit comments

Comments
 (0)