"""Track Localization Validator functions. The localization data is passed as a separate parameter from the track dict so these validation function can be reused if the localization data isn't yet saved in the track dict. """ from collections import defaultdict from backend.constants import error from backend.constants import services as services_const from backend.constants import track_field as tf from backend.constants import validation from backend.utils import localization as loc_utils from backend.utils.validation import format_error from backend.utils.validation import format_missing_error def validate_track_localization_set( track, localizations, product_meta_language): """Validate all the localizations for a single track. Args: track (dict): Track dictionary localizations (list): List of track localizations product_meta_language (string): meta_language of product Returns: list: localization errors """ error_obj = defaultdict(list) # Max localizations validator max_num_of_localizations_error = _validate_number_of_localizations( localizations) # Chinese Meta language validations meta_language_code = product_meta_language track_language_id = loc_utils.code_to_language_id( meta_language_code) chinese_meta_language_error = _validate_chinese_meta_language( localizations, track_language_id) chinese_localization_error = _validate_chinese_localizations( localizations, track_language_id) localization_errors = [ chinese_meta_language_error, chinese_localization_error, max_num_of_localizations_error] for localization_error in localization_errors: if localization_error: error_obj[tf.LOCALIZATIONS].append(localization_error) return dict(error_obj) def validate_track_localization(track, localization, product_meta_language): """Validate a single localization for track. Args: track (dict): Track dictionary localization (dict): Localization to validate product_meta_language (string): Language code of Product. Returns: list: localization errors """ meta_language_code = product_meta_language meta_language_id = loc_utils.code_to_language_id(meta_language_code) error_obj = _validate_localization_language( meta_language_id, localization.get(tf.LANGUAGE_ID)) error_obj.update(_validate_localized_field_string( field_key=tf.TRACK_NAME, value=track[tf.TRACK_NAME], localized_value=localization.get(tf.TRACK_NAME), max_length=validation.TRACK_NAME_MAX_LENGTH, field_name='track name')) error_obj.update(_validate_artists_localized( track[tf.ARTISTS], localization.get(tf.ARTISTS))) if error_obj: error_obj[tf.LANGUAGE_ID] = localization[tf.LANGUAGE_ID] return error_obj def _validate_localization_language(meta_language_id, language_id): """Validate localization language is specified and has no conflicts.""" error_obj = {} error_message = \ error.VALIDATION_ERROR_SIMILAR_LOCALIZATION.format('product') if meta_language_id and language_id == meta_language_id: error_obj['language'] = format_error( validator='validation_language', message=error_message, error_code=error.INVALID_LANGUAGE ) elif not language_id: error_obj['language'] = format_missing_error( field_name='language', error_code=error.MISSING_LANGUAGE ) return error_obj def _validate_localized_field_string( field_key, value, localized_value, max_length=0, field_name=''): """Validate field (with type string) is properly localized. Args: field_key: Field key value: Value of track master field localized_value: Value of localized field max_length: Maximum length of field field_name: Optional pretty name of field to appear in error msgs """ error_obj = {} if not localized_value: return error_obj field_name = field_name if field_name else field_key if not value: error_obj[field_key] = format_error( validator='cannot_be_localized', message=f'{field_name} cannot be localized since parent field is blank', # noqa error_code=error.CANNOT_BE_LOCALIZED.format(field_name=field_name) ) elif max_length and len(localized_value) > max_length: msg = 'Localized {} cannot be longer than {} characters.'.format( field_name, max_length) error_obj[field_key] = format_error( validator='max_length_exceeded', message=msg, error_code=error.MAX_LENGTH_EXCEEDED.format(field_name=field_name) ) return error_obj def _validate_artists_localized(track_artists, localized_artists): """Validate each artist type in track is properly localized. Localization is considered to be valid when for a specific artist_type: - No artists are localized - All artists are localized Args: track_artists (list): List of track artist dictionaries localized_artists (list): TrackArtist.to_dict() with localized name Returns: dict: errors or empty dict if field is valid """ error_obj = {} artist_map = defaultdict(list) loc_artist_map = defaultdict(list) for artist in track_artists: artist_map[artist[tf.TYPE]].append(artist[tf.TRACK_ARTIST_ID]) for artist in localized_artists: loc_artist_map[artist[tf.TYPE]].append(artist[tf.TRACK_ARTIST_ID]) for artist_type, loc_artists_by_type in loc_artist_map.items(): parent_artists_by_type = artist_map.get(artist_type, []) if sorted(parent_artists_by_type) != sorted(loc_artists_by_type): error_obj[tf.ARTISTS] = format_error( validator='artists_mismatch', message=error.LOCALIZATION_ERROR_LOCALIZED_VALUES_MISMATCH, error_code=error.LOCALIZED_ARTISTS_MISMATCH ) break return error_obj def _validate_all_artists_localized(track_artists, localized_artists): """Validate each artist in track has a localization. Might be needed for Chinese localization validation. Localization is considered to be valid only if all track artists have corresponding localizations. Args: track_artists (list): List of track artist dictionaries localized_artists (list): TrackArtist.to_dict() with localized name Returns: dict: errors or empty dict if field is valid """ error_obj = {} existing_artist_ids = sorted( artist[tf.TRACK_ARTIST_ID] for artist in track_artists) localized_artist_ids = sorted( artist[tf.TRACK_ARTIST_ID] for artist in localized_artists) if not existing_artist_ids == localized_artist_ids: error_obj[tf.ARTISTS] = format_error( validator='artists_mismatch', message=error.LOCALIZATION_ERROR_LOCALIZED_VALUES_MISMATCH, error_code=error.LOCALIZED_ARTISTS_MISMATCH ) return error_obj def _validate_chinese_meta_language(localizations, track_language_id): """Complementary language must be localized when meta_language is Chinese. If track meta language is Chinese(Simplified/Traditional), complementary language in this pair should be localized. Args: localizations (list): List of track localizations track_language_id (int): Releases.meta_language_code converted to language_id Returns: dict: errors or empty dict if field is valid """ sc = services_const required_localization_lang_ids = _get_required_localization_language_ids() id_to_language = { sc.CHINESE_TRADITIONAL_LANG_ID: sc.CHINESE_TRADITIONAL, sc.CHINESE_SIMPLIFIED_LANG_ID: sc.CHINESE_SIMPLIFIED } if track_language_id not in required_localization_lang_ids: return {} localized_lang_ids = [ localization[tf.LANGUAGE_ID] for localization in localizations] required_lang_id = required_localization_lang_ids[track_language_id] if required_lang_id in localized_lang_ids: return {} error_message = \ error.VALIDATION_ERROR_CHINESE_LOCALIZATION_REQUIRED.format( track_language=id_to_language[required_lang_id], product_meta_language=id_to_language[track_language_id]) error_dict = format_error( validator=error.REQUIRED_CODE, message=error_message, error_code=error.MISSING_CHINESE_LOCALIZATION ) return error_dict def _validate_chinese_localizations(localizations, track_language_id): """Both Simplified and Traditional Chinese localizations should exist. If either of localizations exists, then both of them should be present. Args: localizations (list): List of track localizations track_language_id (int): Track.meta_language_code converted to language_id Returns: dict: errors or empty dict if field is valid """ required_lang_ids = _get_required_localization_language_ids() if track_language_id in required_lang_ids: return {} # this case is for another validator existing_localization_lang_ids = [ localization[tf.LANGUAGE_ID] for localization in localizations] chinese_lang_localizations_exist = [ required_lang_id in existing_localization_lang_ids for required_lang_id in required_lang_ids] if any(chinese_lang_localizations_exist): if not all(chinese_lang_localizations_exist): return format_error( validator=error.REQUIRED_CODE, message=error.VALIDATION_ERROR_BOTH_CHINESE_LANGS_REQUIRED, error_code=error.MISSING_BOTH_CHINESE_LANGUAGES ) return {} def _validate_number_of_localizations(localizations): """Check if list of localizations exceeds maximum count. Args: localizations (list): List of track localizations Returns: dict: errors or empty dict if field is valid """ if len(localizations) > validation.MAX_LOCALIZATIONS_PER_TRACK: return format_error( validator=error.REQUIRED_CODE, message=error.LOCALIZATION_ERROR_TOO_MANY_LOCALIZATIONS, error_code=error.TOO_MANY_LOCALIZATIONS ) return {} def _get_required_localization_language_ids(): """Get mapping of existing and required localization language ids. Returns: dict: {language_id: corresponding required language id} """ sc = services_const required_localization_lang_ids = { sc.CHINESE_TRADITIONAL_LANG_ID: sc.CHINESE_SIMPLIFIED_LANG_ID, sc.CHINESE_SIMPLIFIED_LANG_ID: sc.CHINESE_TRADITIONAL_LANG_ID } return required_localization_lang_ids