"""Episode Model. This model represents an Episode """ import datetime import sqlalchemy from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.orm import relationship from sqlalchemy.sql import func from podcast.connectors import mysql from podcast.constants import error from podcast.constants.asset_types import MAX_SIGNED_URL_DURATION from podcast.constants.feature_flag import FEATURE_PODCAST_IA_RESTRUCTURE from podcast.logic import user as user_logic from podcast.logic import user_v2 as user_v2_logic from podcast.models import episode as episode_model from podcast.models import network as network_model from podcast.models import order as order_model from podcast.models import podcast as podcast_model from podcast.models import user as user_model from podcast.utils import api_utils from podcast.utils import feature_flag_utils from podcast.utils import signed_urls from podcast.utils.exc import OwsError ad_action_assignee_table = sqlalchemy.Table( 'ad_action_assignee', mysql.BaseModel.metadata, sqlalchemy.Column('user_id', sqlalchemy.Integer, sqlalchemy.ForeignKey('user.id')), sqlalchemy.Column('ad_action_id', sqlalchemy.Integer, sqlalchemy.ForeignKey('ad_action.id')), ) class AdAction(mysql.BaseModel): """AdAction model.""" __tablename__ = 'ad_action' id = sqlalchemy.Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True) # noqa campaign_id = sqlalchemy.Column(sqlalchemy.VARCHAR(255), nullable=False) order_id = sqlalchemy.Column(sqlalchemy.VARCHAR(255), nullable=False) advertisement_id = sqlalchemy.Column(sqlalchemy.VARCHAR(255), nullable=False) roll_type = sqlalchemy.Column(sqlalchemy.Enum(*['pre', 'mid', 'post'])) title = sqlalchemy.Column(sqlalchemy.VARCHAR(255)) copy_url_path = sqlalchemy.Column(sqlalchemy.VARCHAR(128)) copy_filename = sqlalchemy.Column(sqlalchemy.VARCHAR(200)) copy_updated_date = sqlalchemy.Column(sqlalchemy.DateTime, default=func.now()) due_date = sqlalchemy.Column(sqlalchemy.DateTime) is_deleted = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) created_by = sqlalchemy.Column(sqlalchemy.Integer) created_date = sqlalchemy.Column(sqlalchemy.DateTime, default=func.now()) updated_by = sqlalchemy.Column(sqlalchemy.Integer) updated_date = sqlalchemy.Column( sqlalchemy.DateTime, default=func.now(), onupdate=func.now()) assignees = relationship('User', secondary=ad_action_assignee_table, lazy='joined') status = sqlalchemy.Column(sqlalchemy.Enum( *['new', 'submitted', 'rejected', 'completed']), nullable=False, default='new') rejection_reason = sqlalchemy.Column(sqlalchemy.VARCHAR(512)) is_virus_free = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) is_archived_early = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) requires_approval = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) is_copy_updated = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) def to_dict(self, include_copy_url=True): """Return the object as dictionary.""" base_dict = dict( id=self.id, campaign_id=self.campaign_id, order_id=self.order_id, advertisement_id=self.advertisement_id, roll_type=self.roll_type, assignee_ids=[assignee.to_dict()['id'] for assignee in self.assignees], title=self.title, due_date=self.due_date, status=self.status, rejection_reason=self.rejection_reason, ad_read_url=None, created_date=self.created_date, updated_date=self.updated_date, created_by=self.created_by, updated_by=self.updated_by, requires_approval=self.requires_approval, is_archived_early=self.is_archived_early, is_virus_free=self.is_virus_free, is_deleted=self.is_deleted, is_copy_updated=self.is_copy_updated ) if include_copy_url: # Certain calls don't use this field base_dict['copy_url_path'] = signed_urls.sign_url( api_utils.asset_url(self.copy_url_path), MAX_SIGNED_URL_DURATION ) base_dict['copy_filename'] = self.copy_filename base_dict['copy_updated_date'] = self.copy_updated_date return base_dict def get_ad_actions( is_archived=False, only_show_mine=False, limit=9999, offset=0, start_date=None, end_date=None): """Return all ad actions. Ad action access is checked at 3 levels 1) Network level 2) Podcast level or Show-Family level based on the ia_restructure feature flag 3) Episode level Returns: dict: containing the ad actions. """ with mysql.pod_db_session(read_only=True) as session: current_user = user_logic.get_current_user() current_user_id = current_user['id'] network_ids = user_logic.network_ids_for_current_user() if feature_flag_utils.get_feature_flag(FEATURE_PODCAST_IA_RESTRUCTURE): podcast_ids = user_v2_logic.specific_podcast_ids_for_current_user(current_user) else: podcast_ids = user_logic.specific_podcast_ids_for_current_user(current_user) episode_ids = episode_model.get_episode_ids_by_podcast_ids( podcast_ids) if podcast_ids else [] query = session.query(AdAction, order_model.Order) \ .filter(AdAction.order_id == order_model.Order.megaphone_id) \ .filter(AdAction.is_deleted.isnot(True)) \ .filter(sqlalchemy.or_( order_model.Order.networks.any(network_model.Network.id.in_(network_ids)), order_model.Order.podcasts.any(podcast_model.Podcast.id.in_(podcast_ids)), order_model.Order.episodes.any(episode_model.Episode.id.in_(episode_ids)) )) \ .filter(AdAction.is_virus_free.is_(True)) if is_archived: query = query.filter(AdAction.status == 'completed') query = query.filter(sqlalchemy.or_( AdAction.is_archived_early.is_(True), AdAction.due_date <= datetime.datetime.utcnow() - datetime.timedelta(weeks=2))) else: query = query.filter(AdAction.is_archived_early.isnot(True)) query = query.filter(sqlalchemy.or_( AdAction.status != 'completed', AdAction.due_date > datetime.datetime.utcnow() - datetime.timedelta(weeks=2))) if only_show_mine: query = query.filter(sqlalchemy.or_( AdAction.assignees.any(user_model.User.id.in_([current_user_id])), AdAction.created_by == current_user_id)) if start_date: query = query.filter(AdAction.due_date > start_date) if end_date: query = query.filter(AdAction.due_date < end_date) total_records = query.count() query = query.limit(limit).offset(offset) rows = query.all() items = [row[0].to_dict() for row in rows] return { 'items': items, 'pagination': { 'total_records': total_records } } def create_ad_action(data): """Create a new Ad Action. Args: data (dict): the data from which to create the ad action. Returns: dict: the created ad action dict. """ if not data: raise OwsError.bad_request(error.ERROR_MESSAGE_EMPTY_BODY) with mysql.pod_db_session() as session: user_id = api_utils.get_user_id() data['created_by'] = user_id data['updated_by'] = user_id user_ids = data.pop('assignee_ids', []) ad_action = AdAction(**data) ad_action.assignees = session.query(user_model.User).filter(user_model.User.id.in_(user_ids)).all() session.add(ad_action) return ad_action.to_dict() def has_duplicate_request(campaign_id, order_id, advertisement_id): """Check if an active request exists.""" with mysql.pod_db_session(read_only=True) as session: query = session.query(AdAction).filter( AdAction.campaign_id == campaign_id, AdAction.order_id == order_id, AdAction.advertisement_id == advertisement_id, AdAction.is_deleted.isnot(True), AdAction.status != 'completed' ) return query.first() def update_ad_action(ad_action_id, data): """Update Ad Action. Args: data (dict): the data from which to update the ad action. Returns: dict: the updated ad action dict. """ with mysql.pod_db_session() as session: assignee_id = data.pop('assignee_id', None) new_assignee_ids = data.pop('assignee_ids', None) query = session.query(AdAction).filter(AdAction.id == ad_action_id) ad_action = query.first() if not ad_action: raise OwsError.not_found() if data: query.update(data) result = query.first() if new_assignee_ids: result.assignees = session.query(user_model.User).filter( user_model.User.id.in_(new_assignee_ids)).all() elif assignee_id: old_assignee_ids = result.to_dict()['assignee_ids'] if assignee_id not in old_assignee_ids: old_assignee_ids.append(assignee_id) result.assignees = session.query(user_model.User).filter( user_model.User.id.in_(old_assignee_ids)).all() return result.to_dict() def get_ad_action_no_assets(ad_action_id): """Get ad action without the assets. Args: ad_action_id (int): the id Returns: dict: the ad action dict. """ with mysql.pod_db_session(read_only=True) as session: query = session.query(AdAction).filter(AdAction.id == ad_action_id) ad_action = query.first() if not ad_action: raise OwsError.not_found() return ad_action.to_dict() def get_ad_action_no_assets_by_filename(filename): """Get ad action without the assets. Args: filename (str): Ad copy pdf filename Returns: dict: The ad action dict. """ with mysql.pod_db_session(read_only=True) as session: query = session.query(AdAction).filter(AdAction.copy_url_path == filename) ad_action = query.first() if not ad_action: raise OwsError.not_found() return ad_action.to_dict() def delete_ad_action(ad_action_id, current_user, is_network_admin): """Delete an Ad Action. Args: ad_action_id (int): id to delete Returns: dict: the deleted ad action """ with mysql.pod_db_session() as session: ad_action = session.query(AdAction).filter( AdAction.id == ad_action_id, ).first() if not ad_action: raise OwsError.not_found() if ad_action.is_deleted: raise OwsError.not_found(error.ERROR_MESSAGE_EPISODE_IS_DELETED) if is_network_admin and not current_user['all_networks']: user_logic.current_user_owns_ad_action_or_raise(ad_action.to_dict(), current_user, is_network_admin) ad_action.updated_by = current_user['id'] ad_action.is_deleted = True return ad_action.to_dict() def due_in(hours): """Find unfinished ads due in x hours.""" with mysql.pod_db_session(read_only=True) as session: now = datetime.datetime.utcnow() query = session.query(AdAction).filter( AdAction.is_deleted.isnot(True), AdAction.is_virus_free.is_(True), AdAction.due_date >= now + datetime.timedelta(hours=hours - 1), AdAction.due_date < now + datetime.timedelta(hours=hours), AdAction.status != 'completed' ) return [ad.to_dict() for ad in query.all()] def get_ad_actions_by_advertisement_ids(ad_ids): """Get ad actions by advertisement ids.""" with mysql.pod_db_session(read_only=True) as session: query = session.query(AdAction).filter( AdAction.is_deleted.isnot(True), AdAction.is_virus_free.is_(True), AdAction.advertisement_id.in_(ad_ids) ) return {'items': [ad.to_dict(include_copy_url=False) for ad in query.all()]}