diff --git a/fixtures/sample/radiosettings.json b/fixtures/sample/radiosettings.json index 61278a91937674ad3f3f538f76e7bd8404c318db..75f81aa0b131d7b718bfc671a07b5393f3bf5f6c 100644 --- a/fixtures/sample/radiosettings.json +++ b/fixtures/sample/radiosettings.json @@ -3,18 +3,28 @@ "model": "program.radiosettings", "pk": 1, "fields": { - "cba_api_key": "dea", + "cba_api_key": "", "cba_domains": [ "cba.media" ], + "fallback_default_pool": "", "fallback_show": null, + "host_image_aspect_ratio": "1:1", + "host_image_shape": "round", "line_in_channels": { - "1": "live", - "2": "preprod" + "0": "live", + "1": "preprod" }, + "micro_show": null, + "note_image_aspect_ratio": "16:9", + "note_image_shape": "rect", + "show_image_aspect_ratio": "16:9", + "show_image_shape": "rect", + "show_logo_aspect_ratio": "1:1", + "show_logo_shape": "rect", "station_logo": null, - "station_name": "Radio Helsinki", - "station_website": "https://helsinki.at/" + "station_name": "Radio AURA", + "station_website": "https://aura.radio" } } ] diff --git a/poetry.lock b/poetry.lock index 1f31997bd5148d4e14ba2a6d18936f0c5e66ba69..1c1eccf42d7ce7598b11881fff1878af3dc2728c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand. [[package]] name = "argcomplete" @@ -93,13 +93,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -239,63 +239,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.1" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, - {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, - {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, - {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, - {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, - {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, - {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, - {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, - {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, - {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.extras] @@ -520,13 +520,13 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "faker" -version = "25.2.0" +version = "25.5.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-25.2.0-py3-none-any.whl", hash = "sha256:cfe97c4857c4c36ee32ea4aaabef884895992e209bae4cbd26807cf3e05c6918"}, - {file = "Faker-25.2.0.tar.gz", hash = "sha256:45b84f47ff1ef86e3d1a8d11583ca871ecf6730fad0660edadc02576583a2423"}, + {file = "Faker-25.5.0-py3-none-any.whl", hash = "sha256:edb85040a47ef1b30ccd8c4b6f07ee3cb4bd64aab1483be4efe75816ee2e2e36"}, + {file = "Faker-25.5.0.tar.gz", hash = "sha256:84d454fc9fef0b73428e00bdf45a36c04568c75f22727e990071580840cfbb84"}, ] [package.dependencies] @@ -784,18 +784,15 @@ files = [ [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.9.1" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "packaging" version = "24.0" @@ -1173,13 +1170,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "8.2.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1372,13 +1369,13 @@ rpds-py = ">=0.7.0" [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1499,21 +1496,6 @@ files = [ {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, ] -[[package]] -name = "setuptools" -version = "70.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -1553,13 +1535,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, + {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, ] [[package]] @@ -1672,4 +1654,4 @@ tests = ["build", "coverage", "mypy", "ruff", "wheel"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "08207d73bde29fc310e646d91845a97cce45fb708ee3b20a089436d7e18dfee3" +content-hash = "e92284084d976e62cd8b2760be953c54b10a6cc94af62cc631218ddd916753a1" diff --git a/program/admin.py b/program/admin.py index 804236289733b158e081d4f3d45879263606c93f..8836b0fd1260eacae30442bc2c1e5d5cbfb88cee 100644 --- a/program/admin.py +++ b/program/admin.py @@ -105,7 +105,21 @@ admin.site.register(User, UserProfileUserAdmin) class RadioSettingsAdmin(admin.ModelAdmin): fieldsets = [ (None, {"fields": ["station_name", "station_website", "station_logo"]}), - ("Programme", {"fields": ["fallback_show"]}), + ( + "Image requirements", + { + "fields": [ + ("host_image_aspect_ratio", "host_image_shape"), + ( + "note_image_aspect_ratio", + "note_image_shape", + ), + ("show_image_aspect_ratio", "show_image_shape"), + ("show_logo_aspect_ratio", "show_logo_shape"), + ] + }, + ), + ("Programme", {"fields": [("fallback_show", "fallback_default_pool"), "micro_show"]}), ("CBA", {"fields": ["cba_api_key", "cba_domains"]}), ("Playout", {"fields": ["line_in_channels"]}), ] diff --git a/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py b/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..c6032dfccbcfc74eccfa8c131c18b2ab14a5fcbb --- /dev/null +++ b/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py @@ -0,0 +1,70 @@ +# Generated by Django 4.2.13 on 2024-06-03 16:43 + +import program.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0090_alter_playlist_options"), + ] + + operations = [ + migrations.AddField( + model_name="radiosettings", + name="host_image_aspect_ratio", + field=program.models.ImageAspectRadioField( + choices=[("1:1", "1:1"), ("16:9", "16:9")], default="1:1", max_length=4 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="host_image_shape", + field=program.models.ImageShapeField( + choices=[("rect", "rect"), ("round", "round")], default="round", max_length=5 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="note_image_aspect_ratio", + field=program.models.ImageAspectRadioField( + choices=[("1:1", "1:1"), ("16:9", "16:9")], default="16:9", max_length=4 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="note_image_shape", + field=program.models.ImageShapeField( + choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="show_image_aspect_ratio", + field=program.models.ImageAspectRadioField( + choices=[("1:1", "1:1"), ("16:9", "16:9")], default="16:9", max_length=4 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="show_image_shape", + field=program.models.ImageShapeField( + choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="show_logo_aspect_ratio", + field=program.models.ImageAspectRadioField( + choices=[("1:1", "1:1"), ("16:9", "16:9")], default="1:1", max_length=4 + ), + ), + migrations.AddField( + model_name="radiosettings", + name="show_logo_shape", + field=program.models.ImageShapeField( + choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5 + ), + ), + ] diff --git a/program/migrations/0092_radiosettings_micro_show.py b/program/migrations/0092_radiosettings_micro_show.py new file mode 100644 index 0000000000000000000000000000000000000000..d584218ccb84af64d246ee71b2359b2b4e45ce54 --- /dev/null +++ b/program/migrations/0092_radiosettings_micro_show.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.13 on 2024-06-03 16:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0091_radiosettings_host_image_aspect_ratio_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="radiosettings", + name="micro_show", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="program.show", + ), + ), + ] diff --git a/program/migrations/0093_radiosettings_fallback_default_pool.py b/program/migrations/0093_radiosettings_fallback_default_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..0a2906b8769ebebc34dbb1b2481db5b9cf5130ea --- /dev/null +++ b/program/migrations/0093_radiosettings_fallback_default_pool.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-06-03 16:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0092_radiosettings_micro_show"), + ] + + operations = [ + migrations.AddField( + model_name="radiosettings", + name="fallback_default_pool", + field=models.CharField(blank=True, max_length=32), + ), + ] diff --git a/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py b/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..15e71367d12ea7d5cf7086fc80251e636a0d866c --- /dev/null +++ b/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.13 on 2024-06-03 16:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0093_radiosettings_fallback_default_pool"), + ] + + operations = [ + migrations.AlterField( + model_name="radiosettings", + name="fallback_show", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="program.show", + ), + ), + migrations.AlterField( + model_name="radiosettings", + name="station_logo", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="program.image", + ), + ), + ] diff --git a/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py b/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..a40f5fd8047e1ff852d21386d65509a49e56305a --- /dev/null +++ b/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.13 on 2024-06-04 18:36 + +import program.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0094_alter_radiosettings_fallback_show_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="radiosettings", + name="cba_domains", + field=models.JSONField( + blank=True, + default=list, + help_text="JSON array of strings", + validators=[program.models.validate_cba_domains], + verbose_name="CBA domains", + ), + ), + migrations.AlterField( + model_name="radiosettings", + name="line_in_channels", + field=models.JSONField( + blank=True, + default=dict, + help_text="JSON key/value pairs", + validators=[program.models.validate_line_in_channels], + ), + ), + ] diff --git a/program/models.py b/program/models.py index 06b4a775377cd16728627525303cc3e551a72260..16886c5b7af4e30d50ed4b20fb1d2c95663fa78b 100644 --- a/program/models.py +++ b/program/models.py @@ -17,11 +17,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - +import jsonschema from rest_framework.exceptions import ValidationError from versatileimagefield.fields import PPOIField, VersatileImageField from django.contrib.auth.models import User +from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models from django.db.models import Max, Q from django.utils import timezone @@ -539,14 +540,87 @@ class Playlist(models.Model): ] +class ImageAspectRadioField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs["choices"] = [ + ("1:1", "1:1"), + ("16:9", "16:9"), + ] + kwargs["max_length"] = 4 + + super().__init__(*args, **kwargs) + + +class ImageShapeField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs["choices"] = [ + ("rect", "rect"), + ("round", "round"), + ] + kwargs["max_length"] = 5 + + super().__init__(*args, **kwargs) + + +def validate_cba_domains(value): + schema = { + "type": "array", + "items": {"type": "string"}, + } + + try: + jsonschema.validate(value, schema) + except jsonschema.exceptions.ValidationError as e: + raise DjangoValidationError(e.args[0]) + + +def validate_line_in_channels(value): + schema = { + "type": "object", + "patternProperties": { + "^.*$": {"type": "string"}, + }, + } + + try: + jsonschema.validate(value, schema) + except jsonschema.exceptions.ValidationError as e: + raise DjangoValidationError(e.args[0]) + + class RadioSettings(models.Model): cba_api_key = models.CharField(blank=True, max_length=64, verbose_name="CBA API key") cba_domains = models.JSONField( - blank=True, default=list, help_text="JSON array of strings", verbose_name="CBA domains" + blank=True, + default=list, + help_text="JSON array of strings", + validators=[validate_cba_domains], + verbose_name="CBA domains", + ) + fallback_default_pool = models.CharField(blank=True, max_length=32) + fallback_show = models.ForeignKey( + Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+" + ) + host_image_aspect_ratio = ImageAspectRadioField(default="1:1") + host_image_shape = ImageShapeField(default="round") + line_in_channels = models.JSONField( + blank=True, + default=dict, + help_text="JSON key/value pairs", + validators=[validate_line_in_channels], + ) + micro_show = models.ForeignKey( + Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+" + ) + note_image_aspect_ratio = ImageAspectRadioField(default="16:9") + note_image_shape = ImageShapeField(default="rect") + show_image_aspect_ratio = ImageAspectRadioField(default="16:9") + show_image_shape = ImageShapeField(default="rect") + show_logo_aspect_ratio = ImageAspectRadioField(default="1:1") + show_logo_shape = ImageShapeField(default="rect") + station_logo = models.ForeignKey( + Image, blank=True, null=True, on_delete=models.CASCADE, related_name="+" ) - fallback_show = models.ForeignKey(Show, blank=True, null=True, on_delete=models.CASCADE) - line_in_channels = models.JSONField(blank=True, default=dict, help_text="JSON key/value pairs") - station_logo = models.ForeignKey(Image, blank=True, null=True, on_delete=models.CASCADE) station_name = models.CharField(max_length=256, unique=True) station_website = models.URLField() diff --git a/program/serializers.py b/program/serializers.py index ac67e886150abb69d03fa14da240f9051dc242bd..8f30cecfccbfd7ee71ae4395730c491a072f6380 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -19,7 +19,7 @@ # import re -from typing import NotRequired, TypedDict +from typing import Literal, NotRequired, TypedDict from drf_jsonschema_serializer import JSONSchemaField from rest_framework import serializers @@ -110,6 +110,9 @@ class UserSerializer(serializers.ModelSerializer): permissions = serializers.SerializerMethodField() # Add profile fields to JSON profile = ProfileSerializer(required=False) + host_ids = serializers.PrimaryKeyRelatedField( + many=True, queryset=Host.objects.all(), source="hosts" + ) class Meta: model = User @@ -117,6 +120,7 @@ class UserSerializer(serializers.ModelSerializer): "id", "is_privileged", "permissions", + "host_ids", ) fields = ( "email", @@ -344,7 +348,6 @@ class HostSerializer(serializers.ModelSerializer): many=True, required=False, help_text="Array of `HostLink` objects. Can be empty." ) owner_ids = serializers.PrimaryKeyRelatedField( - allow_null=True, many=True, queryset=User.objects.all(), source="owners", @@ -1166,12 +1169,27 @@ class RadioCBASettings(TypedDict): domains: list[str] +class ProgrammeFallback(TypedDict): + default_pool: Literal["fallback"] | None + show_id: int | None + + +class MicroProgramme(TypedDict): + show_id: int | None + + class RadioProgrammeSettings(TypedDict): - fallback_show_id: int | None + fallback: ProgrammeFallback + micro: MicroProgramme + + +class PlayoutPools(TypedDict): + fallback: str | None class RadioPlayoutSettings(TypedDict): line_in_channels: dict[str, str] + pools: PlayoutPools class RadioStationSettings(TypedDict): @@ -1180,15 +1198,43 @@ class RadioStationSettings(TypedDict): website: str +class ImageFrame(TypedDict): + aspect_ratio: tuple[int, int] + shape: Literal["rect", "round"] + + +class ImageRequirements(TypedDict): + frame: ImageFrame + + +# done this way, because the keys have dots (".") +RadioImageRequirementsSettings = TypedDict( + "RadioImageRequirementsSettings", + { + "host.image": ImageRequirements, + "note.image": ImageRequirements, + "show.image": ImageRequirements, + "show.logo": ImageRequirements, + }, +) + + class RadioSettingsSerializer(serializers.ModelSerializer): cba = serializers.SerializerMethodField() + image_requirements = serializers.SerializerMethodField() playout = serializers.SerializerMethodField() programme = serializers.SerializerMethodField() station = serializers.SerializerMethodField() class Meta: - read_only_fields = ("id", "cba", "playout", "programme", "station") - fields = read_only_fields + fields = read_only_fields = ( + "id", + "cba", + "image_requirements", + "playout", + "programme", + "station", + ) model = RadioSettings def get_cba(self, obj) -> RadioCBASettings: @@ -1200,13 +1246,67 @@ class RadioSettingsSerializer(serializers.ModelSerializer): else: return {"domains": obj.cba_domains} + @staticmethod + def get_image_requirements(obj) -> RadioImageRequirementsSettings: + def get_aspect_ration(field) -> tuple[int, int]: + """return the tuple of ints representing the aspect ratio of the image.""" + + values = field.split(":") + + return int(values[0]), int(values[1]) + + aspect_ratios = { + "host.image": get_aspect_ration(obj.host_image_aspect_ratio), + "note.image": get_aspect_ration(obj.note_image_aspect_ratio), + "show.image": get_aspect_ration(obj.show_image_aspect_ratio), + "show.logo": get_aspect_ration(obj.show_logo_aspect_ratio), + } + + return { + "host.image": { + "frame": { + "aspect_ratio": aspect_ratios["host.image"], + "shape": obj.host_image_shape, + } + }, + "note.image": { + "frame": { + "aspect_ratio": aspect_ratios["note.image"], + "shape": obj.host_image_shape, + } + }, + "show.image": { + "frame": { + "aspect_ratio": aspect_ratios["show.image"], + "shape": obj.host_image_shape, + } + }, + "show.logo": { + "frame": { + "aspect_ratio": aspect_ratios["show.logo"], + "shape": obj.host_image_shape, + } + }, + } + @staticmethod def get_programme(obj) -> RadioProgrammeSettings: - return {"fallback_show_id": obj.fallback_show.id if obj.fallback_show else None} + return { + "micro": {"show_id": obj.micro_show.id if obj.micro_show else None}, + "fallback": { + "show_id": obj.fallback_show.id if obj.fallback_show else None, + "default_pool": "fallback", + }, + } @staticmethod def get_playout(obj) -> RadioPlayoutSettings: - return {"line_in_channels": obj.line_in_channels} + return { + "line_in_channels": obj.line_in_channels, + "pools": { + "fallback": obj.fallback_default_pool, + }, + } @staticmethod def get_station(obj) -> RadioStationSettings: diff --git a/program/services.py b/program/services.py index 038f3089355ef1c27a12053364a1d317225fa423..df3a45f27591081083974005778f41fec15a44a4 100644 --- a/program/services.py +++ b/program/services.py @@ -19,7 +19,8 @@ import copy from datetime import datetime, time, timedelta -from typing import TypedDict +from itertools import pairwise +from typing import Literal, TypedDict from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule @@ -30,7 +31,15 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q, QuerySet from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from program.models import Note, RRule, Schedule, ScheduleConflictError, Show, TimeSlot +from program.models import ( + Note, + RadioSettings, + RRule, + Schedule, + ScheduleConflictError, + Show, + TimeSlot, +) from program.serializers import ScheduleSerializer, TimeSlotSerializer from program.utils import parse_date, parse_datetime, parse_time @@ -86,6 +95,36 @@ class ScheduleCreateUpdateData(TypedDict): solutions: dict[str, str] +class ScheduleEntry(TypedDict): + end: str + is_virtual: bool + show_id: int + start: str + title: str + + +class TimeslotEntry(TypedDict): + end: str + id: int + is_virtual: Literal[False] + playlist_id: int | None + repetition_of_id: int | None + schedule_default_playlist_id: int | None + schedule_id: int + show_default_playlist_id: int | None + show_id: int + start: str + title: str + + +class VirtualTimeslotEntry(TypedDict): + end: str + is_virtual: Literal[True] + show_id: int + start: str + title: str + + def create_timeslot(start: str, end: str, schedule: Schedule) -> TimeSlot: """Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`.""" @@ -740,14 +779,93 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts: return conflicts -def get_timerange_timeslots(start: datetime, end: datetime) -> QuerySet[TimeSlot]: - """Gets a queryset of timeslots between the given `start` and `end` datetime.""" +def make_schedule_entry(*, timeslot_entry: TimeslotEntry) -> ScheduleEntry: + """returns a schedule entry for the given timeslot entry.""" + + return { + "end": timeslot_entry["end"], + "show_id": timeslot_entry["show_id"], + "is_virtual": timeslot_entry["is_virtual"], + "start": timeslot_entry["start"], + "title": timeslot_entry["title"], + } + + +def make_timeslot_entry(*, timeslot: TimeSlot) -> TimeslotEntry: + """returns a timeslot entry for the given timeslot.""" + + schedule = timeslot.schedule + show = timeslot.schedule.show + + return { + "end": timeslot.end.strftime("%Y-%m-%dT%H:%M:%S %z"), + "id": timeslot.id, + "is_virtual": False, + "playlist_id": timeslot.playlist_id, + # 'timeslot.repetition_of` is a foreign key that can be null + "repetition_of_id": timeslot.repetition_of.id if timeslot.repetition_of else None, + "schedule_default_playlist_id": schedule.default_playlist_id, + "schedule_id": schedule.id, + "show_default_playlist_id": show.default_playlist_id, + "show_id": show.id, + "start": timeslot.start.strftime("%Y-%m-%dT%H:%M:%S %z"), + "title": f"{show.name} {_('REP')}" if schedule.is_repetition else show.name, + } + + +def make_virtual_timeslot_entry(*, gap_start: datetime, gap_end: datetime) -> VirtualTimeslotEntry: + """returns a virtual timeslot entry to fill the gap in between `gap_start` and `gap_end`.""" + + return { + "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S %z"), + "is_virtual": True, + "show_id": RadioSettings.objects.first().fallback_show.id, + "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S %z"), + "title": RadioSettings.objects.first().fallback_default_pool, + } + + +def get_timerange_timeslot_entries( + timerange_start: datetime, timerange_end: datetime, include_virtual: bool = False +) -> list[TimeslotEntry | VirtualTimeslotEntry]: + """Gets list of timeslot entries between the given `timerange_start` and `timerange_end`. + + Include virtual timeslots if requested.""" + + timeslots = TimeSlot.objects.filter( + # start before `timerange_start` and end after `timerange_start` + Q(start__lt=timerange_start, end__gt=timerange_start) + # start after/at `timerange_start`, end before/at `timerange_end` + | Q(start__gte=timerange_start, end__lte=timerange_end) + # start before `timerange_end`, end after/at `timerange_end` + | Q(start__lt=timerange_end, end__gte=timerange_end) + ).select_related("schedule") + + if not include_virtual: + return [make_timeslot_entry(timeslot=timeslot) for timeslot in timeslots] + + timeslot_entries = [] + # gap before the first timeslot + first_timeslot = timeslots.first() + if first_timeslot.start > timerange_start: + timeslot_entries.append( + make_virtual_timeslot_entry(gap_start=timerange_start, gap_end=first_timeslot.start) + ) - return TimeSlot.objects.filter( - # start before `start` and end after `start` - Q(start__lt=start, end__gt=start) - # start after/at `start`, end before/at `end` - | Q(start__gte=start, end__lte=end) - # start before `end`, end after/at `end` - | Q(start__lt=end, end__gte=end) - ) + for index, (current, upcoming) in enumerate(pairwise(timeslots)): + timeslot_entries.append(make_timeslot_entry(timeslot=current)) + + # gap between the timeslots + if current.end != upcoming.start: + timeslot_entries.append( + make_virtual_timeslot_entry(gap_start=current.end, gap_end=upcoming.start) + ) + + # gap after the last timeslot + last_timeslot = timeslots.last() + if last_timeslot.end < timerange_end: + timeslot_entries.append( + make_virtual_timeslot_entry(gap_start=last_timeslot.end, gap_end=timerange_end) + ) + + return timeslot_entries diff --git a/program/views.py b/program/views.py index b70e0d78c41c7103bdcb2ca424ba5b0bc5ff606e..ecfa6794e3e5ef0e7998e4b4d724330fa56a8a2a 100644 --- a/program/views.py +++ b/program/views.py @@ -20,7 +20,6 @@ import logging from datetime import date, datetime, time, timedelta -from itertools import pairwise from textwrap import dedent from django_filters.rest_framework import DjangoFilterBackend @@ -43,7 +42,6 @@ from django.db import IntegrityError from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404 from django.utils import timezone -from django.utils.translation import gettext as _ from program import filters from program.models import ( Category, @@ -89,59 +87,16 @@ from program.serializers import ( TypeSerializer, UserSerializer, ) -from program.services import get_timerange_timeslots, resolve_conflicts +from program.services import ( + get_timerange_timeslot_entries, + make_schedule_entry, + resolve_conflicts, +) from program.utils import get_values, parse_date logger = logging.getLogger(__name__) -def timeslot_entry(*, timeslot: TimeSlot) -> dict: - """return a timeslot entry as a dict""" - - schedule = timeslot.schedule - show = timeslot.schedule.show - playlist_id = timeslot.playlist_id - - title = show_name = f"{show.name} {_('REP')}" if schedule.is_repetition else show.name - # we start and end as timezone naive datetime objects - start = timezone.make_naive(timeslot.start).strftime("%Y-%m-%dT%H:%M:%S") - end = timezone.make_naive(timeslot.end).strftime("%Y-%m-%dT%H:%M:%S") - - return { - "end": end, - "id": timeslot.id, - "playlistId": playlist_id, - # `Timeslot.repetition_of` is a foreign key that can be null - "repetitionOfId": timeslot.repetition_of.id if timeslot.repetition_of else None, - "scheduleDefaultPlaylistId": schedule.default_playlist_id, - "scheduleId": schedule.id, - "showCategories": ", ".join(show.category.values_list("name", flat=True)), - "showDefaultPlaylistId": show.default_playlist_id, - # `Show.funding_category` is a foreign key can be null - "showFundingCategory": show.funding_category.name if show.funding_category_id else "", - "showHosts": ", ".join(show.hosts.values_list("name", flat=True)), - "showId": show.id, - "showLanguages": ", ".join(show.language.values_list("name", flat=True)), - "showMusicFocus": ", ".join(show.music_focus.values_list("name", flat=True)), - "showName": show_name, - "showTopics": ", ".join(show.topic.values_list("name", flat=True)), - # `Show.type` is a foreign key that can be null - "showType": show.type.name if show.type_id else "", - "start": start, - "title": title, - } - - -def gap_entry(*, gap_start: datetime, gap_end: datetime) -> dict: - """return a virtual timeslot to fill the gap in between `gap_start` and `gap_end` as a dict""" - - return { - "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S"), - "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S"), - "virtual": True, - } - - @extend_schema_view( list=extend_schema( summary="List schedule for a specific date.", @@ -168,18 +123,12 @@ class APIDayScheduleViewSet( end = start + timedelta(hours=24) - timeslots = get_timerange_timeslots(start, end).select_related("schedule") - schedule = [] - - for ts in timeslots: - entry = { - "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"), - "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"), - "title": ts.schedule.show.name, - "id": ts.schedule.show.id, - } + include_virtual = request.GET.get("include_virtual") == "true" - schedule.append(entry) + schedule = [ + make_schedule_entry(timeslot_entry=timeslot_entry) + for timeslot_entry in get_timerange_timeslot_entries(start, end, include_virtual) + ] return JsonResponse(schedule, safe=False) @@ -229,33 +178,9 @@ class APIPlayoutViewSet( include_virtual = request.GET.get("include_virtual") == "true" - timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related( - "schedule" - ) - - schedule = [] - - first_timeslot = timeslots.first() + playout = get_timerange_timeslot_entries(schedule_start, schedule_end, include_virtual) - if include_virtual and first_timeslot.start > schedule_start: - schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start)) - - for current, upcoming in pairwise(timeslots): - schedule.append(timeslot_entry(timeslot=current)) - - if include_virtual and current.end != upcoming.start: - schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start)) - - last_timeslot = timeslots.last() - - # we need to append the last timeslot to the schedule to complete it - if last_timeslot: - schedule.append(timeslot_entry(timeslot=last_timeslot)) - - if include_virtual and last_timeslot.end < schedule_end: - schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end)) - - return JsonResponse(schedule, safe=False) + return JsonResponse(playout, safe=False) @extend_schema_view( diff --git a/pyproject.toml b/pyproject.toml index c81c122aab153851f87d3c5c22801de94af74bf5..7b3be6ed39c58c4405954551ee6d98b771c3434b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ django-versatileimagefield = "^3.0" drf-jsonschema-serializer = "^2.0.0" drf-spectacular = "^0.27.1" gunicorn = "^21.2.0" +jsonschema = "^4.22.0" Pillow = "^10.1.0" psycopg2-binary = "^2.9.3" pydot = "^2.0.0"