"""Validation schemas description.""" import datetime from dateutil.parser import parse as parse_date from marshmallow import EXCLUDE from marshmallow import fields from marshmallow import pre_load from marshmallow import Schema from marshmallow import validate from marshmallow import validates_schema from marshmallow import ValidationError from podcast.constants import common from podcast.constants import user as user_constants from podcast.constants.ad_action import ALLOWED_AD_DATE_RANGE from podcast.constants.analytics import ALLOWED_DATE_RANGE, ALLOWED_OBJECT_TYPES, CUSTOM from podcast.constants.error import ( ERROR_INCORRECT_DATE_FORMAT, ERROR_MESSAGE_MISSING_REQUIRED_DATA, ERROR_REQUIRED_DATE, ERROR_START_DATE_GREATER_THAN_END_DATE) from podcast.constants.participant import ROLES from podcast.utils.exc import OwsError def _validate_dates(start_date, end_date): if not start_date: raise ValidationError(ERROR_REQUIRED_DATE, 'start_date') if not end_date: raise ValidationError(ERROR_REQUIRED_DATE, 'end_date') try: datetime.datetime.strptime(start_date, '%Y-%m-%d') datetime.datetime.strptime(end_date, '%Y-%m-%d') except Exception: raise ValidationError(ERROR_INCORRECT_DATE_FORMAT, 'date_fields') if parse_date(start_date) > parse_date(end_date): raise ValidationError(ERROR_START_DATE_GREATER_THAN_END_DATE, 'start_date') class ParticipantSchema(Schema): """Participant schema.""" name = fields.String(required=True) role = fields.String(required=True, validate=validate.OneOf(ROLES)) participant_id = fields.Int(required=True) class SeasonSchema(Schema): """Season schema.""" season_id = fields.Integer(attribute='id', required=False, validate=validate.Range(min=1), allow_none=True) number = fields.Integer(required=True, validate=validate.Range(min=1)) name = fields.String(required=True, validate=validate.Length(max=255)) external_id = fields.String(required=False, validate=validate.Length(max=64), allow_none=True) class Meta: """Exclude unknown fields from processing and passing by further.""" unknown = EXCLUDE class CreatePodcastSchema(Schema): """Podcast validation schema.""" network_id = fields.Integer(required=True) title = fields.String(required=True) host = fields.String(required=True) owner = fields.String(required=True) description = fields.String(required=True) slug = fields.String( required=False, validate=[validate.Regexp('^[\\w-]+$'), validate.Length(max=255)], allow_none=True ) copyright = fields.String(required=True) # noqa: A003 link = fields.String(required=False, allow_none=True) email = fields.String(required=True, validate=validate.Email()) explicit = fields.String(required=True) language = fields.String(required=True) show_type = fields.String(required=True) artwork_filename = fields.String(required=True) categories = fields.List(fields.Int, required=True) participants = fields.List(fields.Nested(ParticipantSchema()), required=False) seasons = fields.List(fields.Nested(SeasonSchema()), required=False) feed_type = fields.String(required=False, allow_none=True, validate=validate.OneOf(common.FEED_TYPES)) channel_id = fields.String(required=False, validate=validate.Length(max=50), allow_none=True) @validates_schema def validate_slug(self, data, **kwargs): """Validate Slug field.""" if data.get('feed_type') in (common.PUBLIC_RSS, common.PRIVATE_RSS): if not data.get('slug'): raise ValidationError(ERROR_MESSAGE_MISSING_REQUIRED_DATA, 'slug') @validates_schema def validate_channel_id(self, data, **kwargs): """Validate channel_id field.""" if data.get('feed_type') in (common.APPLE_SUBSCRIPTION, common.YOUTUBE): if not data.get('channel_id'): raise ValidationError(ERROR_MESSAGE_MISSING_REQUIRED_DATA, 'channel_id') class Meta: """Exclude unknown fields from processing and passing by further.""" unknown = EXCLUDE class CreateFeedSchema(Schema): """Feed validation schema.""" podcast_id = fields.Integer(required=True) show_family_id = fields.Integer(required=True) title = fields.String(required=True) description = fields.String(required=True) feed_type = fields.String(required=True) artwork_filename = fields.String(required=True) slug = fields.String( required=False, validate=[validate.Regexp('^[\\w-]+$'), validate.Length(max=255)], allow_none=True ) channel_id = fields.String(required=False, validate=validate.Length(max=50), allow_none=True) episode_ids = fields.List(fields.Int, required=False) is_copy_ad_locations = fields.Boolean() @validates_schema def validate_slug(self, data, **kwargs): """Validate Slug field.""" if data['feed_type'] in (common.PUBLIC_RSS, common.PRIVATE_RSS): if not data.get('slug'): raise ValidationError(ERROR_MESSAGE_MISSING_REQUIRED_DATA, 'slug') @validates_schema def validate_channel_id(self, data, **kwargs): """Validate channel_id field.""" if data['feed_type'] in (common.APPLE_SUBSCRIPTION, common.YOUTUBE): if not data.get('channel_id'): raise ValidationError(ERROR_MESSAGE_MISSING_REQUIRED_DATA, 'channel_id') class Meta: """Exclude unknown fields from processing and passing by further.""" unknown = EXCLUDE class UpdatePodcastSchema(Schema): """Update podcast validation schema.""" title = fields.String(required=False) host = fields.String(required=False) owner = fields.String(required=False) description = fields.String(required=False) copyright = fields.String(required=False) # noqa: A003 link = fields.String(required=False, allow_none=True) email = fields.String(required=False, validate=validate.Email()) explicit = fields.String(required=False) language = fields.String(required=False) artwork_filename = fields.String(required=False) categories = fields.List(fields.Int, required=False) participants = fields.List(fields.Nested(ParticipantSchema()), required=False) seasons = fields.List(fields.Nested(SeasonSchema()), required=False) channel_id = fields.String(required=False, validate=validate.Length(max=50), allow_none=True) class Meta: """Exclude unknown fields from processing and passing by further.""" unknown = EXCLUDE class DraftEpisodeSchema(Schema): """Episode validation schema.""" title = fields.String(required=True) description = fields.String(required=False) season_id = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) season_number = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) episode_number = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) episode_type = fields.String( required=False, allow_none=True, validate=validate.OneOf(('full', 'trailer', 'bonus'))) trailer_type = fields.String( required=False, allow_none=True, validate=validate.OneOf(('regular', 'cross-promo'))) content = fields.String(required=False, allow_none=True, validate=validate.OneOf(('clean', 'explicit'))) published_date = fields.String(allow_none=True) planned_pre_roll_count = fields.Integer(required=False, allow_none=True) planned_mid_roll_count = fields.Integer(required=False, allow_none=True) planned_post_roll_count = fields.Integer(required=False, allow_none=True) artwork_filename = fields.String(required=False, allow_none=True) audio_filename = fields.String(required=False, allow_none=True) draft = fields.Boolean(required=True) external_id = fields.String( required=False, validate=validate.Length(max=64), allow_none=True) is_reviewed = fields.Boolean(required=False) apple_id = fields.String( required=False, validate=validate.Length(max=20), allow_none=True) class Meta: """Allow unknown fields to exclude from payload before processing.""" unknown = EXCLUDE class PublishEpisodeSchema(Schema): """Episode validation schema.""" title = fields.String(required=True) description = fields.String(required=True) season_id = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) season_number = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) episode_number = fields.Integer(required=False, allow_none=True, validate=validate.Range(min=1)) episode_type = fields.String( required=True, validate=validate.OneOf(('full', 'trailer', 'bonus'))) trailer_type = fields.String( required=False, allow_none=True, validate=validate.OneOf(('regular', 'cross-promo'))) content = fields.String(required=True, validate=validate.OneOf(('clean', 'explicit'))) published_date = fields.String(required=True) planned_pre_roll_count = fields.Integer(required=False, allow_none=True) planned_mid_roll_count = fields.Integer(required=False, allow_none=True) planned_post_roll_count = fields.Integer(required=False, allow_none=True) artwork_filename = fields.String(required=False, allow_none=True) audio_filename = fields.String(required=False, allow_none=True) draft = fields.Boolean(required=True) external_id = fields.String( required=False, validate=validate.Length(max=64), allow_none=True) is_reviewed = fields.Boolean(required=False) apple_id = fields.String( required=False, validate=validate.Length(max=20), allow_none=True) class Meta: """Allow unknown fields to exclude from payload before processing.""" unknown = EXCLUDE class InsertionPointsPayloadSchema(Schema): """Insertion points validation schema.""" point_type = fields.String(required=True, validate=validate.OneOf(('pre', 'mid', 'post'))) timecode = fields.Decimal(required=True) count = fields.Integer(required=True) class PlannedInventorySchema(Schema): """Planned Inventory validation schema.""" dates = fields.List(fields.String(required=True), required=True) pre_rolls = fields.Integer(required=True, validate=validate.Range(min=0)) post_rolls = fields.Integer(required=True, validate=validate.Range(min=0)) mid_rolls = fields.Integer(required=True, validate=validate.Range(min=0)) title_prefix = fields.String(required=True) class AssetUploadPayloadSchema(Schema): """Asset upload validation schema.""" object_id = fields.String(required=True) object_type = fields.String(required=True) filename = fields.String(required=True) original_filename = fields.String(required=True) asset_type = fields.String(required=True) class Meta: """Allow unknown fields to exclude from payload before processing.""" unknown = EXCLUDE class AdReadSchema(Schema): """Create/Update Ad Read validation schema.""" title = fields.String(required=True) campaign_id = fields.String(required=True) order_id = fields.String(required=True) advertisement_id = fields.String(required=True) roll_type = fields.String(required=True, validate=validate.OneOf(('pre', 'mid', 'post'))) assignee_id = fields.Integer(required=False, validate=validate.Range(min=0)) assignee_ids = fields.List( fields.Integer(required=True), required=False, validate=validate.Length(max=5)) copy_url_path = fields.String(required=True) copy_filename = fields.String() due_date = fields.String(required=True) status = fields.String() rejection_reason = fields.String(required=False, allow_none=True) requires_approval = fields.Boolean(required=True) is_archived_early = fields.Boolean() is_skip_virus_scan = fields.Boolean() class DoesAdReadExistSchema(Schema): """Does Ad Read exist validation schema.""" campaign_id = fields.String(required=True) order_id = fields.String(required=True) advertisement_id = fields.String(required=True) class GetAdReadSchema(Schema): """Ad Read validation schema.""" date_range = fields.String(validate=validate.OneOf(ALLOWED_AD_DATE_RANGE)) is_archived = fields.Boolean() only_show_mine = fields.Boolean() limit = fields.Integer() offset = fields.Integer() class CreateAdReadCommentSchema(Schema): """Ad action comment creation schema.""" ad_action_id = fields.Int(required=True) comment = fields.String(required=True) class Meta: """Allow unknown fields to exclude from payload before processing.""" unknown = EXCLUDE class GetAdReadCommentsSchema(Schema): """Ad Read Comments get validation schema.""" limit = fields.Integer() offset = fields.Integer() class ToggleUserFavoriteSchema(Schema): """User Favorite Schema.""" podcast_id = fields.Integer(required=True, validate=validate.Range(min=0)) class ToggleUserChartFavoriteSchema(Schema): """User Chart Favorite Schema.""" store = fields.String(required=True, allowed_none=False) category = fields.String(required=True, allowed_none=False) country = fields.String(required=True, allowed_none=False) chart_type = fields.String(required=True, validate=validate.OneOf(user_constants.CHART_TYPE)) class UserSettingsSchema(Schema): """User Settings Schema.""" ad_notifications = fields.Boolean(required=False, allow_none=False) podcast_notifications = fields.Boolean(required=False, allow_none=False) transcription_notifications = fields.Boolean(required=False, allow_none=False) spike_notifications = fields.Boolean(required=False, allow_none=False) class CreateUserSchema(Schema): """User creation schema.""" @pre_load def _normalize_email(self, in_data, **kwargs): out_data = in_data.copy() email = in_data.get('email') if email: out_data['email'] = email.strip() return out_data name = fields.String(required=True) email = fields.String(required=True, validate=validate.Email()) network_ids = fields.List(fields.Int, required=False) podcast_ids = fields.List(fields.Int, required=False, allow_none=True) show_family_ids = fields.List(fields.Int, required=False, allow_none=True) role = fields.String(required=True, validate=validate.OneOf(user_constants.ROLES)) organization = fields.String(required=True, validate=validate.OneOf(user_constants.ORGANIZATION)) all_networks = fields.Boolean(required=False, allow_none=False) @validates_schema def validate_network_ids(self, data, **kwargs): """Validate network_ids field.""" if not data.get('all_networks') and not data.get('network_ids'): if data['role'] == user_constants.NETWORK_ADMIN: raise ValidationError(ERROR_MESSAGE_MISSING_REQUIRED_DATA, 'network_ids') class Meta: """Allow unknown fields to exclude from payload before processing.""" unknown = EXCLUDE class UpdateUserSchema(Schema): """User update schema.""" network_ids = fields.List(fields.Int, required=False) podcast_ids = fields.List(fields.Int, required=False) show_family_ids = fields.List(fields.Int, required=False) all_networks = fields.Boolean(required=False, allow_none=False) role = fields.String(required=False, validate=validate.OneOf(user_constants.ROLES)) class UpdateUserLastLoginSchema(Schema): """User last login update schema.""" last_login = fields.DateTime(format='%Y-%m-%dT%H:%M:%S.%fZ') class PodcastLinkSchema(Schema): """Podcast Link schema.""" store_id = fields.Integer(required=True) link = fields.String(required=True) class CreatePodcastLinksSchema(Schema): """Create podcast links schema.""" podcast_id = fields.Integer(required=True) links = fields.List(fields.Nested(PodcastLinkSchema()), required=True) class CreateEpisodeLinkSchema(PodcastLinkSchema): """Create episode link schema.""" podcast_id = fields.Integer(required=True) episode_id = fields.Integer(required=True) class CheckOwnershipSchema(Schema): """Ownership check schema.""" object_type = fields.String( required=True, validate=validate.OneOf(['network', 'podcast', 'episode', 'adupload'])) object_id = fields.String(required=True) class TopAnalyticsSchema(Schema): """Top episodes schema.""" @pre_load def _normalize_country(self, in_data, **kwargs): out_data = in_data.copy() out_data['countries'] = in_data.getlist('countries') out_data['players'] = in_data.getlist('players') out_data['network_id'] = in_data.get('network_id') out_data['podcast_id'] = in_data.get('podcast_id') out_data['sort_field'] = in_data.get('sort_field', 'downloads') if in_data.get('date_range') == CUSTOM: _validate_dates(in_data.get('start_date'), in_data.get('end_date')) return out_data limit = fields.Integer(required=True) offset = fields.Integer(required=True) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) players = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) network_id = fields.Integer(allow_none=True) podcast_id = fields.Integer(allow_none=True) sort_field = fields.String(allow_none=True, validate=validate.OneOf(['downloads', 'published_date'])) sort_order = fields.String(allow_none=True, validate=validate.OneOf(['asc', 'desc'])) start_date = fields.String() end_date = fields.String() def _prepare_download_input(in_data, id_name): out_data = in_data.copy() out_data['countries'] = out_data.getlist('countries') out_data['players'] = out_data.getlist('players') if id_name in in_data: out_data[id_name] = out_data.getlist(id_name) if in_data.get('date_range') == CUSTOM: _validate_dates(in_data.get('start_date'), in_data.get('end_date')) return out_data class ChartableChartSchema(Schema): """Chartable Podcast Chart Schema.""" store = fields.String(required=True) category = fields.String(required=True) country = fields.String(required=True) report_date = fields.String(required=True) class ChartableEpisodeChartSchema(Schema): """Chartable Episode Chart Schema.""" category = fields.String(required=True) country = fields.String(required=True) report_date = fields.String(required=True) class DailyPodcastDownloadsSchema(Schema): """Daily Downloads schema.""" @pre_load def _split_names(self, in_data, **kwargs): return _prepare_download_input(in_data, 'podcast_ids') podcast_ids = fields.List(fields.Integer(), required=True) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) players = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) start_date = fields.String() end_date = fields.String() class DailyEpisodeDownloadsSchema(Schema): """Daily Downloads schema.""" @pre_load def _split_names(self, in_data, **kwargs): return _prepare_download_input(in_data, 'episode_ids') episode_ids = fields.List(fields.Integer(), required=True) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) players = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) start_date = fields.String() end_date = fields.String() class DailyNetworkDownloadsSchema(Schema): """Daily Downloads schema.""" @pre_load def _split_names(self, in_data, **kwargs): return _prepare_download_input(in_data, 'network_ids') network_ids = fields.List(fields.Integer(), required=True) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) players = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) start_date = fields.String() end_date = fields.String() class DailyCountryDownloadsSchema(Schema): """Daily Country Downloads schema.""" @pre_load def _split_names(self, in_data, **kwargs): return _prepare_download_input(in_data, None) object_id = fields.Integer(required=True) object_type = fields.String(required=True, validate=validate.OneOf(ALLOWED_OBJECT_TYPES)) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, validate=validate.Length(min=1, max=10)) players = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) start_date = fields.String() end_date = fields.String() class DailyPlayerDownloadsSchema(Schema): """Daily Player Downloads schema.""" @pre_load def _split_names(self, in_data, **kwargs): return _prepare_download_input(in_data, None) object_id = fields.Integer(required=True) object_type = fields.String(required=True, validate=validate.OneOf(ALLOWED_OBJECT_TYPES)) date_range = fields.String(required=True, validate=validate.OneOf(ALLOWED_DATE_RANGE)) countries = fields.List( fields.String(), required=True, allow_none=True, validate=validate.Length(max=5)) players = fields.List( fields.String(), required=True, validate=validate.Length(min=1, max=10)) start_date = fields.String() end_date = fields.String() class StartTranscriptionSchema(Schema): """Start Transcription schema.""" num_speakers = fields.Integer(required=True, validate=validate.Range(min=1)) language = fields.String(required=True) class GetUsersSchema(Schema): """Get Users Schema.""" @pre_load def _split_names(self, in_data, **kwargs): out_data = in_data.copy() out_data['network_ids'] = map(int, in_data.getlist('network_ids')) out_data['organization'] = in_data['organization'] if in_data.get('organization') else None return out_data limit = fields.Integer() offset = fields.Integer() organization = fields.String(allow_none=True, validate=validate.OneOf(user_constants.ORGANIZATION)) network_ids = fields.List(fields.Int(), validate=validate.Length(max=5), allow_none=True) class CreateNetworkSchema(Schema): """Create Network validation schema.""" name = fields.String(required=True, allow_none=False) code = fields.String(required=True, allow_none=False) is_sony = fields.Boolean(required=False, allow_none=False) class ReplicateSingleEpisodeSchema(Schema): """Replicate Single Episode validation schema.""" original_episode_id = fields.Integer(required=True) podcast_ids = fields.List(fields.Integer(), required=True, validate=validate.Length(min=1)) copy_ad_locations_podcast_ids = fields.List(fields.Integer(), required=False, allow_none=True) class UpdateEpisodeReplicationStatusSchema(Schema): """Update Episode Replication Status validation schema.""" ids = fields.List(fields.Integer(), required=True, validate=validate.Length(min=1)) show_family_id = fields.Integer(required=True) class ReplicateEpisodesSchema(Schema): """Replicate multiple episodes schema.""" original_podcast_id = fields.Integer(required=True) podcast_id = fields.Integer(required=True) original_episode_ids = fields.List(fields.Integer(), required=True) seasons = fields.List(fields.Nested(SeasonSchema()), required=False, allow_none=True) replication_type = fields.String(required=True) is_copy_apple_episode_id = fields.Boolean(required=False, allow_none=False) is_copy_ad_locations = fields.Boolean(required=False, allow_none=True) def validate_episode(data): """Validate episode based on status.""" try: if data['draft'] is False: return PublishEpisodeSchema().load(data) else: return DraftEpisodeSchema().load(data) except ValidationError as err: raise OwsError.bad_request(err.messages)