"""Logic for Project Manager. The logic for the project manager application. """ from flask import current_app from flask import g from flask_executor_pde.executor import Executor from oto import response from project_manager.constant import error_const from project_manager.constant import field_const from project_manager.constant import header_const from project_manager.constant import http_status_codes from project_manager.entity.project_post_request import ProjectPostRequest from project_manager.models import mkt_priority from project_manager.models import ows_account from project_manager.models import ows_artist from project_manager.models import ows_marketing from project_manager.models import ows_product_review from project_manager.models import persister from project_manager.models import project as project_model from project_manager.util.dataloader_util import format_for_dataloader def get_product_genres(): """Get the list of genre options for products. Returns: Response: Response containing the option list. """ return persister.get_product_genres() def get_product_imprints(project_id, account_type=None, account_id=None): """Get product imprint options. Args: project_id (int): ID of the parent project of the product for which imprint options are being retrieved. account_type (str): Requester's account type. Either header_const.GRASS_ACCOUNT_TYPE_VENDOR or header_const.GRASS_ACCOUNT_TYPE_SUBACCOUNT (optional). account_id (int): Account ID to retrieve imprints for. If the account_type arg is header_const.GRASS_ACCOUNT_TYPE_VENDOR, account_id is a vendor_id. If the account_type arg is header_const.GRASS_ACCOUNT_TYPE_SUBACCOUNT, account_id is a subaccount_id (optional). Returns: Response: Response containing the option list. """ project_response = _get_project(account_type, account_id, project_id) if not project_response: return project_response project = project_response.message subaccount_id = project[field_const.SUBACCOUNT_ID] if subaccount_id: return persister.get_product_imprints_for_subaccount(subaccount_id) vendor_id = project[field_const.VENDOR_ID] return persister.get_product_imprints_for_vendor(vendor_id) def get_product_subgenres(genre_id): """Get the list of subgenre options for products given a genre id. Args: genre_id (int): genre id Returns: Response: Response containing the option list. """ return persister.get_product_subgenres(genre_id) def get_product_types(): """Get the list of product type options. Returns: Response: Response containing the option list. """ return persister.get_product_types() def get_project_by_project_code(account_id, subaccount_id, project_code): """Get project by project_code. Returns: Response: Response containing the project. """ return persister.get_project_by_project_code( project_code, account_id, subaccount_id) def get_available_project_codes(account_uuid, subaccount_uuid, project_codes): """Get available project_codes from a list of project codes. Returns: Response: Response containing a list of project codes not in use and a list of in use codes with details. """ existing_projects = persister.get_existing_projects( project_codes, account_uuid, subaccount_uuid).message return response.Response(message={ 'available_project_codes': list( set(project_codes) - set(project['project_code'] for project in existing_projects) ), 'existing_projects': existing_projects }) def get_project( account_type, account_id, project_id, include_soft_delete=False ): """Get the project by project id. Args: account_type (str): vendor or subaccount account_id (int): id of account project_id (int): project id include_soft_delete (bool): Whether to include deleted projects Returns: response """ call_and_args_list = [ ( _get_project, ( account_type, account_id, project_id, include_soft_delete ) ), ( ows_marketing.get_project_highlights_by_project_id, (project_id,) ) ] with Executor(current_app) as executor: response_list = list(executor.map_multiple(call_and_args_list)) project_response = response_list[0] if not project_response: return project_response project = project_response.message highlight_text = None highlight_response = response_list[1] if highlight_response: highlight_text = highlight_response.message.get('description') project['project_highlights'] = highlight_text return response.Response(message=project) def get_products_for_project(account_type, account_id, project_id): """Get a list of products for a project. Args: account_type (str): vendor or subaccount account_id (int): id of account project_id (int): project id Returns: Response: response object with message and status """ project_response = _get_project(account_type, account_id, project_id) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] products_response = persister.get_products_for_project( project_id, vendor_id) if not products_response: return products_response products = products_response.message formatted_products = [] if products: call_and_args_list = [ ( _get_formatted_product, (product,) ) for product in products ] with Executor(current_app) as executor: formatted_products = list(executor.map_multiple(call_and_args_list)) products_response.message = {'items': formatted_products} return products_response def get_product_for_project( account_type, account_id, project_id, product_id, ): """Get a product for a project. Args: account_type (str): vendor or subaccount account_id (int): id of account project_id (int): project id product_id (int): product id Returns: Response: response object with message and status """ project_response = _get_project(account_type, account_id, project_id) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] product_response = persister.get_product_for_project( project_id, product_id, vendor_id) if not product_response: return product_response product = product_response.message formatted_product = _get_formatted_product( product, ) return response.Response(message=formatted_product) def get_projects(vendor_id, subaccount_id, page_offset, page_limit): """Get a list of projects by vendor id or subaccount id. Args: vendor_id (int): vendor id subaccount_id (int): subaccount id page_offset (int): record index used to start page_limit (int): number of records to fetch Returns: list of project dicts """ if vendor_id and subaccount_id: auth_response = ows_account.is_subaccount_for_vendor( vendor_id, subaccount_id) if auth_response.status != 200: return auth_response return persister.get_projects( vendor_id, subaccount_id, page_offset, page_limit) def add_project( project_code, project_name, artist_id, subaccount_id, grass_account_type, grass_account_id, vendor_id=None, project_highlights=None, description=None, user=None, artist=None, ): """Add a project. Args: project_code (string): user-supplied project code project_name (string): free text description of project artist_id (int): the id of the artist subaccount_id (string or None) grass_account_type (string or None): 'vendor' or 'subaccount' grass_account_id (string or None): vendor_id or subaccount_id project_highlights (string) defaults to None description (string): user-supplied description user: orchard_user_id artist (dict): artist metadata Returns: dictionary of entire project record that was inserted """ if not user: return response.create_error_response( code=error_const.ERROR_CODE_MISSING_ORCHARD_USER_ID, message=error_const.ERROR_MESSAGE_MISSING_ORCHARD_USER_ID) if artist and not artist_id: created_artist = ows_artist.create_artist_info(artist) if not created_artist: return created_artist artist_id = created_artist.message.get('id') req = ProjectPostRequest( project_code=project_code, project_name=project_name, artist_id=artist_id, subaccount_id=subaccount_id, grass_account_type=grass_account_type, grass_account_id=grass_account_id, vendor_id=vendor_id, correlation_id=g.ows.correlation_id, description=description, user=user, ) if req.vendor_id is None: # subaccount request req.vendor_id = ows_account.get_vendor_id_for_subaccount_id( req.subaccount_id) if req.vendor_id is None: error_detail = "Unable to find vendor_id for subaccount_id '{}'" error_detail = error_detail.format(req.subaccount_id) return response.create_error_response( error_const.VALIDATION_ERROR, error_detail, 400) else: # vendor or distributor request is_distributor_response = ows_account.is_distributor(req.vendor_id) has_subaccount = ( req.subaccount_id == ProjectPostRequest.SUBACCOUNT_ID_NULL) if is_distributor_response.status not in (200, 204): # unexpected status code: return error return is_distributor_response elif (is_distributor_response.status == 200): # account is a distributor if has_subaccount: # distributor failed to include subaccount_id, return error return response.create_error_response( error_const.VALIDATION_ERROR, error_const.ERROR_MSG_DISTRIBUTOR_MISSING_SUBACCOUNT, 400) elif (not has_subaccount): # account is a distributor and subaccount is non-null # check whether vendor is authorized for subaccount auth_response = ows_account.is_subaccount_for_vendor( req.vendor_id, req.subaccount_id) if auth_response.status != 200: # return system or authorization error return auth_response elif (is_distributor_response.status == 204 and not has_subaccount): # fail if non-distributor passed in a subaccount_id return response.create_error_response( error_const.VALIDATION_ERROR, 'non-distributor cannot create project for subaccount', 400) project = persister.insert_project(req) if project.status == 201: project.message['project_highlights'] = None if project_highlights and project.status == 201: project = _create_project_highlights_for_project( project.message, project_highlights) return response.Response(status=201, message=project) return project def check_project_ownership(project_id, vendor_id=None, subaccount_id=None): """Check if a vendor or subaccount owns a project. Args: project_id (int): Project id of project to verify ownership of. vendor_id (int): Vendor id to verify project ownership for. subaccount_id (int): Subaccount id to verify project ownership for. Returns: Response: Response containing the result of the ownership check and a message with the tuple: (vendor_id, subaccount_id). """ project_response = persister.get_project_by_id(project_id) if not project_response: return project_response project = project_response.message vendor_and_subaccount = ( project.get(field_const.VENDOR_ID), project.get(field_const.SUBACCOUNT_ID)) if not subaccount_id and not vendor_id: return response.Response( status=http_status_codes.BAD_REQUEST, message=vendor_and_subaccount) if vendor_id and not is_vendor_project_owner(project, vendor_id): return response.Response( status=http_status_codes.FORBIDDEN, message=vendor_and_subaccount) if subaccount_id and not is_subaccount_project_owner( project, subaccount_id): return response.Response( status=http_status_codes.FORBIDDEN, message=vendor_and_subaccount) return response.Response( status=http_status_codes.OK, message=vendor_and_subaccount) def update_project(account_type, account_id, project_id, params, user): """Update project by id. Update the project by given project_id. Args: account_type (string): account_type account_id (int): account_id project_id (int): project_id user (string): orchard_user_id params (dict): parameters to be updated Returns: response """ is_project_owner = _get_project( account_type, account_id, project_id, True) if not is_project_owner: return is_project_owner if not user: return response.create_error_response( code=error_const.ERROR_CODE_MISSING_ORCHARD_USER_ID, message=error_const.ERROR_MESSAGE_MISSING_ORCHARD_USER_ID) project_response = persister.update_project_by_id(project_id, params, user) if not project_response: return project_response return _create_or_update_project_highlights_for_project( project_id, project_response, params) def is_vendor_project_owner(project, vendor_id): """Check if a vendor owns a project. Args: project (dict): Project to check ownership of. vendor_id (int): Vendor id to check ownership for. Returns: bool: Result of ownership check. """ return vendor_id == project.get(field_const.VENDOR_ID) def is_subaccount_project_owner(project, subaccount_id): """Check if a subaccount owns a project. Args: project (dict): Project to check ownership of. subaccount_id (int): Subaccount id to check ownership for. Returns: bool: Result of ownership check. """ return subaccount_id == project.get(field_const.SUBACCOUNT_ID) def is_owner(account_type, account_id, project): """Check if an account owns a project. Args: account_type (str): Account type to check ownership for. account_id (int): Account id to check ownership for. project (dict): Project to check ownership of. Returns: bool: Result of ownership check. """ if account_type == header_const.GRASS_ACCOUNT_TYPE_VENDOR: return is_vendor_project_owner(project, account_id) else: return is_subaccount_project_owner(project, account_id) def delete_project(project_id, account_type, account_id, hard_delete=False): """Delete a project if the project has no associated products. Args: project_id (int): Unique identifier for project to be deleted. account_type (str): Account type to check ownership for. account_id (int): Account id to check ownership for. Returns: response.Response: the outcome of the deletion. """ project_response = _get_project(account_type, account_id, project_id) if not project_response: return project_response project = project_response.message vendor_id = project['vendor_id'] found_products = persister.get_products_for_project( project_id, vendor_id).message if len(found_products) > 0: return response.create_error_response( code=error_const.ERROR_CODE_BAD_REQUEST, message=error_const.ERROR_MESSAGE_PRODUCTS_FOUND) if not hard_delete: project_delete_response = project_model.delete_project(project_id) if not project_delete_response: return project_delete_response project_highlight = ows_marketing.get_project_highlights_by_project_id( project_id) if project_highlight: highlight_id = project_highlight.message.get('highlight_id') ows_marketing.delete_project_highlight_with_project_deletion( highlight_id) if hard_delete: project_delete_response = project_model.hard_delete_project(project_id) if not project_delete_response: return project_delete_response return response.Response() def _get_final_release_status(release_status, review_status, correction_status): """Combine inputs to get final release_status.""" if release_status == 'in_content' and correction_status is None and review_status == 'rejected': # jayfid - this state makes no sense to me? return 'action_required' if release_status == 'transfer_to_content' or correction_status == 'submitted': return 'transfer_to_content' if ( release_status == 'in_content' and review_status == 'rejected' and correction_status == 'active' ): return 'action_required' if release_status == 'in_content' and correction_status == 'active': return 'error_correction' if ( release_status in {'label_processing', 'orchard_processing'} and review_status == 'rejected' ): return 'action_required' return release_status def get_is_error_correction_and_action_required(review_status, correction_status): """Get er and ar status.""" return bool(correction_status and review_status == 'rejected') def _get_formatted_product(product): """Helper function to format product for response. Return a formatted product with required data for response. Args: product (dict): product used to build a formatted product Returns: A formatted dict representing a product. """ product_id = product['product_id'] statuses = persister.get_release_status_for_product(product_id).message latest_review_status = None if product['_is_enabled_rejections_from_cr']: status_response = ows_product_review.get_status(product_id) if status_response and status_response.status == 200: latest_review_status = status_response.message['release_approval_status'] else: latest_review_status = statuses['review_status'] release_status = _get_final_release_status( statuses['release_status'], latest_review_status, statuses['correction_status'] ) ec_and_ar = get_is_error_correction_and_action_required( latest_review_status, statuses['correction_status'] ) return { 'version': product.get('version'), 'delivered_version': product.get('delivered_version'), 'product_type_id': product.get('product_type_id'), 'display_upc': product.get('display_upc'), 'upc': product.get('upc'), 'product_type': product.get('product_type'), 'product_id': product_id, 'release_name': product.get('release_name'), 'artist_names': persister.get_artists_for_product(product_id).message, 'release_status': release_status, 'is_error_correction_and_action_required': ec_and_ar, 'distribution_format': { 'id': product.get('distribution_format_id'), 'name': product.get('distribution_format_name'), 'context': product.get('context_type')}, 'format': product.get('format'), 'release_approval_status': latest_review_status, 'not_for_distribution': product.get('not_for_distribution') } def _create_project_highlights_for_project(project, project_highlights): """Helper function to create project highlights for a project. Args: project (dict): a dictionary that represents a project Returns: A dictionary that represents a project with project highlights if created. """ marketing_response = ows_marketing.create_project_highlights( project['project_id'], project_highlights) if marketing_response.status == 201: project['project_highlights'] = project_highlights return project def get_project_by_id(project_id): """Get project by id.""" project_response = persister.get_project_by_id(project_id) if not project_response: return response.create_not_found_response(message='Project not found.') return project_response.message def _get_project( account_type, account_id, project_id, include_soft_delete=False): """Helper function that calls get_project_by_id from persister. Args: account_type (str): vendor or subaccount account_id (int): id of account project_id (int): project id include_soft_delete (bool): include soft delete project Returns: response object containing the project """ project_response = persister.get_project_by_id( project_id, include_soft_delete) project = project_response.message if account_type and account_id: if project and not is_owner(account_type, account_id, project): return response.create_error_response( error_const.AUTHORIZATION_ERROR, 'Not the owner of this project', 403) return project_response def get_bulk_projects(account_type, account_id, project_ids): """Bulk-fetch projects aligned to project_ids for the dataloader. Applies the same ownership scoping as get_project: with an account context, projects the caller does not own are returned as None. Args: account_type (str | None): grass account type of the caller account_id (int | None): grass account id of the caller project_ids (list[int]): project ids to fetch; defines result order Returns: Response: response with {'projects': [...]} aligned to project_ids """ projects = persister.get_projects_by_ids(project_ids).message if account_type and account_id: projects = [ project for project in projects if is_owner(account_type, account_id, project) ] return response.Response(message={ 'projects': format_for_dataloader(projects, project_ids, 'project_id') }) def _create_or_update_project_highlights_for_project( project_id, project_response, params): """Update a project's project highlights. Updates a project's project highlights if project highlights provided. Args: project_id (int): project_id project_response (response): response object from updated project call params (dict): parameters to be updated Returns: response """ highlight = ows_marketing.get_project_highlights_by_project_id(project_id) if 'project_highlights' in params: project_highlights = params.get('project_highlights') highlight_response = None if highlight: highlight_response = ows_marketing.update_project_highlights( # noqa highlight.message.get('highlight_id'), project_highlights) if highlight.status == 404: highlight_response = ows_marketing.create_project_highlights( # noqa project_id, project_highlights) if highlight_response: new_highlight = highlight_response.message.get('description') project_response.message['project_highlights'] = new_highlight return project_response if highlight: old_highlight = highlight.message.get('description') project_response.message['project_highlights'] = old_highlight return project_response def _make_request(request_item): """Make a request as part of a multi-thread series of requests. Args: request_item: a dictionary containing a function and parameters. Returns: Result of calling the given function with the given parameters. """ return request_item.get('function_to_call')(*request_item.get('params')) def set_mkt_priority(project_id, country_id, priority, user_id): """Set mkt priority for a given project_id. Args: project_id (int): id of the project to fetch country_id (int): id of the country priority (str): a or b on None Returns: response.Response: a response object with mkt priority information """ return mkt_priority.set_mkt_priority_by_project_id( project_id, country_id, priority, user_id ) def get_mkt_priority(project_id): """Fetch an mkt priority from a given project_id. Args: project_id (int): id of the project to fetch Returns: response.Response: a response object with mkt priority information """ mkt_priority_response = mkt_priority.get_mkt_priority_by_project_id( project_id, ) if not mkt_priority_response: return mkt_priority_response return response.Response(message={'mkt_priority': mkt_priority_response.message}) def get_bulk_mkt_priority(project_ids): """Fetch bulk mkt priorities from given project_ids. Args: project_ids (list): id of the project to fetch Returns: response.Response: a response object with mkt priority information """ mkt_priority_response = mkt_priority.get_bulk_mkt_priority_by_project_ids( project_ids, ) if not mkt_priority_response: return mkt_priority_response return response.Response(message={ 'mkt_priority': format_for_dataloader( mkt_priority_response.message, sorted([mkt_p['id'] for mkt_p in mkt_priority_response.message]), 'id', ) }) def delete_mkt_priority(project_id, projection_id): """Delete mkt priority for a given project_id and projection_id. Args: project_id (int): id of the project projection_id (int): id of the mkt_priority_project Returns: response.Response: a response object with delete message """ return mkt_priority.delete_mkt_priority_by_project_id( project_id, projection_id, ) def bulk_delete_mkt_priority_for_project(project_id): """Delete all marketing priorities of a project. Args: project_id (int): id of the project Returns: response.Response: a response object """ return mkt_priority.bulk_delete_mkt_priority_for_project( project_id )