"""Model representing a digital audio product.""" from oto import response from product_digital import features from product_digital.constants import error from product_digital.constants import product as product_model from product_digital.constants import product_statuses from product_digital.models import catalog_transfer from product_digital.models import mkt_priority from product_digital.models import ows_assets from product_digital.models import ows_product_review from product_digital.models import ows_product_workflow from product_digital.models import ows_timed_release from product_digital.models import ows_track from product_digital.models import project as project_model from product_digital.models import release from product_digital.models import release_approval_queue from product_digital.models import release_artist from product_digital.models import release_grid from product_digital.models import release_logging from product_digital.models import release_manual_adjustment from product_digital.models import release_status as release_status_model from product_digital.models import release_subgenre from product_digital.models import release_subtitles from product_digital.models import track _product_release_mapping = { 'product_name': 'release_name', 'previewable': 'itunes_previewable', 'imprint': 'label', 'format': 'release_format'} PROPERTIES_FOR_CREATE = [ 'artist_id', 'c_line', 'description', 'display_upc', 'distribution_format_id', 'genre_id', 'itunes_previewable', 'label', 'manufacturer_upc', 'meta_language', 'p_line', 'preorder_date', 'product_code', 'product_type_id', 'delivered_version', 'project_id', 'release_date', 'release_format', 'release_name', 'release_status', 'sale_start_date', 'special_instructions', 'subaccount_id', 'upc', 'vendor_catalog_number', 'vendor_release_identifier', 'version', 'not_for_distribution', ] PROPERTIES_FOR_UPDATE = [ 'artist_id', 'c_line', 'format', 'description', 'genre_id', 'previewable', 'imprint', 'manufacturer_upc', 'meta_language', 'p_line', 'preorder_date', 'product_code', 'delivered_version', 'release_date', 'release_status', 'product_name', 'sale_start_date', 'special_instructions', 'vendor_release_identifier', 'version', 'project_code', 'project_id', ] def create(audio_data): """Create an audio product. Args: audio_data (dict): audio product information Returns: response.Response: response representing the outcome of the create. """ release_data = clean_audio_product_to_release_data( audio_data, PROPERTIES_FOR_CREATE) release_response = release.create(release_data) if not release_response: return release_response new_release = release_response.message new_product = _format_product(release_data=new_release) return response.Response(status=201, message=new_product) def update(product_id, audio_data, orchard_user_id=''): """Update an existing product with the given data. Args: product_id (int): id of the product to update. audio_data (dict): properties to update. orchard_user_id (str): orchard user id. Returns: response.Response: response representing the outcome of the update. """ is_product_checked_out_by_another_user = \ ows_product_workflow.is_product_checked_out_by_another_user( product_id=product_id, requesting_orchard_user_id=orchard_user_id ) is_updating_project_id = audio_data.get('project_id') if is_product_checked_out_by_another_user and not is_updating_project_id: return response.create_error_response( code=error.INTERNAL_ERROR, message=error.ERROR_MESSAGE_PRODUCT_ALREADY_CHECKED_OUT, status=400 ) genre_id = False is_genre_changed = False is_soundtrack_subgenre_changed = False subgenre_id = None genre_id = audio_data.get('genre_id') subgenre_id = audio_data.get('subgenre_id') if genre_id: is_genre_changed = _is_genre_change(product_id, genre_id) if subgenre_id: is_soundtrack_subgenre_changed = _is_soundtrack_subgenre_change( product_id, subgenre_id, orchard_user_id ) if is_genre_changed or is_soundtrack_subgenre_changed: request_data = _format_request_data(audio_data) ows_track_response = ows_track.change_genre( product_id, request_data, orchard_user_id ) if not ows_track_response: return ows_track_response release_response = _update_release_data(product_id, audio_data, orchard_user_id) if release_response.status in (403, 404): return release_response if not release_response: return response.create_fatal_response('failed to save product') product = release_response.message project_id = product['project_id'] project_response = project_model.get_project_by_id(project_id) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] # we need the upc in order to save subgenre and artists upc = product['upc'] subgenre_response = _save_release_subgenre(product_id, upc, audio_data) if not subgenre_response: return response.create_fatal_response('failed to save subgenre') subgenre_id = audio_data.get('subgenre_id') artists_response = _save_release_artists(product_id, upc, audio_data) release_artists = artists_response.message if 'subgenre_id' not in audio_data: subgenre = release_subgenre.get_release_subgenre_by_release_id( product_id) if subgenre: subgenre_id = subgenre.message.get('subgenre_id') if not release_artists: artists_response = release_artist.get_release_artists_by_release_id( product_id) product_highlights = None audio_product = _compose_audio_product( product=product, artists_response=artists_response, subgenre_id=subgenre_id, product_highlights=product_highlights, product_id=product_id, vendor_id=vendor_id, orchard_user_id=orchard_user_id, ) if not audio_product: return audio_product return response.Response(message=audio_product) def format_submit_product(product, workflow_response, orchard_user_id=''): """Build product data for submit product response. Args: product (release.Release): properties to update. workflow_response (response.Response): product workflow response. orchard_user_id (str): ALW or OA user id needed by downstream services to evaluate feature flags. Returns: response.Response: response representing the outcome of the update. """ subgenre_id = None subgenre = release_subgenre.get_release_subgenre_by_release_id( product.release_id) if subgenre: subgenre_id = subgenre.message.get('subgenre_id') artists_response = release_artist.get_release_artists_by_release_id( product.release_id) product_highlights = None corrections = None release_approval = None release_approval_status = None if workflow_response: corrections = workflow_response.message.get('latest_correction') if corrections and corrections.get('status') == product_statuses.SUBMITTED: corrections['items'] = _format_corrections( product.release_id, corrections['items'], orchard_user_id) release_approval = workflow_response.message.get('release_approval') release_approval_status = workflow_response.message.get('release_approval')['status'] audio_product = _format_product( release_data=product.to_dict(), subgenre_id=subgenre_id, release_artists=artists_response.message, product_highlights=product_highlights, release_approval=release_approval, release_status=product_statuses.TRANSFER_TO_CONTENT, rejections=None, corrections=corrections, ec_and_ar=False ) audio_product['release_approval_status'] = release_approval_status return response.Response(message=audio_product) def _format_corrections(release_id, correction_items, orchard_user_id): """Format response data from release_correction_detail. Args: release_id (int): the product id. correction_items (list): product workflow response. orchard_user_id (str): ALW or OA user id needed by ows-assets to evaluate feature flags. """ for index, value in enumerate(correction_items): if value.get('field_name') == 'coverart': # Special case: Replace key value of coverart with the image asset URL # in error correction. image_response = ows_assets.get_correction_image(release_id, orchard_user_id) if image_response.status == 200: value['key_value'] = image_response.message['image'] else: value['key_value'] = None correction_items[index] = value elif value.get('field_name') in ['release_name', 'product_code']: value['key_value'] = str(value.get('key_value')) return correction_items def get_product(product_id, orchard_user_id=None): """Return basic information for an audio product. Args: product_id (int): product_id for the audio product to be obtained. orchard_user_id (str): ALW or OA user id needed by downstream services to evaluate feature flags. Returns: Response: Response containing the product information. """ release_response = release.get_release(product_id) if not release_response: return release_response product = release_response.message project_id = product['project_id'] project_response = project_model.get_project_by_id(project_id) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] artists_response = release_artist.get_release_artists_by_release_id(product_id) if not artists_response: return artists_response subgenre = release_subgenre.get_release_subgenre_by_release_id(product_id) subgenre_id = subgenre.message.get('subgenre_id') if subgenre else None catalog_transfer_response = catalog_transfer.get_catalog_transfer(product_id) catalog_transfer_info = None if catalog_transfer_response: catalog_transfer_data = catalog_transfer_response.message catalog_transfer_info = { 'original_distributor': catalog_transfer_data['original_distributor'], 'same_isrc_upc': catalog_transfer_data['same_isrc_upc'] == 'Y', } product_highlights = None audio_product = _compose_audio_product( product=product, artists_response=artists_response, subgenre_id=subgenre_id, product_highlights=product_highlights, product_id=product_id, vendor_id=vendor_id, orchard_user_id=orchard_user_id ) if not audio_product: return audio_product audio_product['catalog_transfer_info'] = catalog_transfer_info return response.Response(message=audio_product) def get_products(product_ids, orchard_user_id=None): """Return basic information for a list of audio products. Args: product_ids (list of int): List of product IDs for the audio products to be obtained. orchard_user_id (str): ALW or OA user ID needed by downstream services to evaluate feature flags. Returns: Response: Response containing the list of product information. """ products = [] for product_id in product_ids: release_response = release.get_release(product_id) product = release_response.message project_id = product['project_id'] project_response = project_model.get_project_by_id(project_id, True) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] artists_response = release_artist.get_release_artists_by_release_id(product_id) if not artists_response: return artists_response subgenre_response = release_subgenre.get_release_subgenre_by_release_id(product_id) subgenre_id = subgenre_response.message.get('subgenre_id') \ if subgenre_response and subgenre_response.status == 200 else None product_highlights = None audio_product = _compose_audio_product( product=product, artists_response=artists_response, subgenre_id=subgenre_id, product_highlights=product_highlights, product_id=product_id, vendor_id=vendor_id, orchard_user_id=orchard_user_id ) products.append(audio_product) response_message = products return response.Response(message=response_message) def _compose_audio_product( product, artists_response, subgenre_id, product_highlights, product_id, vendor_id, orchard_user_id='', ): release_status = product['release_status'] distribution_format_id = product['distribution_format_id'] approval_message = { 'release_correction_id': None, 'status': None, 'release_approval_id': None } rejections_ff_enabled = features.is_rejections_from_content_review_enabled( vendor_id, distribution_format_id) if rejections_ff_enabled: review_response = ows_product_review.get_status(product_id) if review_response and review_response.status == 200: # we emulate some fields from ows_product_workflow response approval_message = review_response.message approval_message['release_correction_id'] = None approval_message['status'] = ( approval_message['release_approval_status']) elif not review_response and review_response.status != 404: return review_response else: approval_response = ows_product_workflow.get_last_release_approval_queue(product_id) if not approval_response and approval_response.status != 404: return approval_response approval_message = approval_response.message rejections = None ec_and_ar = False # If this update is changing the status to 'transfer_to_content' # do not fetch the rejections and alter the release status. if release_status != product_statuses.TRANSFER_TO_CONTENT: if approval_message: if approval_message.get('status') == product_statuses.REJECTED: release_status = product_statuses.ACTION_REQUIRED if approval_message.get('release_correction_id'): ec_and_ar = True rejections_response = None if rejections_ff_enabled: rejections_response = ( ows_product_review.get_rejection(product_id)) if not rejections_response: rejections_response = ows_product_workflow.get_rejections( approval_message.get('release_approval_id')) if not rejections_response: return rejections_response rejections = _format_rejections(rejections_response.message.get('items')) # Calls ows-product-workflow for correction info if flag enabled. cr_ec_and_ar = ( rejections_ff_enabled and release_status == product_statuses.ACTION_REQUIRED and product['release_status'] == product_statuses.IN_CONTENT ) corrections = None if release_status in [ product_statuses.IN_CONTENT, product_statuses.TRANSFER_TO_CONTENT, ] or ec_and_ar or cr_ec_and_ar: correction_response = ( ows_product_workflow.get_last_release_correction(product_id)) # Unable to get the release correction. if not correction_response and correction_response.status != 404: return correction_response elif correction_response.status != 404 \ and correction_response.message.get('status') \ in [product_statuses.ACTIVE, product_statuses.SUBMITTED]: correction_status = correction_response.message.get('status') if cr_ec_and_ar and correction_status == product_statuses.ACTIVE: ec_and_ar = True if correction_status == product_statuses.ACTIVE and not ec_and_ar: release_status = product_statuses.ERROR_CORRECTION elif correction_status == product_statuses.SUBMITTED: # An in_content product with submitted corrections is counted # as a submitted product (transfer_to_content). release_status = product_statuses.TRANSFER_TO_CONTENT corrections = correction_response.message corrections['items'] = _format_corrections( product_id, corrections['items'], orchard_user_id) return _format_product( release_data=product, subgenre_id=subgenre_id, release_artists=artists_response.message, product_highlights=product_highlights, release_approval=approval_message, release_status=release_status, rejections=rejections, corrections=corrections, ec_and_ar=ec_and_ar ) def _format_product( release_data, release_status=None, subgenre_id=None, release_artists=None, product_highlights=None, release_approval=None, rejections=None, corrections=None, ec_and_ar=False ): """Convert release-level data to a product structure and format.""" if release_status is None: release_status = release_data.get('release_status') if release_approval is not None: release_approval_status = release_approval.get('status') else: release_approval_status = None formatted_product = { 'artist_id': release_data.get('artist_id'), 'description': release_data.get('description'), 'distribution_format_id': release_data.get('distribution_format_id'), 'format': release_data.get('format'), 'genre_id': release_data.get('genre_id'), 'c_line': release_data.get('c_line'), 'imprint': release_data.get('label'), 'manufacturer_upc': release_data.get('manufacturer_upc'), 'meta_language': release_data.get('meta_language'), 'p_line': release_data.get('p_line'), 'preorder_date': release_data.get('preorder_date'), 'product_code': release_data.get('product_code'), 'previewable': release_data.get('itunes_previewable'), 'product_id': release_data.get('release_id'), 'product_name': release_data.get('release_name'), 'product_type_id': release_data.get('product_type_id'), 'project_id': release_data.get('project_id'), 'product_artists': _format_product_artists(release_artists or []), 'delivered_version': release_data.get('delivered_version'), 'release_date': release_data.get('release_date'), 'release_status': release_status, 'is_error_correction_and_action_required': ec_and_ar, 'sale_start_date': release_data.get('sale_start_date'), 'special_instructions': release_data.get('special_instructions'), 'subaccount_id': release_data.get('subaccount_id'), 'subgenre_id': subgenre_id, 'upc': release_data.get('display_upc'), 'vendor_catalog_number': release_data.get('vendor_catalog_number'), 'version': release_data.get('version'), 'not_for_distribution': release_data.get('not_for_distribution'), } if rejections: formatted_product['rejections'] = rejections if corrections: formatted_product['corrections'] = corrections if release_approval_status: formatted_product['release_approval_status'] = release_approval_status vendor_release_identifier = release_data.get('vendor_release_identifier') if vendor_release_identifier: formatted_product[ 'vendor_release_identifier'] = vendor_release_identifier return formatted_product def _get_rejections(release_approval_id): rejections_response = ows_product_workflow.get_rejections( release_approval_id) return _format_rejections(rejections_response.message.get('items')) def _format_rejections(rejections): return [ { 'rejection_id': rejection.get('rejection_id'), 'field': rejection.get('field_name'), 'note': rejection.get('comments'), 'corrected': rejection.get('corrected'), 'key_type': rejection.get('table_name'), 'key_id': rejection.get('key_id') } for rejection in rejections ] def _format_product_artists(release_artists): """Convert a release artist list to product artists.""" return [_format_product_artist(artist) for artist in release_artists] def _format_product_artist(release_artist_data): """Convert release artist data to product artist data.""" role = release_artist_data.get('role') if role == 'performer': role = 'primary_artist' return { 'role': role, 'name': release_artist_data.get('artist_name'), 'id': release_artist_data.get('release_artist_id'), 'artist_info_id': release_artist_data.get('artist_info_id') } def _parse_release_artists(product_artists): """Convert a product artist list to release artists.""" return [_parse_release_artist(artist) for artist in product_artists] def _parse_release_artist(product_artist_data): """Convert product artist data to release artist data.""" release_artist_data = {} for key, value in product_artist_data.items(): if key == 'name': release_artist_data['artist_name'] = value elif key == 'id': release_artist_data['release_artist_id'] = value else: if key == 'role' and value == 'primary_artist': value = 'performer' release_artist_data[key] = value return release_artist_data def clean_audio_product_to_release_data(data, properties): """Prepare audio product data to be used to create or update the release. Args: data (dict): audio product data properties (list): allowed release properties for create/update Returns: dict: prepared data """ release_data = {} for product_prop in data: mapped_property = product_prop in _product_release_mapping direct_property = product_prop in properties if mapped_property: release_prop = _product_release_mapping[product_prop] release_data[release_prop] = data[product_prop] elif direct_property: release_data[product_prop] = data[product_prop] return release_data def _update_release_data(product_id, audio_data, orchard_user_id=''): """Update product metadata. Args: product_id (int): id of the release to update. audio_data (dict): audio product data orchard_user_id (str): orchard user id. """ release_data = clean_audio_product_to_release_data( audio_data, PROPERTIES_FOR_UPDATE) if ('project_id' in audio_data) and ('subaccount_id' in audio_data): release_data['subaccount_id'] = audio_data['subaccount_id'] # Always call update, even if there is are no release props to save. # We will need the upc from the response to save subgenre and/or artists. return release.update(product_id, release_data, orchard_user_id) def _save_release_subgenre(product_id, upc, audio_data): if 'subgenre_id' not in audio_data: return response.Response() if audio_data.get('subgenre_id') is None: return release_subgenre.delete_release_subgenre(product_id) return release_subgenre.save_release_subgenre({ 'release_id': product_id, 'subgenre_id': audio_data['subgenre_id'], 'upc': upc}) def _save_release_artists(product_id, upc, audio_data): if 'product_artists' not in audio_data: return response.Response() product_artists = audio_data.get('product_artists') or [] return release_artist.save_for_release_id( product_id, upc, _parse_release_artists(product_artists)) def _is_genre_change(product_id, genre_id): release_response = release.get_release(product_id) if not release_response: return release_response return release_response.message.get('genre_id') and \ release_response.message.get('genre_id') != genre_id def _format_request_data(audio_data): return { 'genre_id': audio_data.get('genre_id'), 'subgenre_id': audio_data.get('subgenre_id') } def _is_soundtrack_subgenre_change( product_id, subgenre_id, orchard_user_id=None): product_response = get_product(product_id, orchard_user_id) if not product_response: return product_response existing_subgenre_id = product_response.message.get('subgenre_id') return (existing_subgenre_id != subgenre_id) and \ ((existing_subgenre_id in product_model.SOUNDTRACK_SUBGENRE_IDS) or (subgenre_id in product_model.SOUNDTRACK_SUBGENRE_IDS)) def delete(product_id, session): """Delete a product for the given product_id. Args: product_id (int): release id of the product to be deleted session (db_session): Database session. Returns: Response: Response containing the result of deleting the product. """ track_response = track.delete_all_tracks(product_id) if not track_response: return track_response release_artist.delete(product_id, session) release_status_model.delete(product_id, session) release_subgenre.delete_release_subgenre(product_id, session=session) mkt_priority.bulk_delete_mkt_priority_by_release_id(product_id, session) ows_timed_release.delete_timed_release(product_id) release_logging.delete(product_id, session) release_manual_adjustment.delete(product_id, session) release_grid.delete(product_id, session) release_subtitles.delete(product_id, session) release_approval_queue.delete(product_id, session) release.delete(product_id, session) return response.Response(message={'status': 'ok'})