from itertools import groupby from flask import g from datetime import datetime from typing import List, Optional from marshmallow.orderedset import OrderedSet from sqlalchemy import func from db import db from media_plan.repositories.media_plan_repository import MediaPlanRepository from models import Campaign, MediaPlan from models.media_plans import DEFAULT_MEDIA_PLAN_NAME from services.artists_moments_fetcher import ArtistsMomentsFetcher from projects.exceptions import ProjectScheduleError, ProjectPRSArtistsError from utils.list_utils import exists from projects.services.project_target_items_maper import ProjectTargetItemsMapper from projects.services.project_history_service import ProjectHistoryService from models.artists import Artist from models.projects_import_info import ProjectsImportInfo from services.playlists_repository import PlaylistsRepository from services.artists_repository import ArtistsRepository from artists.repositories.artist_team_repository import ArtistTeamRepository from artists.repositories.artist_repository import ArtistRepository from constants import project_statuses from projects.constants import ProjectTargetTypes from models.user_project import UserProjectRoles from utils.exceptions import NotFound from projects.schemas import ( ProjectListQueryParams, ProjectListResponseSchema, UpdateProjectParams, ProjectResponseSchema, ProjectIdsResponseModel, ProjectStatusSchema, ProjectModelSchema, ProjectCalendarListSchema, ProjectCalendarItemResponseSchema, RecentProjectModelSchema, UpcomingProjectsResponseSchema, ProjectArtistType, ProjectArtistSchema, TrackSchema, ProductFamilySchema, ProjectMetadata, ProjectLinkfireMetadata, ProjectCampaignsMetadata, RecentViewedProjectsQueryParam, RecentViewedProjectsResponseSchema, NewlyAddedProjectsQueryParam, NewlyAddedProjectsResponseSchema ) from projects.validators import ProjectsValidator from shared.schemas import PRSBudgetGroupItem, PRSBudgetCategoryItem from models.projects import Project, ProjectTargetItem, ProjectEntityType, ProjectEntityAddType, ProjectSource from models.media_plan_phases import MediaPlanPhase, DEFAULT_PHASE_NAME from projects.repositories.projects_repository import ProjectsRepository from projects.repositories.projects_list_repository import ( ProjectsListRepository, ProjectListItemResultModel, ProjectExportItemModel, ) from projects.repositories.projects_counter_repository import ProjectsCounterRepository from services.users_service import UsersService from shared.schemas import RoleSchema from services.permissions.exceptions import ProjectNotFound from services.permissions.permissions_service import PermissionsService from services.permission_role_service import PermissionsByRoleService from campaigns.repositories.campaigns_repository import CampaignsRepository from campaigns.services.validators import CampaignsValidator from linkfire.linkfire_repository import LinkfireRepository from linkfire.schemas import LinkfireLinkSchema from services.media_plan_service import MediaPlanService from handlers.enums import CampaignWorkflowStatus from users.services.users_validator import UsersValidator class ApolloLinkNotConnected(NotFound): detail = "Apollo link is not connected to this phase." code = "apollo_link_not_connected" def artists_diff(old: List[Artist], new: List[Artist]): added_artists = list(OrderedSet(new) - OrderedSet(old)) removed_artists = list(OrderedSet(old) - OrderedSet(new)) return added_artists, removed_artists class ProjectsService: users_service = UsersService() projects_repository = ProjectsRepository() artists_repository = ArtistsRepository() artist_team_repository = ArtistTeamRepository() artist_repository = ArtistRepository() playlists_repository = PlaylistsRepository() campaigns_repository = CampaignsRepository() campaigns_validator = CampaignsValidator() projects_validator = ProjectsValidator() project_history_service = ProjectHistoryService() permission_role_service = PermissionsByRoleService() permission_service = PermissionsService() target_items_mapper = ProjectTargetItemsMapper() projects_list_repository = ProjectsListRepository() projects_counter_repository = ProjectsCounterRepository() linkfire_repository = LinkfireRepository() media_plan_service = MediaPlanService() media_plan_repository = MediaPlanRepository() users_validator = UsersValidator() def set_project_default_media_plan(self, project: Project): if not self.media_plan_repository.project_have_media_plan(project.id): media_plan = MediaPlan( name=DEFAULT_MEDIA_PLAN_NAME, order=0, ) project.media_plans.append(media_plan) phase = MediaPlanPhase(name=DEFAULT_PHASE_NAME, order=0) media_plan.phases.append(phase) campaign = Campaign() campaign.project_id = project.id campaign.order_in_phase = 0 campaign.workflow_status = CampaignWorkflowStatus.DRAFT.value campaign.create_user_id = getattr(g, "user_id", None), # we don't have g.user_id in worker context phase.campaigns.append(campaign) def update_project(self, project_id: int, params: UpdateProjectParams, user_id: int) -> ProjectResponseSchema: self.projects_validator.validate_project_details(project_id, params) project = self.projects_repository.get_project_by_id(project_id) if params.endDate < params.startDate: raise ProjectScheduleError() project_state = self.project_history_service.get_project_state(project) moments_fetcher = ArtistsMomentsFetcher(project.target_items) project.name = params.name project.initial_start_date = params.startDate project.edit_user_id = user_id project.notes = params.notes project.end_date = params.endDate self.update_schedules_if_needed(project, params) self.set_target_items_from_params(params, project) self.linkfire_repository.update_project_linkfire_links( project, [ LinkfireLinkSchema(id=link.id, link_id=link.linkId, link_url=link.linkUrl, deletable=link.deletable) for link in params.linkfireLinks ], ) project.touch() project.touch_last_edit() self.projects_repository.add_or_update_project(project) self.projects_repository.refresh_project(project) moments_fetcher.fetch_moments_for_target_items_if_needed(project.target_items) self.project_history_service.log_project_changes(project, project_state, user_id) return self.get_project_detail(project.id) def __update_project_artists(self, project: Project, primary: List[Artist], featured: List[Artist]): was_playlist_project = project.targets_type == ProjectEntityType.PLAYLIST target_items = project.target_items if not was_playlist_project else [] prs_artists = list(map(lambda x: x.entity, filter(lambda x: x.is_locked() and not x.entity.is_unknown, target_items))) old_primary = list(map(lambda x: x.entity, filter(lambda x: x.is_primary_artist(), target_items))) old_featured = list(map(lambda x: x.entity, filter(lambda x: x.is_featured_artist(), target_items))) added_primary_artists, removed_primary_artists = artists_diff(old_primary, primary) added_featured_artists, removed_featured_artists = artists_diff(old_featured, featured) if set.intersection(set(prs_artists), set(removed_primary_artists)): raise ProjectPRSArtistsError() for item in target_items: item.is_deleted = not exists(primary + featured, lambda x: x.id == item.entity.id) if exists(added_primary_artists, lambda x: x.id == item.entity.id): item.entity_type = ProjectEntityType.PRIMARY_ARTIST.value added_primary_artists = list(filter(lambda x: x.id != item.entity.id, added_primary_artists)) if exists(added_featured_artists, lambda x: x.id == item.entity.id): item.entity_type = ProjectEntityType.FEATURED_ARTIST.value added_featured_artists = list(filter(lambda x: x.id != item.entity.id, added_featured_artists)) target_items.extend(self.__project_target_items(added_primary_artists, ProjectEntityType.PRIMARY_ARTIST)) target_items.extend(self.__project_target_items(added_featured_artists, ProjectEntityType.FEATURED_ARTIST)) project.target_items = target_items def update_schedules_if_needed(self, project: Project, params: UpdateProjectParams): schedules = params.scheduleUpdates if not schedules: return if schedules.campaigns: for campaign in schedules.campaigns: self.campaigns_validator.validate_campaign( campaign.entityId, project.id, campaign.startDate, campaign.endDate, params.startDate, params.endDate, ) self.campaigns_repository.update_campaign_schedule( campaign.entityId, campaign.startDate, campaign.endDate ) def __project_target_items(self, artists: List[Artist], type: ProjectEntityType): return [ProjectTargetItem(artist, type, ProjectEntityAddType.MANUALLY) for artist in artists] def set_target_items_from_params(self, params: UpdateProjectParams, project: Project): if params.targets.type == ProjectTargetTypes.ARTIST.value: primary = list(filter(lambda x: x.type == ProjectArtistType.PRIMARY.value, params.targets.items)) featured = list(filter(lambda x: x.type == ProjectArtistType.FEATURED.value, params.targets.items)) primary_artists = self.artists_repository.get_or_create_artists(primary) featured_artists = self.artists_repository.get_or_create_artists(featured) self.__update_project_artists(project, primary_artists, featured_artists) if params.targets.type == ProjectTargetTypes.PLAYLIST.value: playlists = self.playlists_repository.get_or_create_playlists(params.targets.items) targets = [ProjectTargetItem(playlist, ProjectEntityType.PLAYLIST) for playlist in playlists] project.target_items = targets def __map_target_items_from_artists(self, artists: List[Artist], params: List[ProjectArtistSchema]): result = [] for index, artist in enumerate(artists): type = params[index].type result.append(ProjectTargetItem(artist, self.__artist_type_from_string(type))) return result def __artist_type_from_string(self, type: str) -> ProjectEntityType: if type == ProjectArtistType.PRIMARY.value: return ProjectEntityType.PRIMARY_ARTIST return ProjectEntityType.FEATURED_ARTIST def get_project_list(self, user_id: int, filters: ProjectListQueryParams) -> ProjectListResponseSchema: fetch_result = self.projects_list_repository.get_projects_list(user_id, filters) projects = self.__map_project_list_models(fetch_result) metadata = self.projects_counter_repository.get_projects_count(user_id, filters) return ProjectListResponseSchema(items=projects, metadata=metadata, limit=filters.limit, offset=filters.offset) def get_project_calendar_list(self, user_id: int, filters: ProjectListQueryParams) -> ProjectCalendarListSchema: result = [] for item in self.projects_list_repository.get_calendar_projects_list(user_id, filters): target = self.target_items_mapper.map_project_target_items_from_model(item.targets) model = ProjectCalendarItemResponseSchema(item, target) result.append(model) return ProjectCalendarListSchema(result) def get_project_export_list( self, user_id: int, filters: ProjectListQueryParams, artist_external_id: Optional[str] = None ) -> List[ProjectExportItemModel]: return self.projects_list_repository.get_export_projects_list(user_id, filters, artist_external_id) def get_project_detail(self, project_id: int) -> ProjectResponseSchema: project = self.projects_repository.get_project_by_id(project_id) if not project: raise ProjectNotFound() target = self.target_items_mapper.map_project_target_items_from_model(project.not_deleted_items()) territories = self.projects_repository.get_territories_by_project_id(project_id) shared_status = self.projects_repository.get_shared_status(project_id, g.user_id) roles = self.permission_role_service.get_roles_for_project(user_id=g.user_id, project=project) linkfire_links_counts = self.projects_repository.get_project_linkfire_links_count(project_id) pending_campaigns_count = self.campaigns_repository.get_pending_campaigns_count(project_id)[0] external_campaigns_count = self.campaigns_repository.get_external_campaigns_count(project_id)[0] internal_campaigns_count = self.campaigns_repository.get_internal_campaigns_count(project_id)[0] campaign_budget_platforms = self.campaigns_repository.get_campaign_budget_platforms(project_id=project_id) budget_ecpms = self.campaigns_repository.get_campaign_budget_ecpms(project_id=project_id) dates_metadata = self.projects_repository.get_gras_project_campaign_schedule(project.id) metadata = ProjectMetadata( estimatedImpressions=self.media_plan_service.calculate_estimated_impressions(budget_ecpms), plannedBudget=self.media_plan_service.calculate_budget(budget_ecpms), campaignPlatforms=self.media_plan_service.map_platforms_project_metadata(campaign_budget_platforms), assignedUsersCount=self.projects_repository.get_user_project_user_ids_count(project_id), campaigns=ProjectCampaignsMetadata( internalCampaignsCount=internal_campaigns_count, externalCampaignsCount=external_campaigns_count, pendingCampaignsCount=pending_campaigns_count, ), productFamiliesCount=self.projects_repository.get_product_families_count(project_id), linkfire=ProjectLinkfireMetadata( projectLinksCount=linkfire_links_counts.project_links, campaignsLinksCount=linkfire_links_counts.campaign_links, ), earliestStartDate=getattr(dates_metadata, 'earliest_start_date', None), latestEndDate=getattr(dates_metadata, 'latest_end_date', None), ) project_response = ProjectResponseSchema( project=project, targets=target, territories=territories, shared_status=shared_status, user_roles=roles, metadata=metadata, ) return project_response def get_project_budgets(self, project_id: int) -> List[PRSBudgetGroupItem]: project = self.projects_repository.get_project_by_id(project_id) if not project: raise ProjectNotFound() budgets = self.projects_repository.get_budget_categories_by_project_id(project.id) data = sorted(budgets, key=lambda x: x.group_id) budgets_items = [] def category_mapper(x): return PRSBudgetCategoryItem(x.category_id, x.category_name, x.budget, x.allocation) for key, group_items in groupby(data, lambda x: x.group_id): _items = list(group_items) budgets_items.append( PRSBudgetGroupItem( id=_items[0].group_id, title=_items[0].group_name, budget=sum(i.budget for i in _items), allocation=sum(i.allocation for i in _items), categories=[category_mapper(i) for i in _items], ) ) return budgets_items def get_projects_id(self): projects_ids = self.projects_repository.get_all_user_project_ids(g.user_id) return ProjectIdsResponseModel([id for id in projects_ids]) def get_project_statuses(self) -> List[ProjectStatusSchema]: return [ ProjectStatusSchema(id=s_id, name=s_name) for s_id, s_name in project_statuses.PROJECT_STATUSES_MAPPING.items() ] def __map_project_list_models(self, list_models: List[ProjectListItemResultModel]) -> List[ProjectModelSchema]: result = [] for item in list_models: model = ProjectModelSchema( id=item.id, name=item.name, targets=self.target_items_mapper.map_project_target_items_from_model(item.targets), budget=item.budget, allocation=item.allocation, totalSpend=item.total_spend, startDate=item.start_date, endDate=item.end_date, status=item.status, sharedStatus=item.shared_status, userRoles=[RoleSchema(UserProjectRoles(role)) for role in item.user_roles], title=item.title, isConfidential=item.isConfidential, currency=item.currency, isClaimed=item.isClaimed, ) result.append(model) return result def get_recent_projects(self) -> List[RecentProjectModelSchema]: recent_projects = self.projects_list_repository.get_recent_projects(g.user_id) return [ RecentProjectModelSchema( id=item.id, name=item.name, targets=self.target_items_mapper.map_project_target_items_from_model(item.targets), budget=item.budget, allocation=item.allocation, totalSpend=item.total_spend, startDate=item.start_date, endDate=item.end_date, status=item.status, sharedStatus=item.shared_status, userRoles=[RoleSchema(UserProjectRoles(role)) for role in item.user_roles], campaignsCount=item.campaigns_count, title=item.title, isConfidential=item.isConfidential, currency=item.currency, isClaimed=item.isClaimed, ) for item in recent_projects ] def get_upcoming_projects(self) -> List[UpcomingProjectsResponseSchema]: upcoming_projects = self.projects_list_repository.get_upcoming_projects(g.user_id) return [ UpcomingProjectsResponseSchema( id=item.id, name=item.name, targets=self.target_items_mapper.map_project_target_items_from_model(item.targets), userRoles=[RoleSchema(UserProjectRoles(role)) for role in item.user_roles], budget=item.budget, allocation=item.allocation, startDate=item.start_date, isConfidential=item.isConfidential, currency=item.currency, ) for item in upcoming_projects ] def get_product_families(self, project_id: int) -> List[ProductFamilySchema]: product_families = self.projects_repository.get_product_families(project_id) return [ ProductFamilySchema( id=product_family.id, externalId=product_family.external_id, title=product_family.title, releaseDate=product_family.release_date(), tracks=[TrackSchema(track) for track in product_family.tracks], ) for product_family in product_families ] def check_projects_import(self): class CheckProjectsImportException(Exception): pass projects_import_info = db.session.query(ProjectsImportInfo).one_or_none() today = datetime.today().date() if not projects_import_info: raise CheckProjectsImportException("No projects import info found") if today > projects_import_info.imported_prs_date: raise CheckProjectsImportException(f"Last prs import was on {projects_import_info.imported_prs_date}") if today > projects_import_info.imported_ccp_date: raise CheckProjectsImportException(f"Last ccp import was on {projects_import_info.imported_ccp_date}") most_recent_prs_project_date = ( db.session.query(func.max(Project.updated_by_import_at)) .filter(Project.source == ProjectSource.PRS.value) .scalar() ) if most_recent_prs_project_date.date() < today: raise CheckProjectsImportException(f"Latest prs project was imported on {most_recent_prs_project_date}") most_recent_ccp_project_date = ( db.session.query(func.max(Project.updated_by_import_at)) .filter(Project.source == ProjectSource.CCP.value) .scalar() ) if most_recent_ccp_project_date.date() < today: raise CheckProjectsImportException(f"Latest ccp project was imported on {most_recent_ccp_project_date}") def process_is_confidential(self, imported_project): projects = db.session.query(Project).filter( Project.gras_project_code == str(imported_project.external_id)).all() for project in projects: if (project.is_confidential != imported_project.is_confidential and imported_project.is_confidential is False): project.refresh_is_claimed() def get_recent_viewed_projects( self, params: RecentViewedProjectsQueryParam, user_id: int ) -> List[RecentViewedProjectsResponseSchema]: if params.label: self.users_validator.validate_user_has_label(user_id, params.label) recent_projects = self.projects_repository.get_recent_viewed_projects(params, user_id) projects = [] for project in recent_projects: projects.append( RecentViewedProjectsResponseSchema( project=project, targets=self.target_items_mapper.map_project_target_items_from_model(project.not_deleted_items()) ) ) return projects def get_newly_added_projects(self, params: NewlyAddedProjectsQueryParam, user_id: int): newly_added_projects = self.projects_repository.get_new_projects(params, user_id) return [ NewlyAddedProjectsResponseSchema( project=project, targets=self.target_items_mapper.map_project_target_items_from_model(project.not_deleted_items()) ) for project in newly_added_projects ]