import uuid from datetime import date, datetime from dataclasses import dataclass, field from typing import Optional, List, Any from marshmallow import validate, EXCLUDE from models.campaign import Campaign, CampaignTypes from projects.schemas import ProjectTargetSchema from campaigns.constants import ( PAGE_LIMIT_DEFAULT, ALL_STATUS, PENDING_STATUS ) from marshmallow_dataclass import NewType from config import TIMEZONE from models.prs_purchase_order import PRSPurchaseOrder, PRSPurchaseOrderStatus from shared.schemas import CampaignLinkfireLinkListMapper, LinkfireLinkResponseSchema from shared.validators import UniqueValidator from services.territory.constants import GLOBAL_TERRITORY_ID from constants.currency import DEFAULT_CURRENCY from api import ma import marshmallow_dataclass from flask_fsm import state_map CampaignTypeID = NewType("CampaignTypeID", int, validate=validate.Range(min=1)) CampaignStatus = NewType("CampaignStatus", int, validate=validate.Range(min=1)) TerritoryID = NewType("TerritoryID", int) DEFAULT_GENDERS = [1, 2, 3] DEFAULT_AGE_RANGE = [13, 65] @dataclass class TerritoryResponseSchema: id: int name: str @dataclass class HistoryUserResponseSchema: id: str email: str @dataclass class CampaignDetailsObjectiveResponseSchema: id: int name: str @dataclass class CampaignDetailsProviderResponseSchema: id: int name: str @dataclass class CampaignDetailsGenderResponseSchema: id: int name: str @dataclass class CampaignDetailsAudienceAgeResponseSchema: lowerAge: Optional[int] higherAge: Optional[int] @dataclass class CampaignObjectiveResponse: id: int name: str @dataclass class CampaignGoalResponse: id: int name: str objectives: List[CampaignObjectiveResponse] = field(default_factory=list) @dataclass class CampaignTypeCityPlacementsSchema: city_id: Optional[int] city_name: Optional[str] placements: Optional[int] = 0 @dataclass class CampaignPlatformsNamesList: items: List[str] @dataclass class CampaignTypeGroupSchema: id: int name: str @dataclass class CampaignTypeSchema: id: int name: str category: CampaignTypeGroupSchema def __init__(self, campaign_type: CampaignTypes): self.id = campaign_type.id self.name = campaign_type.name self.category = CampaignTypeGroupSchema(id=campaign_type.group.id, name=campaign_type.group.name) @dataclass class CampaignCreateSchema: startDate: date endDate: date platforms: List[int] = field(metadata={"validate": validate.Length(min=1)}) destinationLinks: Optional[List[str]] = field(metadata={"validate": [UniqueValidator(), validate.Length(max=20)]}) campaignType: Optional[int] gratis: Optional[bool] notes: Optional[str] audienceNotes: Optional[str] adCreativeNotes: Optional[str] kpiMetricsFieldId: Optional[int] provider: Optional[int] objective: Optional[int] namingConvention: Optional[str] = field(metadata={"validate": validate.Length(max=5000)}) budgetSpend: Optional[float] = field(metadata={"validate": lambda n: -99999999 <= n <= 99999999}) ecpm: Optional[float] = field(metadata={"validate": lambda n: -99999999 <= n <= 99999999}) name: Optional[str] = field(default=" ", metadata={"validate": validate.Length(max=5000)}) plannedBudget: Optional[float] = field(default=0, metadata={"validate": lambda n: -99999999 <= n <= 99999999}) workflowStatus: Optional[str] = field(default='Draft') genders: Optional[List[int]] = field(default_factory=lambda: DEFAULT_GENDERS) territories: Optional[List[TerritoryID]] = field( default_factory=lambda: [GLOBAL_TERRITORY_ID], metadata={"validate": validate.Length(max=1000)} ) audienceLowerAge: Optional[int] = field(default=DEFAULT_AGE_RANGE[0]) audienceHigherAge: Optional[int] = field(default=DEFAULT_AGE_RANGE[1]) linkfireLinks: Optional[List[LinkfireLinkResponseSchema]] = field(default_factory=list) placements: Optional[List[int]] = field(default_factory=list, metadata={"validate": UniqueValidator()}) adCreativeLinks: List[Optional[str]] = field(default_factory=list) campaign_create_schema = marshmallow_dataclass.class_schema(CampaignCreateSchema, base_schema=ma.Schema) @dataclass class CampaignNameGenerateSchema: startDate: Optional[date] platforms: Optional[List[int]] = field(metadata={"validate": validate.Length(min=1)}) provider: Optional[int] objective: Optional[int] territories: Optional[List[TerritoryID]] = field( default_factory=lambda: [GLOBAL_TERRITORY_ID], metadata={"validate": validate.Length(max=1000)} ) placements: Optional[List[int]] = field(default_factory=list, metadata={"validate": UniqueValidator()}) campaignType: Optional[int] = field(default=None) audienceNotes: Optional[str] = field(default=None) genders: Optional[List] = field(default_factory=list) creativeDescription: Optional[str] = field(default=None) lowerAge: Optional[int] = field(default=None) higherAge: Optional[int] = field(default=None) class Meta: unknown = EXCLUDE @dataclass class CampaignNameGenerateResponseSchema: name: str @dataclass class CampaignsBulkCreateSchema: campaigns: List[CampaignCreateSchema] = field(default_factory=list) campaigns_bulk_create_schema = marshmallow_dataclass.class_schema(CampaignsBulkCreateSchema, base_schema=ma.Schema) @dataclass class CampaignActionsRequestModel: campaignUuid: str campaign_actions_reqest_schema = marshmallow_dataclass.class_schema(CampaignActionsRequestModel, base_schema=ma.Schema) @dataclass class CampaignsBulkActionsRequestModel: campaignUuids: List[str] = field(default_factory=list) campaigns_bulk_actions_request_model = marshmallow_dataclass.class_schema(CampaignsBulkActionsRequestModel, base_schema=ma.Schema) @dataclass class CampaignScheduleUpdateSchema: startDate: date endDate: date @dataclass class CampaignExportQueryParams: timeZone: Optional[str] = field(default=TIMEZONE) @dataclass class CampaignListQueryParams: freeText: Optional[str] = field(metadata={"validate": validate.Length(min=1, max=500)}) earliestStartDate: Optional[date] latestEndDate: Optional[date] territories: Optional[List[TerritoryID]] = field(metadata={"validate": validate.Length(min=1)}) isDigital: Optional[bool] = field(default=False) campaign_list_query_params = marshmallow_dataclass.class_schema(CampaignListQueryParams, base_schema=ma.Schema) @dataclass class CampaignEditableFields: budget: bool = True spend: bool = True startDate: bool = True endDate: bool = True genders: bool = True audienceAge: bool = True countries: bool = True destinations: bool = True notes: bool = True provider: bool = True name: bool = True objective: bool = True typeId: bool = True platforms: bool = True gratis: bool = True @dataclass class CampaignMetadata: fields: CampaignEditableFields @dataclass class CampaignPlatformSchema: id: int name: str labelId: int @dataclass class CampaignPlacementSchema: id: int name: str labelId: int @dataclass class CampaignStatusSchema: status: str available_transitions: Optional[List[str]] = field(default=None, metadata={"data_key": "availableTransitions"}) @dataclass class CampaignResponseSchema: id: int uuid: str projectId: int name: Optional[str] namingConvention: Optional[str] plannedBudget: Optional[float] budgetSpend: Optional[float] currency: Optional[str] startDate: Optional[date] endDate: Optional[date] territories: Optional[List[TerritoryResponseSchema]] createUser: Optional[HistoryUserResponseSchema] editUser: Optional[HistoryUserResponseSchema] editDate: datetime status: int workflowStatus: Optional[CampaignStatusSchema] objective: Optional[CampaignDetailsObjectiveResponseSchema] notes: Optional[str] provider: Optional[CampaignDetailsProviderResponseSchema] genders: Optional[List[CampaignDetailsGenderResponseSchema]] destinationLinks: Optional[List[str]] audienceAge: Optional[CampaignDetailsAudienceAgeResponseSchema] externalId: Optional[str] isPending: bool isEditable: bool metadata: Any ecpm: Optional[float] source: str platforms: Optional[List[CampaignPlatformSchema]] type: Optional[CampaignTypeSchema] gratis: bool placements: Optional[List[CampaignPlacementSchema]] def __init__(self, campaign: Campaign, is_pending: bool, currency): self.id = campaign.id self.uuid = campaign.uuid self.projectId = campaign.project_id self.name = campaign.name self.namingConvention = campaign.naming_convention self.plannedBudget = campaign.planned_budget self.budgetSpend = campaign.budget_spend if currency is not None: self.currency = currency else: self.currency = DEFAULT_CURRENCY self.startDate = campaign.start_date self.endDate = campaign.fallback_end_date self.source = campaign.formatted_source self.territories = [TerritoryResponseSchema(at.territory.id, at.territory.name) for at in campaign.territories] if campaign.create_user: self.createUser = HistoryUserResponseSchema(campaign.create_user.external_id, campaign.create_user.email) if campaign.edit_user: self.editUser = HistoryUserResponseSchema(campaign.edit_user.external_id, campaign.edit_user.email) self.editDate = campaign.updated_at self.status = campaign.status_id self.workflowStatus = CampaignStatusSchema( status=campaign.workflow_status, available_transitions=state_map.get_target(campaign.workflow_status) ) if campaign.objective: self.objective = CampaignDetailsObjectiveResponseSchema(campaign.objective.id, campaign.objective.name) self.notes = campaign.notes if campaign.provider_id: self.provider = CampaignDetailsProviderResponseSchema(campaign.provider.id, campaign.provider.name) self.genders = [CampaignDetailsGenderResponseSchema(gender.id, gender.name) for gender in campaign.genders] if campaign.age_range: lower = ( campaign.age_range.lower if campaign.age_range and isinstance(campaign.age_range.lower, int) else None ) upper = ( campaign.age_range.upper - 1 if campaign.age_range and isinstance(campaign.age_range.upper, int) else None ) self.audienceAge = CampaignDetailsAudienceAgeResponseSchema(lower, upper) self.destinationLinks = [destination.link for destination in campaign.links] self.isPending = is_pending or False self.isEditable = campaign.is_editable self.externalId = campaign.external_id self.metadata = campaign.metainfo self.platforms = [CampaignPlatformSchema(id=p.id, name=p.name, labelId=p.label_id) for p in campaign.platforms] self.gratis = campaign.gratis self.ecpm = campaign.ecpm if campaign.type: self.type = CampaignTypeSchema(campaign.type) self.placements = [ CampaignPlacementSchema(id=p.id, name=p.name, labelId=p.label_id) for p in campaign.placements ] campaign_response_schema = marshmallow_dataclass.class_schema(CampaignResponseSchema, base_schema=ma.Schema)(many=True) @dataclass class PurchaseOrderLinkSchema: id: int poNumber: str name: str status: str def __init__(self, purchase_order: PRSPurchaseOrder): self.id = purchase_order.id self.poNumber = str(purchase_order.po_number) self.name = purchase_order.name self.status = PRSPurchaseOrderStatus.status_name(purchase_order.status) @dataclass class CampaignDetailsResponseSchema(CampaignResponseSchema): notes: str workflowStatus: Optional[CampaignStatusSchema] audienceNotes: Optional[str] currency: Optional[str] adCreativeNotes: Optional[str] externalName: Optional[str] kpiMetricsFieldId: Optional[int] purchaseOrder: Optional[PurchaseOrderLinkSchema] adCreativeLinks: Optional[List[str]] = field(default_factory=list) linkfireLinks: Optional[List[LinkfireLinkResponseSchema]] = field(default_factory=list) def __init__(self, campaign: Campaign, phases, is_pending: bool, currency: str): super().__init__(campaign, is_pending, currency) self.notes = campaign.notes if campaign.purchase_order: self.purchaseOrder = PurchaseOrderLinkSchema(campaign.purchase_order) if campaign.linkfire_campaigns: self.linkfireLinks = CampaignLinkfireLinkListMapper.map(campaign.linkfire_campaigns) if campaign.audience_notes: self.audienceNotes = campaign.audience_notes if campaign.ad_creative_links: self.adCreativeLinks = [ad_creative_link.url for ad_creative_link in campaign.ad_creative_links] if campaign.ad_creative_notes: self.adCreativeNotes = campaign.ad_creative_notes if campaign.kpi_metrics_field_id: self.kpiMetricsFieldId = campaign.kpi_metrics_field_id if campaign.workflow_status: self.workflowStatus = CampaignStatusSchema( status=campaign.workflow_status, available_transitions=state_map.get_target(campaign.workflow_status) ) if campaign.external_name: self.externalName = campaign.external_name campaign_details_response_schema = marshmallow_dataclass.class_schema(CampaignDetailsResponseSchema, base_schema=ma.Schema) @dataclass class CampaignAssignSchema: id: int uuid: str source: str platforms: List[CampaignPlatformSchema] type: Optional[CampaignTypeSchema] gratis: bool def __init__(self, campaign: Campaign): self.id = campaign.id self.uuid = campaign.uuid self.source = campaign.formatted_source self.platforms = [CampaignPlatformSchema(id=p.id, name=p.name, labelId=p.label_id) for p in campaign.platforms] self.gratis = campaign.gratis if campaign.type: self.type = CampaignTypeSchema(campaign.type) campaign_assign_schema = marshmallow_dataclass.class_schema(CampaignAssignSchema, base_schema=ma.Schema) @dataclass class CampaignWidgetResponseModel: id: int name: str max_length: Optional[int] @dataclass class CampaignTypeResponseModel: id: int name: str @dataclass class CampaignCategoryResponseModel: id: int name: str campaignTypes: Optional[List[CampaignTypeResponseModel]] @dataclass class CampaignPlatformResponseModel: id: int name: str labelId: Optional[int] defaultPlacements: Optional[List[int]] = field(default_factory=list) @dataclass class CampaignPlatformGroupResponseModel: id: int name: str type: str items: List[CampaignPlatformResponseModel] @dataclass class CampaignPlacementResponseModel: id: int name: str labelId: int @dataclass class CampaignTypesResponseModel: categories: List[CampaignCategoryResponseModel] platforms: List[CampaignPlatformGroupResponseModel] placements: List[CampaignPlacementResponseModel] @dataclass class CampaignTabRequestModel: platforms: Optional[List[str]] freeText: Optional[str] = field(metadata={"validate": validate.Length(min=1, max=500)}) pending: Optional[bool] = field(default=False) status: int = field(default=ALL_STATUS, metadata={"validate": validate.OneOf([ALL_STATUS, PENDING_STATUS])}) limit: Optional[int] = field(default=PAGE_LIMIT_DEFAULT, metadata={"validate": validate.Range(min=1)}) offset: Optional[int] = field(default=0, metadata={"validate": validate.Range(min=0)}) sort: Optional[str] = field(default="name") campaign_tab_request_schema = marshmallow_dataclass.class_schema(CampaignTabRequestModel, base_schema=ma.Schema) @dataclass class CampaignTabSchema: id: int uuid: str externalId: Optional[str] name: str platforms: List[str] adSetsCount: int plannedBudget: Optional[int] budgetSpend: Optional[int] currency: str startDate: date endDate: Optional[date] editDate: datetime source: str metadata: Any isEditable: bool platforms: List[CampaignPlatformSchema] type: Optional[CampaignTypeSchema] gratis: bool isPending: bool = field(default=False) linkfireLinks: Optional[List[LinkfireLinkResponseSchema]] = field(default_factory=list) def __init__(self, campaign: Campaign, is_pending: bool, currency): self.id = campaign.id self.uuid = campaign.uuid self.externalId = campaign.external_id self.name = campaign.name self.budgetSpend = campaign.budget_spend self.plannedBudget = campaign.planned_budget if currency is not None: self.currency = currency else: self.currency = DEFAULT_CURRENCY self.startDate = campaign.start_date self.endDate = campaign.fallback_end_date self.editDate = campaign.updated_at self.platforms = campaign.platform_value or [] self.adSetsCount = campaign.ads_count self.isPending = is_pending self.isEditable = campaign.is_editable self.metadata = campaign.metainfo self.source = campaign.formatted_source self.linkfireLinks = CampaignLinkfireLinkListMapper.map(campaign.linkfire_campaigns) self.platforms = [CampaignPlatformSchema(id=p.id, name=p.name, labelId=p.label_id) for p in campaign.platforms] self.gratis = campaign.gratis if campaign.type: self.type = CampaignTypeSchema(campaign.type) @dataclass class CampaignsTabResponseSchema: items: List[CampaignTabSchema] limit: int offset: int total: int campaigns_tab_response_schema = marshmallow_dataclass.class_schema(CampaignsTabResponseSchema, base_schema=ma.Schema) @dataclass class AssignableProjectByCampaignQueryParams: query: Optional[str] @dataclass class AssignableProjectSchema: id: int name: str targets: ProjectTargetSchema isConfidential: bool @dataclass class AssignableProjectsResponseSchema: items: List[AssignableProjectSchema] @dataclass class ProjectCampaignPlatformSchema: withPending: bool = field(default=True) @dataclass class FieldValidator: is_edit_allowed: bool expected: Any actual: Any hint: str def is_valid(self) -> bool: return self.is_edit_allowed or self.expected == self.actual def error_message(self) -> str: return f"{self.hint} field is restricted to edit. Received: {self.actual} but expected: {self.expected}" @dataclass class CampaignProviderQuery: onlyExisting: bool = field(default=False) @dataclass class UnassignedCampaignsRequestModel: freeText: Optional[str] = field(metadata={"validate": validate.Length(min=1, max=500)}) limit: Optional[int] = field(default=PAGE_LIMIT_DEFAULT, metadata={"validate": validate.Range(min=1)}) offset: Optional[int] = field(default=0, metadata={"validate": validate.Range(min=0)}) sort: Optional[str] = field(default="-updatedDate") unassigned_campaigns_request_model = marshmallow_dataclass.class_schema(UnassignedCampaignsRequestModel, base_schema=ma.Schema) @dataclass class ManageExternalCampaignsRequestModel: assign_campaigns_uuids: Optional[List[uuid.UUID]] = field( default_factory=list, metadata={"data_key": "assignCampaignsUuids"} ) unassign_campaigns_uuids: Optional[List[uuid.UUID]] = field( default_factory=list, metadata={"data_key": "unassignCampaignsUuids"} ) manage_external_campaigns_request_model = marshmallow_dataclass.class_schema(ManageExternalCampaignsRequestModel, base_schema=ma.Schema) @dataclass class CreatePlacementSchema: name: str @dataclass class PlacementResponseModel: id: int name: str = field(metadata={"validate": validate.Length(max=20)}) label_id: int @dataclass class CampaignTypesRequestSchema: labelIds: List[int] @dataclass class CampaignKpiMetricsSchema: id: int name: str