"""Logic class product's localization.""" from oto import response from product.connectors import mysql from product.constants import error from product.logic import product as product_logic from product.models import itunes_languages as language_model from product.models import language from product.models import ows_product_digital as product_digital_model from product.models import product_localization as localization_model from product.models import release_artist as release_artist_model from product.models import \ release_artist_localized_metadata as artist_localized_model from product.models import \ release_localized_metadata as release_localized_model from product.models import track_artist_localized_metadata from product.models import track_localized_metadata def validate(product_id, language_id): """Validate product and language ids by DB lookup. Args: product_id (int): product id. language_id (int): language id. Returns: Response: The response object with failure/True for success. """ product = product_logic.get_product_by_product_id(product_id) if not product: return product language = get_itunes_language_by_id(language_id) if not language: return language return response.Response(message='success') def delete(product_id, language_id, legacy_flag=False): """Delete localization data. Args: product_id (int): product id. language_id (int): language id. legacy_flag (bool): flag indicating if we should get data from legacy system(mysql tables) or from the new dynamodb tables. Returns: Response: The response object with message of success/failure. """ if not legacy_flag: # Temporary fix for MOV-2363 is to remove the language validation # when legacy=false (film for now) product = product_logic.get_product_by_product_id(product_id) if not product: return product return localization_model.delete(product_id, language_id) # (DB lookup validations) is_valid = validate(product_id, language_id) if not is_valid: return is_valid # legacy release_response = release_localized_model.delete_metadata( product_id, language_id) release_artist_ids = release_artist_model.get_artist_ids_for_product( product_id) artist_response = artist_localized_model.delete_all_metadata( release_artist_ids.message, language_id) # if both delete fails report error, as we can have no data for either # and it will still be valid. if not release_response and not artist_response: return response.create_not_found_response( message=release_response.errors['message'] + artist_response.errors['message']) else: return response.Response( message='Successfully deleted release and artist localizations.') def get_itunes_language_by_id(language_id): """Return language information from language_id. Args: language_id (int): language id. Returns: Response: The response object with message as actual data dict. """ return language_model.get_language_by_id(language_id) def get_all_itunes_languages(): """Return all languages and its info for localization. Returns: Response: The response object with message as actual data dict. """ itunes_languages = language_model.get_all_itunes_languages() if not itunes_languages: return response.create_fatal_response( message='failed to fetch iTunes langauges') orchard_languages = language.get_languages() if not orchard_languages: return response.create_fatal_response( message='failed to fetch Orchard languages') return response.Response(message=_merge_languages( itunes_languages.message, orchard_languages.message)) def get_localization_by_product_id(product_id, legacy_flag=False): """Get localization data by product's id. Args: product_id (int): product id. legacy_flag (bool): flag indicating if we should get data from legacy system(mysql tables) or from the new dynamodb tables. Returns: Response: The response object with valid data/failure message. """ product = product_logic.get_product_by_product_id(product_id) if not product: return product if not legacy_flag: return localization_model.get_localization_data(product_id) # with legacy release_metadata = release_localized_model.get_metadata_by_id(product_id) if not release_metadata: return release_metadata artist_metadata = artist_localized_model.get_artist_metadata_by_product( product_id) if not artist_metadata: return artist_metadata return _format_legacy_get_response( product_id, release_metadata.message, artist_metadata.message) def _format_legacy_get_response(product_id, release_metadata, artist_metadata): """Format GET response data for legacy call. Since we get data from 2 diff models release and artist, this fn merges data from both and group by language_id so we have one language item and all localization for that language in it. Args: product_id (int): product id. release_metadata (list): release metadata list. artist_metadata (dict): formatted release artist metadata list. Returns: Response: The response object that has items of release and artist's localized metadata along with pagination details. Sample response is in spec raml. """ return_list = release_metadata[:] for language_id, each_artist in artist_metadata.items(): added = False for row in return_list: if language_id == row['language_id']: row.update(each_artist) # append to existing language added = True break if not added: # add a new entry for this language each_artist.update({'language_id': language_id}) return_list.append(each_artist) formatted_response = { 'product_id': product_id, 'items': return_list, 'pagination': { 'type': 'standard', 'offset': 0, 'limit': -1, 'total_records': len(return_list)}} return response.Response(message=formatted_response) def get_track_localization(tuids): """Get track's localization data by tuids. Args: tuids (list): List of track ids.. Returns: Response: The response object with valid data/failure message. """ with mysql.db_session(read_only=True) as session: track_metadata = track_localized_metadata.get_tracks_localizations( tuids, session) if not track_metadata: return track_metadata artist_metadata = track_artist_localized_metadata.get_metadata_by_id( tuids, session) if not artist_metadata: return artist_metadata return _format_track_get_response( track_metadata.message, artist_metadata.message) def _format_track_get_response(track_metadata, artist_metadata): """Format GET response data for track localization. Since we get data from 2 diff models track_localization and track_artist_localization, this fn merges data from both and group by language_id and tuid. Args: track_metadata (list): track metadata list. artist_metadata (dict): formatted track artist metadata list. Returns: Response: The response object that has items of track and artist's localized metadata along with pagination details. """ return_list = [] for language_id, tracks in track_metadata.items(): for tuid, track_data in tracks.items(): if (language_id in artist_metadata and tuid in artist_metadata[language_id]): track_data.update({ 'artists': artist_metadata[language_id][tuid]}) else: track_data.update({'artists': {}}) return_list.append(track_data) formatted_response = { 'items': return_list, 'pagination': { 'type': 'none', 'total_records': len(return_list)}} return response.Response(message=formatted_response) def create(product_id, language_id, data, legacy_flag=False): """Create a new localization data. Args: product_id (int): product id. language_id (int): language id. data (dict): translated fields. legacy_flag (bool): flag indicating if we should get data from legacy system(mysql tables) or from the new dynamodb tables. Returns: Response: The response object with message as latest object state. """ if not legacy_flag: # Temporary fix for MOV-2363 is to remove the language validation # when legacy=false (film for now) product = product_logic.get_product_by_product_id(product_id) if not product: return product return localization_model.create_update(product_id, language_id, data) # (DB lookup validations for ids) is_valid = validate(product_id, language_id) if not is_valid: return is_valid # legacy. response_data = {} post_release_artists = [] to_delete_artist_ids = [] release_response = release_localized_model.insert_metadata( product_id, language_id, data) if not release_response: return release_response response_data.update(release_response.message) for artist_role, artist_data in data.items(): # release and artist data is in same dict, so skip release. if artist_role in ['product_name', 'language_id', 'delivered_version']: continue artist_response = artist_localized_model.insert_metadata( language_id, artist_data) if not artist_response: # if artist role is provided, it must have valid data. return artist_response post_release_artists.extend(artist_data.keys()) response_data.update({artist_role: artist_response.message}) release_artist_ids = release_artist_model.get_artist_ids_for_product( product_id) # delete release artist post_release_artists = list(map(int, post_release_artists)) if release_artist_ids: to_delete_artist_ids = [ artist for artist in release_artist_ids.message if artist not in post_release_artists ] if to_delete_artist_ids: artist_localized_model.delete_all_metadata( to_delete_artist_ids, language_id) return response.Response(message=response_data) def _track_localization_has_duplicates(data): """Check if the data has duplicate tuid+language_id pair. Args: data (dict): items of localized tracks. Returns: boolean: True if there are duplicate records. """ new_data = set( (old_item['tuid'], old_item['language_id']) for old_item in data) return len(data) != len(new_data) @mysql.db_session_wrap def update_multiple_localizations(data, session): """Update multiple track's localization for several languages. Args: data (dict): items of localized tracks. session(Object): DB session instance. Returns: Response: The response object with message as latest object state. """ # validate that we don't get dupe lang+tuid pair in items. if _track_localization_has_duplicates(data['items']): return response.create_error_response( error.ERROR_CODE_INVALID_DATA, 'Duplicate data sent for save track localization') for each_item in data['items']: each_response = update_track_localization( tuid=each_item['tuid'], language_id=each_item['language_id'], data=each_item, session=session) if not each_response: session.rollback() return each_response return response.Response(message=data) @mysql.db_session_wrap def update_track_localization(tuid, language_id, data, session): """Update track localization data. Args: tuid (int): track id. language_id (int): language id. data (dict): translated fields as {artist_id : name, ...}. session(Object): DB session instance. Returns: Response: The response object with message as latest object state. """ language_response = language_model.get_language_by_id(language_id) if not language_response: return language_response # no tuid validation as no track model # save track level localizations release_response = track_localized_metadata.update_metadata( tuid=tuid, language_id=language_id, data=data, session=session) return release_response def delete_track_localization(tuids, language_id=None): """Delete multiple tracks localization by tuids and language if provided. Args: tuids (list): List of track ids. language_id (int): language id. Optional parameter. Returns: Response: The response object with valid data/failure message. """ if language_id: language_response = language_model.get_language_by_id(language_id) if not language_response: return language_response return track_localized_metadata.delete_track_metadata(tuids, language_id) def _merge_languages(itunes_languages, orchard_languages): """Merge language codes from language models into itunes_languages models. Args: itunes_languages (dict): response payload of itunes languages. orchard_languages (dict): response payload of Orchard languages. Returns: dict: itunes_languages with an extra 'meta_language_code' property. """ orchard_language_map = {} # map the 2-char ISO codes from languages to their 3-char versions for orchard_language in orchard_languages['items']: orchard_iso_code = orchard_language.get('iso_code') orchard_language_code = orchard_language.get('code') orchard_language_map[orchard_iso_code] = orchard_language_code for itunes_language in itunes_languages['items']: itunes_language['meta_language_code'] = _get_orchard_language_code( itunes_language, orchard_language_map) return itunes_languages def _get_orchard_language_code(itunes_language, orchard_language_map): """Determine which Orchard language code to use for an iTunes language. Args: itunes_language (dict): the iTunes language. orchard_language_map (dict): mapping of iTunes language codes. Returns: str: Orchard language code that corresponds to the iTunes language. """ # Hard-coded edge cases for Chinese dialects that do not map by ISO code if (itunes_language.get('language_code') == 'cmn-Hant' or itunes_language.get('language_code') == 'zh-hant'): return 'cmn-Hant' if (itunes_language.get('language_code') == 'cmn-Hans' or itunes_language.get('language_code') == 'zh-hans'): return 'CHI' return orchard_language_map.get( itunes_language.get('language_code').upper()) def copy_localization( source_product_id, destination_product_id, legacy_flag=False): """Copy all the localization data from one product to another. Args: source_product_id (int): product id of product with localizations to copy from. destination_product_id (int): product id of product with localizations to copy to. legacy_flag (bool): flag indicating if we should get data from legacy system(mysql tables) or from the new dynamodb tables. Returns: Response: The response object with message or error description. """ source_localizations_response = get_localization_by_product_id( source_product_id, legacy_flag) if not source_localizations_response: return source_localizations_response source_localizations = source_localizations_response.message source_to_dest_ids_response = _get_source_to_copied_artists_mapping( source_product_id, destination_product_id) if not source_to_dest_ids_response: return source_to_dest_ids_response source_to_dest_ids_mapping = source_to_dest_ids_response.message for localization_data in source_localizations.get('items'): language_id = localization_data.pop('language_id') for field, value in localization_data.items(): if field in ('product_name', 'delivered_version'): continue value = { str(source_to_dest_ids_mapping[int(source_id)]): local for source_id, local in value.items()} localization_data[field] = value localization_create_response = create( destination_product_id, language_id=language_id, data=localization_data, legacy_flag=legacy_flag) if not localization_create_response: return localization_create_response return response.Response() def _get_source_to_copied_artists_mapping( source_product_id, copied_product_id): """Create mapping of source artists ids and copied artists ids. Args: source_product_id (int): product id of source product where artists already have theirs localizations. copied_product_id (int): product id of copied product where we want to replicate artists localizations. Returns: response.Response object with artists mapping like {'source_artist_id': 'related_copied_artist_id'} or an error. """ source_artists_response = product_digital_model.get_digital_product( source_product_id) if not source_artists_response: return source_artists_response source_artists = source_artists_response.message.get('product_artists') copied_artists_response = product_digital_model.get_digital_product( copied_product_id) if not copied_artists_response: return copied_artists_response copied_artists = copied_artists_response.message.get('product_artists') source_to_dist_ids_mapping = {} for source_artist in source_artists: source_id = source_artist['id'] source_name = source_artist['name'] for pos, copy_artist in enumerate(copied_artists): dest_id = copy_artist['id'] dest_name = copy_artist['name'] if dest_name != source_name: continue source_to_dist_ids_mapping[source_id] = dest_id copied_artists.pop(pos) break return response.Response(source_to_dist_ids_mapping)