From 9b3207b8a474f76b675c57ff9dc7c5c9f17adf8d Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 14 Sep 2022 12:48:08 +0200 Subject: [PATCH] feat: move timezone preference to config file (#2096) BREAKING CHANGE: The timezone preference moved to the configuration file. --- .../legacy/migrations/sql/data.sql | 1 - docker/config.yml | 5 +++++ docker/example/config.yml | 5 +++++ docs/admin-manual/setup/configuration.md | 5 +++++ docs/releases/unreleased.md | 14 ++++++++++++++ installer/config.yml | 5 +++++ legacy/application/configs/conf.php | 5 +++++ .../controllers/PreferenceController.php | 1 - .../application/controllers/SetupController.php | 1 - legacy/application/forms/GeneralPreferences.php | 1 + legacy/application/models/Preference.php | 13 +------------ shared/libretime_shared/config/_models.py | 17 +++++++++++++++++ shared/requirements.txt | 1 + shared/setup.py | 2 ++ shared/tests/config/models_test.py | 12 ++++++++++++ 15 files changed, 73 insertions(+), 15 deletions(-) diff --git a/api/libretime_api/legacy/migrations/sql/data.sql b/api/libretime_api/legacy/migrations/sql/data.sql index d0a272a5d..775d8d4c4 100644 --- a/api/libretime_api/legacy/migrations/sql/data.sql +++ b/api/libretime_api/legacy/migrations/sql/data.sql @@ -6,7 +6,6 @@ ANALYZE "cc_pref"; INSERT INTO cc_live_log ("state", "start_time") VALUES ('S', now() at time zone 'UTC'); INSERT INTO cc_pref ("keystr", "valstr") VALUES ('import_timestamp', '0'); -INSERT INTO cc_pref ("keystr", "valstr") VALUES ('timezone', 'UTC'); INSERT INTO cc_pref ("keystr", "valstr") VALUES ('off_air_meta', 'LibreTime - offline'); INSERT INTO cc_pref ("keystr", "valstr") VALUES ('enable_replay_gain', 1); INSERT INTO cc_pref ("keystr", "valstr") VALUES ('locale', 'en_US'); diff --git a/docker/config.yml b/docker/config.yml index ed21a46cb..c477f8b67 100644 --- a/docker/config.yml +++ b/docker/config.yml @@ -13,6 +13,11 @@ general: # > default is [] allowed_cors_origins: [] + # The server timezone, should be a lookup key in the IANA time zone database, + # for example Europe/Berlin. + # > default is UTC + timezone: UTC + # How many hours ahead Playout should cache scheduled media files. # > default is 1 cache_ahead_hours: 1 diff --git a/docker/example/config.yml b/docker/example/config.yml index fbd3d4e8f..3a84cdb01 100644 --- a/docker/example/config.yml +++ b/docker/example/config.yml @@ -13,6 +13,11 @@ general: # > default is [] allowed_cors_origins: [] + # The server timezone, should be a lookup key in the IANA time zone database, + # for example Europe/Berlin. + # > default is UTC + timezone: UTC + # How many hours ahead Playout should cache scheduled media files. # > default is 1 cache_ahead_hours: 1 diff --git a/docs/admin-manual/setup/configuration.md b/docs/admin-manual/setup/configuration.md index 48eb678b5..cf82faf93 100644 --- a/docs/admin-manual/setup/configuration.md +++ b/docs/admin-manual/setup/configuration.md @@ -29,6 +29,11 @@ general: # > default is [] allowed_cors_origins: [] + # The server timezone, should be a lookup key in the IANA time zone database, + # for example Europe/Berlin. + # > default is UTC + timezone: UTC + # How many hours ahead Playout should cache scheduled media files. # > default is 1 cache_ahead_hours: 1 diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 0699a92b2..aff30fef5 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -74,6 +74,20 @@ sudo -u libretime libretime-api dbshell --command=" ::: +### Timezone configuration + +The timezone preference moved from the database to the [configuration](../admin-manual/setup/configuration.md#general) file. Make sure to save your existing timezone preference to the configuration file. + +:::info + +To prevent accidental data loss during upgrade, the timezone preference will only be removed from the database in future releases. You can view the data using the following commands: + +```bash +sudo -u libretime libretime-api dbshell --command="SELECT * FROM cc_pref WHERE keystr = 'timezone'"; +``` + +::: + ## :arrow_up: Upgrading ### Worker python package and service diff --git a/installer/config.yml b/installer/config.yml index a39d6def3..aedeefcac 100644 --- a/installer/config.yml +++ b/installer/config.yml @@ -13,6 +13,11 @@ general: # > default is [] allowed_cors_origins: [] + # The server timezone, should be a lookup key in the IANA time zone database, + # for example Europe/Berlin. + # > default is UTC + timezone: UTC + # How many hours ahead Playout should cache scheduled media files. # > default is 1 cache_ahead_hours: 1 diff --git a/legacy/application/configs/conf.php b/legacy/application/configs/conf.php index ca77353bf..7e0856ce6 100644 --- a/legacy/application/configs/conf.php +++ b/legacy/application/configs/conf.php @@ -36,6 +36,11 @@ class Schema implements ConfigurationInterface /**/->scalarNode('public_url')->cannotBeEmpty()->end() /**/->scalarNode('api_key')->cannotBeEmpty()->end() /**/->arrayNode('allowed_cors_origins')->scalarPrototype()->defaultValue([])->end()->end() + /**/->scalarNode('timezone')->cannotBeEmpty()->defaultValue("UTC") + /* */->validate()->ifNotInArray(DateTimeZone::listIdentifiers()) + /* */->thenInvalid('invalid general.timezone %s') + /* */->end() + /**/->end() /**/->scalarNode('dev_env')->defaultValue('production')->end() /**/->scalarNode('auth')->defaultValue('local')->end() /**/->integerNode('cache_ahead_hours')->defaultValue(1)->end() diff --git a/legacy/application/controllers/PreferenceController.php b/legacy/application/controllers/PreferenceController.php index 278340ca0..caa4a3910 100644 --- a/legacy/application/controllers/PreferenceController.php +++ b/legacy/application/controllers/PreferenceController.php @@ -50,7 +50,6 @@ class PreferenceController extends Zend_Controller_Action Application_Model_Preference::SetAllow3rdPartyApi($values['thirdPartyApi']); Application_Model_Preference::SetAllowedCorsUrls($values['allowedCorsUrls']); Application_Model_Preference::SetDefaultLocale($values['locale']); - Application_Model_Preference::SetDefaultTimezone($values['timezone']); Application_Model_Preference::SetWeekStartDay($values['weekStartDay']); Application_Model_Preference::setRadioPageDisplayLoginButton($values['radioPageLoginButton']); Application_Model_Preference::SetFeaturePreviewMode($values['featurePreviewMode']); diff --git a/legacy/application/controllers/SetupController.php b/legacy/application/controllers/SetupController.php index 97d2a7d17..cc7085b0c 100644 --- a/legacy/application/controllers/SetupController.php +++ b/legacy/application/controllers/SetupController.php @@ -25,7 +25,6 @@ class SetupController extends Zend_Controller_Action $currentUserId = $currentUser->getDbId(); Application_Model_Preference::SetUserTimezone($formData['setup_timezone'], $currentUserId); - Application_Model_Preference::SetDefaultTimezone($formData['setup_timezone']); Application_Model_Preference::SetUserLocale($formData['setup_language'], $currentUserId); Application_Model_Preference::SetDefaultLocale($formData['setup_language']); diff --git a/legacy/application/forms/GeneralPreferences.php b/legacy/application/forms/GeneralPreferences.php index 68d19406b..5027aae47 100644 --- a/legacy/application/forms/GeneralPreferences.php +++ b/legacy/application/forms/GeneralPreferences.php @@ -187,6 +187,7 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm // Form Element for setting the Timezone $timezone = new Zend_Form_Element_Select('timezone'); $timezone->setLabel(_('Station Timezone')); + $timezone->setAttrib('disabled', 'true'); $timezone->setMultiOptions(Application_Common_Timezone::getTimezones()); $timezone->setValue(Application_Model_Preference::GetDefaultTimezone()); $this->addElement($timezone); diff --git a/legacy/application/models/Preference.php b/legacy/application/models/Preference.php index 7ca68007c..92837c2f9 100644 --- a/legacy/application/models/Preference.php +++ b/legacy/application/models/Preference.php @@ -536,21 +536,10 @@ class Application_Model_Preference return sprintf(_('Powered by %s'), SAAS_PRODUCT_BRANDING_NAME); } - // Sets station default timezone (from preferences) - public static function SetDefaultTimezone($timezone) - { - self::setValue('timezone', $timezone); - } - // Returns station default timezone (from preferences) public static function GetDefaultTimezone() { - $stationTimezone = self::getValue('timezone'); - if (is_null($stationTimezone) || $stationTimezone == '') { - $stationTimezone = 'UTC'; - } - - return $stationTimezone; + return Config::get('general.timezone'); } public static function SetUserTimezone($timezone = null) diff --git a/shared/libretime_shared/config/_models.py b/shared/libretime_shared/config/_models.py index e2e731955..08f96fee0 100644 --- a/shared/libretime_shared/config/_models.py +++ b/shared/libretime_shared/config/_models.py @@ -5,6 +5,11 @@ from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Union from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, validator from typing_extensions import Annotated, Literal +try: + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +except ImportError: + from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError # type: ignore + if TYPE_CHECKING: from pydantic.typing import AnyClassMethod @@ -38,9 +43,21 @@ class GeneralConfig(BaseModel): public_url: AnyHttpUrl api_key: str + timezone: str = "UTC" + # Validators _public_url_no_trailing_slash = no_trailing_slash_validator("public_url") + @validator("timezone") + @classmethod + def _validate_timezone(cls, value: str) -> str: + try: + ZoneInfo(value) + except ZoneInfoNotFoundError as exception: + raise ValueError(f"invalid timezone '{value}'") from exception + + return value + # StorageConfig ######################################################################################## diff --git a/shared/requirements.txt b/shared/requirements.txt index a8ffe7b2c..e96e82b56 100644 --- a/shared/requirements.txt +++ b/shared/requirements.txt @@ -1,5 +1,6 @@ # Please do not edit this file, edit the setup.py file! # This file is auto-generated by tools/extract_requirements.py. +backports.zoneinfo>=0.2.1,<0.3;python_version<'3.9' click~=8.0.4 loguru==0.6.0 pydantic>=1.7.4,<1.11 diff --git a/shared/setup.py b/shared/setup.py index 8400f26d9..5dfe43600 100644 --- a/shared/setup.py +++ b/shared/setup.py @@ -10,6 +10,7 @@ setup( packages=find_packages(exclude=["*tests*", "*fixtures*"]), package_data={"": ["py.typed"]}, install_requires=[ + "backports.zoneinfo>=0.2.1,<0.3;python_version<'3.9'", "click~=8.0.4", "loguru==0.6.0", "pydantic>=1.7.4,<1.11", @@ -17,6 +18,7 @@ setup( ], extras_require={ "dev": [ + "types-backports", "types-pyyaml", ], }, diff --git a/shared/tests/config/models_test.py b/shared/tests/config/models_test.py index 44541909d..37731e992 100644 --- a/shared/tests/config/models_test.py +++ b/shared/tests/config/models_test.py @@ -6,10 +6,22 @@ from libretime_shared.config._models import ( AudioMP3, AudioOGG, AudioOpus, + GeneralConfig, StreamConfig, ) +def test_general_config_timezone(): + defaults = { + "public_url": "http://localhost:8080", + "api_key": "api_key", + } + GeneralConfig(**defaults, timezone="UTC") + GeneralConfig(**defaults, timezone="Europe/Berlin") + with pytest.raises(ValidationError): + GeneralConfig(**defaults, timezone="Europe/Invalid") + + @pytest.mark.parametrize( "audio", [