mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
rename Channel.user_hidden to Channel.hidden_from_output
The previous name suggested "hidden from a specific user account" rather than "hidden from downstream client output", which was the actual
behavior. The new name is unambiguous and matches the help_text language ("Exclude this channel from downstream client output").
Mechanical rename across the migration, model, serializer, signals, sync logic, output endpoints, frontend forms / tables / utils, tests, and CHANGELOG. 77 occurrences across 20 files.
No behavior change.
This commit is contained in:
parent
b9b3ded71c
commit
b2b108bfe5
20 changed files with 72 additions and 72 deletions
|
|
@ -31,9 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **Per-field channel overrides for auto-synced channels.** A new `ChannelOverride` table (one-to-one with `Channel`) holds user-specified values for `name`, `channel_number`, `channel_group`, `logo`, `tvg_id`, `tvc_guide_stationid`, `epg_data`, and `stream_profile`. Sync writes only to `Channel.*` columns; the override row takes precedence at read time via a new `with_effective_values()` queryset helper that coalesces both sources at the SQL layer. Every output surface that consumes channel data reads through this helper so overrides surface consistently: HDHR (`/hdhr/lineup.json` and per-profile variants, `/hdhr/discover.json`), M3U (`/output/m3u`, per-profile variants), EPG (`/output/epg`, per-profile variants), XC API (`get_live_streams`, `xmltv.php`), the channels list/detail endpoints, and the TV Guide summary endpoint (`GET /api/channels/channels/summary/`). Channel API responses now include both the raw `name`/`channel_number`/etc. fields AND new `effective_*` annotations plus the `override` object, so frontend consumers can show both "what the user picked" and "what the provider sent" simultaneously.
|
||||
- **Per-field reset-to-provider icon.** FK pickers (channel group, logo, EPG, stream profile) and scalar inputs (name, channel_number, tvg_id, tvc_guide_stationid) display a small reset icon ("Provider: <value>" subtext + Undo2 button) when the field has an active override on an auto-synced channel. Clicking it sets the form value back to the provider value; the override for that field is cleared on save while other overrides remain intact.
|
||||
- **Auto-created channel source attribution.** The channel edit form shows an "Auto-created from: <provider name> / <stream name>" label at the top of auto-synced channels so the user can see which M3U account and which stream produced the row.
|
||||
- **Hide-from-output flag (`user_hidden`).** A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows.
|
||||
- **Hide-from-output flag (`hidden_from_output`).** A user-toggleable boolean that excludes a channel from HDHR, M3U, EPG, and XC output without deleting it. Hidden channels are preserved across auto-sync refreshes and excluded from auto-cleanup. The channels table shows an EyeOff icon on hidden rows.
|
||||
- **Channels table visibility filter and inline indicators.** The table header has a new visibility selector ("Active Only" / "Hidden Only" / "Show All") plus a "Has Overrides" filter that narrows to channels with an active override row. Each row's name cell renders a yellow Pencil icon when overrides are active (tooltip lists the overridden field names) and an EyeOff icon when the channel is hidden.
|
||||
- **Bulk hide / unhide in the bulk edit modal.** Selecting any number of channels and toggling user_hidden in bulk edit applies the change in a single PATCH; auto-routes through override-aware bulk endpoint so it works equivalently for auto-synced and manual channels.
|
||||
- **Bulk hide / unhide in the bulk edit modal.** Selecting any number of channels and toggling hidden_from_output in bulk edit applies the change in a single PATCH; auto-routes through override-aware bulk endpoint so it works equivalently for auto-synced and manual channels.
|
||||
- **Auto-sync channel ranges per group.** `ChannelGroupM3UAccount` accepts `auto_sync_channel_start` (lower bound, inclusive) and `auto_sync_channel_end` (upper bound, inclusive; nullable for unbounded fill). Channels created by auto-sync are assigned numbers within the configured range; streams that do not fit are surfaced in the failure detail modal with a typed "RANGE_EXHAUSTED" reason.
|
||||
- **Per-group inline configuration in the M3U Live group filter.** Each group row now exposes a Sync toggle, a Numbering Mode selector (Provider / Next Available / Fixed), and Start/End channel-number inputs alongside the existing fields. The numbering modes control how each stream's channel number is chosen during sync: Provider tries the M3U-supplied number first then falls back to next-available; Next Available always picks the lowest free number from 1 upward; Fixed picks sequentially from the configured Start.
|
||||
- **Per-group "Configure" modal.** A gear-icon button on each group row opens a `GroupConfigureModal` containing the full set of advanced options. Fields surfaced in the modal: Channel Sort Order (provider order / name / tvg_id / updated_at, with reverse toggle), Compact Numbering toggle and "Re-pack now" button, Group Override (re-route this group's auto-created channels into a different `ChannelGroup` so multiple provider groups merge into one logical group), Name Regex Find/Replace pattern, Exclude Regex pattern, Force Dummy EPG, and a live regex preview pane.
|
||||
|
|
@ -102,7 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- **`Channel.channel_number` is now nullable.** Auto-sync may produce channels without an assigned number (for example, a sync run where the configured range was exhausted before this stream could be slotted, or a hidden channel in compact mode whose slot was released for visible channels). Existing channels are unchanged. HDHR / M3U / EPG / XC output endpoints filter out channels with `null` channel_number, so a number-less row is invisible to clients but visible in the channels page where the user can assign one. The 0037 auto-sync overhaul migration includes a reverse-direction backfill on the `channel_number` AlterField step so a rollback that re-imposes NOT NULL still succeeds even if NULLs are present.
|
||||
- **M3U account delete always cascades auto-created channels.** The delete dialog is a single confirmation showing the count of affected channels (fetched from the new auto-created-channels-count endpoint). Auto-created channels are removed regardless of `user_hidden` or override state: override rows cascade via the one-to-one FK; hidden status does not preserve the channel because there is no provider left to populate it on the next sync. Manual channels are unaffected; their non-provider streams remain intact, and only streams owned by the deleted account are removed. The destroy response now returns `{"deleted_channels": N}` (HTTP 200) instead of an empty 204 so the confirmation toast can show the actual count.
|
||||
- **M3U account delete always cascades auto-created channels.** The delete dialog is a single confirmation showing the count of affected channels (fetched from the new auto-created-channels-count endpoint). Auto-created channels are removed regardless of `hidden_from_output` or override state: override rows cascade via the one-to-one FK; hidden status does not preserve the channel because there is no provider left to populate it on the next sync. Manual channels are unaffected; their non-provider streams remain intact, and only streams owned by the deleted account are removed. The destroy response now returns `{"deleted_channels": N}` (HTTP 200) instead of an empty 204 so the confirmation toast can show the actual count.
|
||||
- **Bulk channel edit auto-routes auto-created channels through the override path.** When a bulk edit includes auto-synced channels, the changed fields are written to each channel's `ChannelOverride` row instead of the raw `Channel.*` columns, so the edit survives the next sync. Manual channels in the same selection write directly to `Channel.*`. A "Clear all overrides" affordance pre-clears overrides on the selection before applying new edits in the same submit (clear runs before the routing PATCH so a same-submit clear cannot wipe just-written overrides).
|
||||
- **Inline edits on the channels table route through the override row for auto-synced channels.** Editing a cell directly in the table (number, name, group, EPG, logo) now writes to `ChannelOverride.<field>` for auto-synced rows so the edit survives the next refresh; manual rows continue to write directly. The save mechanic and validation are unchanged from the user's perspective.
|
||||
- **Channel-number duplicates are explicitly allowed.** Sync's `used_numbers` seed still avoids assigning the same number to two newly-created auto channels in the same run, so accidental duplication during sync remains impossible. Manual or override-driven duplication is permitted; downstream client behavior on duplicates varies by client.
|
||||
|
|
|
|||
|
|
@ -859,9 +859,9 @@ class ChannelViewSet(viewsets.ModelViewSet):
|
|||
# the same hidden semantic as downstream clients.
|
||||
if self.action in ("list", "get_ids", "summary"):
|
||||
if visibility_filter == "hidden":
|
||||
q_filters &= Q(user_hidden=True)
|
||||
q_filters &= Q(hidden_from_output=True)
|
||||
elif visibility_filter != "all":
|
||||
q_filters &= Q(user_hidden=False)
|
||||
q_filters &= Q(hidden_from_output=False)
|
||||
|
||||
if self.request.user.user_level < 10:
|
||||
filters["user_level__lte"] = self.request.user.user_level
|
||||
|
|
@ -1024,14 +1024,14 @@ class ChannelViewSet(viewsets.ModelViewSet):
|
|||
unhide_transition_ids = [
|
||||
channel.id
|
||||
for channel, validated_data in validated_updates
|
||||
if validated_data.get("user_hidden") is False
|
||||
and channel.user_hidden is True
|
||||
if validated_data.get("hidden_from_output") is False
|
||||
and channel.hidden_from_output is True
|
||||
]
|
||||
hide_transition_candidates = [
|
||||
channel
|
||||
for channel, validated_data in validated_updates
|
||||
if validated_data.get("user_hidden") is True
|
||||
and channel.user_hidden is False
|
||||
if validated_data.get("hidden_from_output") is True
|
||||
and channel.hidden_from_output is False
|
||||
and channel.channel_number is not None
|
||||
and channel.auto_created
|
||||
and channel.auto_created_by_id
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ def assign_compact_numbers_for_channels(channel_ids):
|
|||
id__in=channel_ids,
|
||||
auto_created=True,
|
||||
auto_created_by__isnull=False,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
channel_number__isnull=True,
|
||||
).select_related("override", "channel_group", "auto_created_by")
|
||||
)
|
||||
|
|
@ -279,7 +279,7 @@ def _repack_inner(group_relation):
|
|||
for ch in channels:
|
||||
if _channel_has_number_override(ch):
|
||||
pinned.append(ch)
|
||||
elif ch.user_hidden:
|
||||
elif ch.hidden_from_output:
|
||||
hidden.append(ch)
|
||||
else:
|
||||
visible.append(ch)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ flag, configurable auto-sync number range, and nullable channel_number for
|
|||
compact-numbering slot release.
|
||||
|
||||
Bundled operations (in forward order; reversed on rollback):
|
||||
1. AddField Channel.user_hidden
|
||||
1. AddField Channel.hidden_from_output
|
||||
2. CreateModel ChannelOverride (one-to-one Channel)
|
||||
3. AddField ChannelGroupM3UAccount.auto_sync_channel_end
|
||||
4. RunPython backfill_auto_created_by_null (orphan re-attribution / demotion)
|
||||
|
|
@ -113,7 +113,7 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='channel',
|
||||
name='user_hidden',
|
||||
name='hidden_from_output',
|
||||
field=models.BooleanField(
|
||||
db_index=True,
|
||||
default=False,
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ class Channel(models.Model):
|
|||
# Auto-sync still recognizes them so they are not recreated when their
|
||||
# underlying provider stream persists; this is an output-layer concern, not
|
||||
# a sync-time flag.
|
||||
user_hidden = models.BooleanField(
|
||||
hidden_from_output = models.BooleanField(
|
||||
default=False,
|
||||
db_index=True,
|
||||
help_text="Exclude this channel from downstream client output (HDHR, M3U, EPG, XC). Auto-sync still updates provider metadata."
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
"logo_id",
|
||||
"user_level",
|
||||
"is_adult",
|
||||
"user_hidden",
|
||||
"hidden_from_output",
|
||||
"auto_created",
|
||||
"auto_created_by",
|
||||
"auto_created_by_name",
|
||||
|
|
|
|||
|
|
@ -109,13 +109,13 @@ def assign_compact_number_on_unhide(sender, instance, created, **kwargs):
|
|||
"""
|
||||
if created:
|
||||
return
|
||||
# Skip the signal when update_fields proves user_hidden was not
|
||||
# Skip the signal when update_fields proves hidden_from_output was not
|
||||
# touched. Sync sets update_fields on every save, so this keeps the
|
||||
# signal off the sync hot path.
|
||||
update_fields = kwargs.get("update_fields")
|
||||
if update_fields is not None and "user_hidden" not in update_fields:
|
||||
if update_fields is not None and "hidden_from_output" not in update_fields:
|
||||
return
|
||||
if instance.user_hidden:
|
||||
if instance.hidden_from_output:
|
||||
return
|
||||
if instance.channel_number is not None:
|
||||
return
|
||||
|
|
@ -161,9 +161,9 @@ def release_compact_number_on_hide(sender, instance, created, **kwargs):
|
|||
if created:
|
||||
return
|
||||
update_fields = kwargs.get("update_fields")
|
||||
if update_fields is not None and "user_hidden" not in update_fields:
|
||||
if update_fields is not None and "hidden_from_output" not in update_fields:
|
||||
return
|
||||
if not instance.user_hidden:
|
||||
if not instance.hidden_from_output:
|
||||
return
|
||||
if instance.channel_number is None:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class LineupAPIView(APIView):
|
|||
|
||||
channels = (
|
||||
with_effective_values(base_qs)
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class LineupAPIView(APIView):
|
|||
|
||||
channels = (
|
||||
with_effective_values(Channel.objects.all())
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
lineup = []
|
||||
|
|
|
|||
|
|
@ -1849,7 +1849,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
Channel.objects.exclude(
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
).values_list("channel_number", flat=True)
|
||||
)
|
||||
# Override pins are global reservations: effective_channel_number
|
||||
|
|
@ -2158,7 +2158,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
channels_to_delete = [
|
||||
ch
|
||||
for ch in existing_channel_map.values()
|
||||
if not ch.user_hidden
|
||||
if not ch.hidden_from_output
|
||||
]
|
||||
if channels_to_delete:
|
||||
deleted_count = len(channels_to_delete)
|
||||
|
|
@ -2275,7 +2275,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
if end_number is not None:
|
||||
overflow_delete_ids = []
|
||||
for stream_id, ch in list(existing_channel_map.items()):
|
||||
if ch.user_hidden:
|
||||
if ch.hidden_from_output:
|
||||
continue
|
||||
num = ch.channel_number
|
||||
if num is None:
|
||||
|
|
@ -2654,7 +2654,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
channels_to_delete = []
|
||||
for ch_id, pairs in channel_streams_in_group.items():
|
||||
channel = pairs[0][1]
|
||||
if channel.user_hidden:
|
||||
if channel.hidden_from_output:
|
||||
continue
|
||||
stream_ids = {sid for sid, _ in pairs}
|
||||
if not (stream_ids & processed_stream_ids):
|
||||
|
|
@ -2713,7 +2713,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
orphaned_channels = Channel.objects.filter(
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
).exclude(
|
||||
id__in=ChannelStream.objects.filter(
|
||||
stream__m3u_account=account,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class CompoundFixtureSyncTests(TestCase):
|
|||
channel_group=self.group,
|
||||
auto_created=True,
|
||||
auto_created_by=self.account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
)
|
||||
ChannelStream.objects.create(
|
||||
channel=self.hidden_channel, stream=self.hidden_stream, order=0
|
||||
|
|
@ -172,7 +172,7 @@ class CompoundFixtureSyncTests(TestCase):
|
|||
|
||||
# Hidden channel survives and remains hidden.
|
||||
self.hidden_channel.refresh_from_db()
|
||||
self.assertTrue(self.hidden_channel.user_hidden)
|
||||
self.assertTrue(self.hidden_channel.hidden_from_output)
|
||||
|
||||
# Overridden channel: the override row is intact, not cleared by
|
||||
# sync. The pinned channel_number persists.
|
||||
|
|
|
|||
|
|
@ -968,7 +968,7 @@ class ChannelOverrideClearResponseTests(TestCase):
|
|||
|
||||
class HiddenChannelPreservationTests(TestCase):
|
||||
"""
|
||||
Hidden channels (`user_hidden=True`) must be preserved across every
|
||||
Hidden channels (`hidden_from_output=True`) must be preserved across every
|
||||
sync cleanup path so users can rely on the hide flag as "keep this
|
||||
channel even if its source stream temporarily disappears". Common
|
||||
case: event / PPV / seasonal channels whose provider stream comes
|
||||
|
|
@ -1003,13 +1003,13 @@ class HiddenChannelPreservationTests(TestCase):
|
|||
channel_group=group,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
)
|
||||
ChannelStream.objects.create(channel=ch, stream=stream, order=0)
|
||||
|
||||
# Sync's scan_start_time is now (stream's last_seen is older,
|
||||
# so the stream is excluded from current_streams - simulating
|
||||
# the provider dropping it). Without the user_hidden guard, the
|
||||
# the provider dropping it). Without the hidden_from_output guard, the
|
||||
# channel would be cleaned up here.
|
||||
result = _sync(account)
|
||||
|
||||
|
|
@ -1047,7 +1047,7 @@ class HiddenChannelPreservationTests(TestCase):
|
|||
channel_group=group,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
)
|
||||
ChannelStream.objects.create(channel=ch, stream=stream, order=0)
|
||||
|
||||
|
|
@ -1105,7 +1105,7 @@ class CompactNumberingTests(TestCase):
|
|||
)
|
||||
Channel.objects.filter(
|
||||
id__in=[c.id for c in hidden_targets]
|
||||
).update(user_hidden=True)
|
||||
).update(hidden_from_output=True)
|
||||
|
||||
# Re-run sync; compact pack runs at the end.
|
||||
_sync(account)
|
||||
|
|
@ -1114,7 +1114,7 @@ class CompactNumberingTests(TestCase):
|
|||
Channel.objects.filter(
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
).values_list("channel_number", flat=True)
|
||||
)
|
||||
self.assertEqual(nums, [100, 101, 102])
|
||||
|
|
@ -1123,7 +1123,7 @@ class CompactNumberingTests(TestCase):
|
|||
Channel.objects.filter(
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
).values_list("channel_number", flat=True)
|
||||
)
|
||||
self.assertTrue(
|
||||
|
|
@ -1184,10 +1184,10 @@ class CompactNumberingTests(TestCase):
|
|||
channel_group=group,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
)
|
||||
|
||||
ch.user_hidden = False
|
||||
ch.hidden_from_output = False
|
||||
ch.save()
|
||||
ch.refresh_from_db()
|
||||
|
||||
|
|
@ -1293,7 +1293,7 @@ class CompactNumberingTests(TestCase):
|
|||
channel_group=group,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=False,
|
||||
hidden_from_output=False,
|
||||
)
|
||||
|
||||
# Make sure the lock starts free.
|
||||
|
|
@ -1526,7 +1526,7 @@ class AutoCleanupToggleTests(TestCase):
|
|||
channel_group=group,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
)
|
||||
|
||||
result = _sync(account)
|
||||
|
|
@ -1536,7 +1536,7 @@ class AutoCleanupToggleTests(TestCase):
|
|||
Channel.objects.filter(
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
).count(),
|
||||
1,
|
||||
"Hidden channels must survive cleanup regardless of the toggle",
|
||||
|
|
@ -1698,7 +1698,7 @@ class HiddenChannelNumberCollisionTests(TestCase):
|
|||
rel_b.save()
|
||||
|
||||
# A hidden auto-created channel pinned at #100 in group_a.
|
||||
# Its source stream is gone (last_seen old), but user_hidden=True
|
||||
# Its source stream is gone (last_seen old), but hidden_from_output=True
|
||||
# protects the channel from cleanup.
|
||||
old_seen = timezone.now() - timedelta(days=1)
|
||||
Stream.objects.create(
|
||||
|
|
@ -1715,7 +1715,7 @@ class HiddenChannelNumberCollisionTests(TestCase):
|
|||
channel_group=group_a,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
user_hidden=True,
|
||||
hidden_from_output=True,
|
||||
)
|
||||
|
||||
# A NEW stream in group_b, no channel yet. Sync should create a
|
||||
|
|
|
|||
|
|
@ -169,12 +169,12 @@ def generate_m3u(request, profile_name=None, user=None):
|
|||
base_qs = Channel.objects.select_related('channel_group', 'logo')
|
||||
|
||||
# Resolve effective (override | provider) values at SQL level so ordering,
|
||||
# naming, and logo resolution honor user overrides. `exclude(user_hidden=True)`
|
||||
# naming, and logo resolution honor user overrides. `exclude(hidden_from_output=True)`
|
||||
# is the consumer-facing hide guarantee.
|
||||
from apps.channels.managers import with_effective_values
|
||||
channels = (
|
||||
with_effective_values(base_qs, select_related_fks=True)
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
|
||||
|
|
@ -1372,7 +1372,7 @@ def generate_epg(request, profile_name=None, user=None):
|
|||
from apps.channels.managers import with_effective_values
|
||||
channels = (
|
||||
with_effective_values(base_qs, select_related_fks=True)
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
|
||||
|
|
@ -2133,12 +2133,12 @@ def xc_get_live_categories(user):
|
|||
response = []
|
||||
|
||||
# Rank categories by the minimum EFFECTIVE channel number across their
|
||||
# visible (not user_hidden) channels so overridden numbers drive the
|
||||
# visible (not hidden_from_output) channels so overridden numbers drive the
|
||||
# ordering, not the underlying provider values.
|
||||
effective_min = Min(
|
||||
Coalesce("channels__override__channel_number", "channels__channel_number")
|
||||
)
|
||||
hidden_exclusion = {"channels__user_hidden": False}
|
||||
hidden_exclusion = {"channels__hidden_from_output": False}
|
||||
|
||||
if user.user_level < 10:
|
||||
user_profile_count = user.channel_profiles.count()
|
||||
|
|
@ -2220,7 +2220,7 @@ def xc_get_live_streams(request, user, category_id=None):
|
|||
|
||||
channels = (
|
||||
with_effective_values(base_qs, select_related_fks=True)
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
|
||||
|
|
@ -2305,7 +2305,7 @@ def xc_get_epg(request, user, short=False):
|
|||
# resolution path so a single channel lookup honors the same visibility
|
||||
# rules as xc_get_live_streams.
|
||||
def _annotate(qs):
|
||||
return with_effective_values(qs, select_related_fks=True).exclude(user_hidden=True)
|
||||
return with_effective_values(qs, select_related_fks=True).exclude(hidden_from_output=True)
|
||||
|
||||
if user.user_level < 10:
|
||||
user_profile_count = user.channel_profiles.count()
|
||||
|
|
@ -2354,7 +2354,7 @@ def xc_get_epg(request, user, short=False):
|
|||
with_effective_values(
|
||||
Channel.objects.filter(channel_group=effective_group) if effective_group else Channel.objects.none()
|
||||
)
|
||||
.exclude(user_hidden=True)
|
||||
.exclude(hidden_from_output=True)
|
||||
.order_by("effective_channel_number")
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -927,9 +927,9 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => {
|
|||
<Box>
|
||||
<Switch
|
||||
label="Hide from Clients"
|
||||
checked={watch('user_hidden')}
|
||||
checked={watch('hidden_from_output')}
|
||||
onChange={(event) =>
|
||||
setValue('user_hidden', event.currentTarget.checked)
|
||||
setValue('hidden_from_output', event.currentTarget.checked)
|
||||
}
|
||||
size="md"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
stream_profile_id: '-1',
|
||||
user_level: '-1',
|
||||
is_adult: '-1',
|
||||
user_hidden: '-1',
|
||||
hidden_from_output: '-1',
|
||||
clear_overrides: '-1',
|
||||
},
|
||||
});
|
||||
|
|
@ -166,9 +166,9 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
getMatureContentChange(values.is_adult),
|
||||
getEpgChange(selectedDummyEpgId, epgs),
|
||||
];
|
||||
if (values.user_hidden && values.user_hidden !== '-1') {
|
||||
if (values.hidden_from_output && values.hidden_from_output !== '-1') {
|
||||
lines.push(
|
||||
`• Hide from Clients: ${values.user_hidden === 'true' ? 'Yes' : 'No'}`
|
||||
`• Hide from Clients: ${values.hidden_from_output === 'true' ? 'Yes' : 'No'}`
|
||||
);
|
||||
}
|
||||
if (values.clear_overrides === 'clear') {
|
||||
|
|
@ -877,8 +877,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
size="xs"
|
||||
label="Hide from Clients"
|
||||
description="Hidden channels are excluded from HDHR, M3U, EPG, and XC output."
|
||||
{...form.getInputProps('user_hidden')}
|
||||
key={form.key('user_hidden')}
|
||||
{...form.getInputProps('hidden_from_output')}
|
||||
key={form.key('hidden_from_output')}
|
||||
data={[
|
||||
{ value: '-1', label: '(no change)' },
|
||||
{ value: 'true', label: 'Hide' },
|
||||
|
|
|
|||
|
|
@ -967,7 +967,7 @@ const ChannelsTable = ({ onReady }) => {
|
|||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
{row.user_hidden && (
|
||||
{row.hidden_from_output && (
|
||||
<Tooltip label="Hidden from HDHR, M3U, EPG, and XC output.">
|
||||
<Box
|
||||
component="span"
|
||||
|
|
|
|||
|
|
@ -191,10 +191,10 @@ export const buildSubmitValues = (
|
|||
values.is_adult = values.is_adult === 'true';
|
||||
}
|
||||
|
||||
if (values.user_hidden === '-1' || values.user_hidden === undefined) {
|
||||
delete values.user_hidden;
|
||||
if (values.hidden_from_output === '-1' || values.hidden_from_output === undefined) {
|
||||
delete values.hidden_from_output;
|
||||
} else {
|
||||
values.user_hidden = values.user_hidden === 'true';
|
||||
values.hidden_from_output = values.hidden_from_output === 'true';
|
||||
}
|
||||
|
||||
// clear_overrides is a UI-only flag; the caller splits it out and routes a
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export const getChannelFormDefaultValues = (channel, channelGroups) => {
|
|||
logo_id: logoId ? `${logoId}` : '',
|
||||
user_level: `${channel?.user_level ?? '0'}`,
|
||||
is_adult: channel?.is_adult ?? false,
|
||||
user_hidden: channel?.user_hidden ?? false,
|
||||
hidden_from_output: channel?.hidden_from_output ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -235,14 +235,14 @@ export const handleEpgUpdate = async (
|
|||
) => {
|
||||
// Auto-synced channels route identity edits into the override row. Sync
|
||||
// keeps writing provider values to Channel.* unmodified, so the override
|
||||
// is what actually persists user changes across refreshes. `user_hidden`
|
||||
// is what actually persists user changes across refreshes. `hidden_from_output`
|
||||
// stays as a direct Channel field even for auto-created channels because
|
||||
// it is a status flag, not a value replacement.
|
||||
if (channel.auto_created) {
|
||||
const overridePayload = buildOverridePayload(channel, formattedValues);
|
||||
const payload = {
|
||||
id: channel.id,
|
||||
user_hidden: formattedValues.user_hidden,
|
||||
hidden_from_output: formattedValues.hidden_from_output,
|
||||
};
|
||||
if (overridePayload !== undefined) {
|
||||
payload.override = overridePayload;
|
||||
|
|
|
|||
|
|
@ -73,20 +73,20 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('always-raw fields (user_hidden, user_level, is_adult) bypass override routing', async () => {
|
||||
// user_hidden is a status flag, not a value override - it goes to
|
||||
// Channel.user_hidden directly even on auto-created channels.
|
||||
it('always-raw fields (hidden_from_output, user_level, is_adult) bypass override routing', async () => {
|
||||
// hidden_from_output is a status flag, not a value override - it goes to
|
||||
// Channel.hidden_from_output directly even on auto-created channels.
|
||||
const channelsById = {
|
||||
1: { id: 1, auto_created: true },
|
||||
};
|
||||
await updateChannelsWithOverrideRouting(
|
||||
[1],
|
||||
{ user_hidden: true, name: 'Renamed' },
|
||||
{ hidden_from_output: true, name: 'Renamed' },
|
||||
channelsById,
|
||||
);
|
||||
const body = API.bulkUpdateChannels.mock.calls[0][0];
|
||||
expect(body).toEqual([
|
||||
{ id: 1, user_hidden: true, override: { name: 'Renamed' } },
|
||||
{ id: 1, hidden_from_output: true, override: { name: 'Renamed' } },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ describe('ChannelUtils', () => {
|
|||
logo_id: '10',
|
||||
user_level: '1',
|
||||
is_adult: false,
|
||||
user_hidden: false,
|
||||
hidden_from_output: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ describe('ChannelUtils', () => {
|
|||
logo_id: '',
|
||||
user_level: '0',
|
||||
is_adult: false,
|
||||
user_hidden: false,
|
||||
hidden_from_output: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue