"""Utility Functions for Logic.""" import datetime from functools import wraps import pickle from oto import response from oto import status as response_code from owsrequest import access from backend.constants import error from backend.constants import product as product_consts from backend.constants import track_audit as audit_consts from backend.constants import track_field as tf from backend.logic import subgenre from backend.models import ows_assets from backend.models import ows_product from backend.models import track_audit from backend.models.track_persister import TrackPersister from backend.utils import api as api_utils from backend.utils.parallel import parallel from backend.utils.product_utils import get_digital_product_corrections from backend.utils.product_utils import is_in_correction_mode from backend.utils.validation import is_positive_int def fast_deepcopy(obj): """Fast copy of object. This is four times faster than deepcopy when copying dictionary objects. Args: obj: Object to copy Returns: object """ return pickle.loads(pickle.dumps(obj)) def copy_response(res): """Copy response and response message. Args: res: response Returns: Response: Copy of response """ return response.Response( message=fast_deepcopy(res.message), errors=fast_deepcopy(res.errors), status=res.status) def get_product_data_and_validate_ownership( product_id, account_type, account_id): """Validate and get product data. Args: product_id (int): Id of product account_type (string): the user account type in the header. account_id (int): the user account id from the header. """ if not is_positive_int(product_id): return api_utils.create_validation_error_response( error.VALIDATION_ERROR_INVALID_PRODUCT_ID_MSG) if account_type and account_id: ownership_response = ows_product.verify_product_ownership( account_type, account_id, product_id) if not ownership_response: return ownership_response return ows_product.get_product_by_product_id(product_id) def verify_product_ownership(function): """Verify ownership with product_ids. Args: Function (func): the function to be called after grabbing the headers. Returns: Function: The decorated function. """ @wraps(function) def call_function_after_ownership_check(*args, **kwargs): account_type = kwargs.get('account_type') account_id = kwargs.get('account_id') # If Grass account info isn't present, ownership check can be skipped if not account_type and not account_id: kwargs['account_type'] = None kwargs['account_id'] = None return function(*args, **kwargs) product_id = kwargs.get('product_id') # If tuid was passed in, retrieve product_id from that if not product_id and 'tuid' in kwargs: track_response = TrackPersister.get_by_tuid(kwargs.get('tuid')) if track_response: product_id = track_response.message[tf.PRODUCT_ID] if not product_id: return response.create_error_response( code=error.OWS_PRODUCT_ERROR_CODE, message=error.OWS_PRODUCT_NO_PRODUCT_ID_MESSAGE, status=response_code.BAD_REQUEST) if account_type or account_id: account_information = {str(account_type): account_id} has_grass_access = access.verify_grass_access( account_type, account_id, True, **account_information) if not has_grass_access: return has_grass_access ownership_response = ows_product.verify_product_ownership( account_type, account_id, product_id) if not ownership_response: return ownership_response return function(*args, **kwargs) return call_function_after_ownership_check def get_digital_product_genre_type(product): """Get digital product genre. Only a subset of genres (classical and soundtrack) as used. Args: product (dict): Digital Product dictionary Returns: dict: An object with {'genre': string, 'subgenre': string}. """ genre_id = product[product_consts.GENRE_ID] subgenre_id = product[product_consts.SUBGENRE_ID] # Use corrected genre and subgenre if present. if is_in_correction_mode(product): corrections = get_digital_product_corrections(product) for correction in corrections: field_name = correction['field_name'] if field_name == product_consts.GENRE_ID: genre_id = correction['key_value'] elif field_name == product_consts.RELEASE_SUBGENRE: subgenre_id = correction['key_value'][0] if genre_id == product_consts.CLASSICAL_GENRE_ID: subgenres = subgenre.get_genre_subgenres(genre_id).message names = [item['name'] for item in subgenres['items'] if item['orchard_id'] == subgenre_id] subgenre_name = names[0] if names else None return { 'genre': product_consts.CLASSICAL_GENRE, 'subgenre': subgenre_name } if subgenre_id in product_consts.SOUNDTRACK_SUBGENRE_IDS: return { 'genre': product_consts.SOUNDTRACK_GENRE, 'subgenre': product_consts.SOUNDTRACK_SUBGENRE_IDS[subgenre_id] } if subgenre_id in product_consts.WORLD_MUSIC_SUBGENRE_IDS: return { 'genre': product_consts.WORLD_MUSIC_GENRE, 'subgenre': product_consts.WORLD_MUSIC_SUBGENRE_IDS[subgenre_id] } return { 'genre': product_consts.OTHER_GENRE, 'subgenre': '' } def verify_products_ownership( product_ids, account_type, account_id): """Verify all given products belong to given account. Args: product_ids (list): Product(Release) ids account_type (string): the user account type in the header. account_id (int): the user account id from the header. Returns: empty Response if all products belong to given account error if not """ # If Grass account info isn't present, ownership check can be skipped if not account_type and not account_id: return response.Response() # FIXME Replace with call to ows-products that gets all products of account for product_id in product_ids: ownership_response = ows_product.verify_product_ownership( account_type, account_id, product_id) if not ownership_response: return ownership_response return response.Response() def make_publishing_obligation_response(tracks): """Take list of tracks and makes the proper response. Args: tracks (list): List of track dictionaries Returns: response.Response: a Response object with JSON data """ pub_obl_tracks = [] for track in tracks: filtered_track = filter_track_fields( track, tf.PUBLISHING_OBLIGATION_FIELDS) # Set 3rd-party question to no if pub obl field is not set if not filtered_track[tf.US_PUBLISHING_OBLIGATION]: filtered_track[tf.THIRD_PARTY_PUBLISHER] = None # Use publisher_names instead of publishers field filtered_track[tf.PUBLISHER_NAMES] = [ pub[tf.NAME] for pub in filtered_track[tf.PUBLISHERS]] pub_obl_tracks.append(filtered_track) return api_utils.create_get_list_response(pub_obl_tracks) def filter_track_fields(track, fields, include_tuid=True): """Filter track dict to contain only fields passed in. Args: track (dict): Track dictionary fields (list): List of fields to filter include_tuid (bool): Include the tuid in the response Returns: dict: Filtered track dictionary """ track_filtered = { field: track[field] for field in fields} if include_tuid: track_filtered[tf.TUID] = track[tf.TUID] return track_filtered def copy_tracks_assets_in_parallel(source_dest_tuid_list, orchard_user_id, correlation_id): """Copy assets in parallel so request isn't blocked. Args: source_dest_tuid_list (list): List of tuples with source to dest tuid. orchard_user_id (str): ALW user id. correlation_id (str): UUID used for logging. """ requests = dict({source_tuid: _features_parallel_partial(source_tuid, dest_tuid, orchard_user_id, correlation_id) for source_tuid, dest_tuid in source_dest_tuid_list}) return parallel(requests).message def _features_parallel_partial(source_tuid, dest_tuid, orchard_user_id, correlation_id): return {'func': ows_assets.copy_assets, 'args': (source_tuid, dest_tuid, orchard_user_id, correlation_id,)} def add_tracks_log_entries( import_source_to_destination, orchard_user_id, action, audit_metadata=None): """ Save log entries for created tracks. Args: import_source_to_destination (list): list of following dicts {'source': source_track.to_dict, 'destination': new_track.to_dict} orchard_user_id (str): ALW user id. action (str): What triggered creation of logs, e.g. import, delete audit_metadata (str): JSON string Returns: oto.response.Response with list of log_entry.to_dict() on success """ entries = [] for import_dict in import_source_to_destination: source_track = import_dict['source'] destination_track = import_dict['destination'] entry_data = { audit_consts.SOURCE_TUID: source_track[tf.TUID], audit_consts.DESTINATION_TUID: destination_track[tf.TUID], audit_consts.SOURCE_PRODUCT_ID: source_track[tf.PRODUCT_ID], audit_consts.DESTINATION_PRODUCT_ID: destination_track[tf.PRODUCT_ID], audit_consts.ORCHARD_USER_ID: orchard_user_id, audit_consts.ACTION: action, audit_consts.CREATED_DATE: datetime.datetime.utcnow(), audit_consts.AUDIT_METADATA: audit_metadata } entries.append(entry_data) return track_audit.create_log_entries(entries) def verify_and_update_focus_track_dates(tuid, product_id, data): """Checks to see if a given tracks focus track range overlaps updates null end dates if not last. Args: tuid (int): track tuid product_id (int): product id that track belongs to data (request.body): { update request body } Returns: Request body with proper focus track dates or an empty dict if there is overlap """ tracks_response = TrackPersister.get_all_focus_track_by_product_id(product_id) tracks = [track for track in tracks_response.message['items'] if tuid != track['tuid']] tracks = sorted(tracks, key=lambda t: t['focus_track_start_date']) start_date = data.get('focus_track_start_date') end_date = data.get('focus_track_end_date', None) # if given start date doesn't have an end date and it's later than any other track if tracks and start_date > tracks[-1].get('focus_track_start_date'): check_start_date = tracks[-1].get('focus_track_start_date') check_end_date = tracks[-1].get('focus_track_end_date', None) if not check_end_date and start_date > check_start_date: new_date = start_date - datetime.timedelta(1) TrackPersister.update_track( tuid=tracks[-1]['tuid'], data={ 'focus_track': 'Y', 'focus_track_start_date': check_start_date, 'focus_track_end_date': new_date, 'user_id': data.get('user_id', None), 'user_type': data.get('user_type', None) } ) return data # checks for overlap, if an end date is null and not last, it will be updated for i in range(0, len(tracks)): check_start_date = tracks[i].get('focus_track_start_date') if (not end_date and start_date < check_start_date and (i == 0 or start_date > tracks[i - 1].get('focus_track_end_date'))): new_date = check_start_date - datetime.timedelta(1) data['focus_track_end_date'] = new_date return data check_end_date = tracks[i].get('focus_track_end_date', None) if ((check_end_date is None or not (start_date > check_end_date)) and (end_date is None or not (end_date < check_start_date))): return {} return data def focus_track_values_updated(cur_data, req_data): """Checks to see if current focus track data is different from request data. Args: cur_data: current track data req_data: new track data Returns: Bool: True if data is different and false if remains the same """ if (cur_data.get('focus_track') != req_data.get('focus_track') or cur_data.get('focus_track_start_date') != req_data.get('focus_track_start_date') or cur_data.get('focus_track_end_date') != req_data.get('focus_track_end_date')): return True return False