from datetime import datetime, timezone from typing import List, Type, Callable from sqlalchemy import and_, or_, exists from sqlalchemy.orm import aliased from flask import g from db import db from models.projects import ProjectCampaignStatus from models import Project, Campaign, UserProject, User, user_label, Artist, MarketingAccount, ProjectCampaign from models.recent_search_item import RecentSearchItem, RecentSearchItemType from projects.constants import MAX_RECENTLY_VIEWED_PROJECTS MAX_SEARCHES_TO_STORE = 20 class RecentSearchesRepository: def __validate_project(self, user_id: int): return and_( RecentSearchItem.type == RecentSearchItemType.PROJECT.value, Project.is_deleted.is_(False), # self.__has_access_to_project(Project, UserProject), ).self_group() # we do show entities users don't have access to for now # def __has_access_to_project(self, projectType: Type[Project], userProjectType: Type[UserProject]): # return or_( # userProjectType.id.isnot(None), # user_label.c.label_id == projectType.label_id, # # For unassigned & pending campaigns # or_(projectType.id.is_(None), user_label.c.label_id == MarketingAccount.label_id).self_group(), # ).self_group() def __validate_campaign(self, projectClass: Type[Project], userProjectClass: Type[UserProject]): return and_( RecentSearchItem.type == RecentSearchItemType.CAMPAIGN.value, Campaign.is_deleted.is_(False), # self.__has_access_to_project(projectClass, userProjectClass), ).self_group() def __valid_artist(self): return RecentSearchItem.type == RecentSearchItemType.ARTIST.value def get_recent_searches_for_user(self, user_id: int, limit: int) -> List[RecentSearchItem]: campaignsProject = aliased(Project) campaignsUserProject = aliased(UserProject) return ( db.session.query(RecentSearchItem) .distinct() .join(User, RecentSearchItem.user) .outerjoin(user_label, user_label.c.user_id == User.id) .outerjoin(Project, RecentSearchItem.project) .outerjoin(Campaign, RecentSearchItem.campaign) .outerjoin(MarketingAccount, Campaign.marketing_account) .outerjoin(campaignsProject, Campaign.project) .outerjoin(UserProject, and_(UserProject.project_id == Project.id, UserProject.user_id == user_id)) .outerjoin( campaignsUserProject, and_(campaignsUserProject.project_id == campaignsProject.id, campaignsUserProject.user_id == user_id), ) .filter( RecentSearchItem.user_id == user_id, RecentSearchItem.type != RecentSearchItemType.RECENTLY_VIEWED_PROJECT.value ) .filter( or_( self.__validate_campaign(campaignsProject, campaignsUserProject), self.__validate_project(user_id), self.__valid_artist(), ) ) .order_by(RecentSearchItem.searched_at.desc()) .limit(limit) .all() ) def __get_recent_search_item(self, user_id: int, predicate) -> RecentSearchItem: return ( db.session.query(RecentSearchItem) .filter(and_(RecentSearchItem.user_id == user_id, predicate)) .one_or_none() ) ItemMap = Callable[[RecentSearchItem], RecentSearchItem] def __track_opened_entity_details(self, user_id: int, predicate, item_map: ItemMap): item = self.__get_recent_search_item(user_id, predicate) if not item: item = RecentSearchItem() item.user_id = user_id item = item_map(item) db.session.add(item) item.searched_at = datetime.now(timezone.utc) db.session.commit() def track_opened_project_details(self, project_id: int, user_id: int): def __item_map(item: RecentSearchItem) -> RecentSearchItem: item.project_id = project_id item.type = RecentSearchItemType.PROJECT.value return item predicate = and_(RecentSearchItem.project_id == project_id, RecentSearchItem.type == RecentSearchItemType.PROJECT.value) self.__track_opened_entity_details(user_id, predicate, __item_map) def track_recently_viewed_project(self, project_id: int, user_id: int): def __item_map(item: RecentSearchItem) -> RecentSearchItem: item.project_id = project_id item.type = RecentSearchItemType.RECENTLY_VIEWED_PROJECT.value return item predicate = and_(RecentSearchItem.project_id == project_id, RecentSearchItem.type == RecentSearchItemType.RECENTLY_VIEWED_PROJECT.value) self.__track_opened_entity_details(user_id, predicate, __item_map) def track_opened_campaign_details(self, campaign_uuid: str, user_id: int): campaign = db.session.query(Campaign).filter(Campaign.uuid == campaign_uuid).one_or_none() def __item_map(item: RecentSearchItem) -> RecentSearchItem: item.campaign_id = campaign.id item.type = RecentSearchItemType.CAMPAIGN.value return item self.__track_opened_entity_details(user_id, RecentSearchItem.campaign_id == campaign.id, __item_map) def track_opened_artist_details(self, artist_external_id: str, user_id: int): artist_id = db.session.query(Artist.id).filter(Artist.external_id == artist_external_id).scalar() def __item_map(item: RecentSearchItem) -> RecentSearchItem: item.artist_id = artist_id item.type = RecentSearchItemType.ARTIST.value return item self.__track_opened_entity_details(user_id, RecentSearchItem.artist_id == artist_id, __item_map) def __clean_old_items(self, ids_to_delete): ( db.session.query(RecentSearchItem) .filter(RecentSearchItem.id.in_(ids_to_delete)) .delete(synchronize_session="fetch") ) db.session.commit() def clean_old_searches(self, user_id): ids_to_delete = ( db.session.query(RecentSearchItem.id) .filter( RecentSearchItem.user_id == user_id, RecentSearchItem.type != RecentSearchItemType.RECENTLY_VIEWED_PROJECT.value ) .order_by(RecentSearchItem.searched_at.desc()) .offset(MAX_SEARCHES_TO_STORE) .subquery() ) self.__clean_old_items(ids_to_delete) def clean_old_recently_viewed_projects(self, user_id: int): ids_to_delete = ( db.session.query(RecentSearchItem.id) .filter( RecentSearchItem.user_id == user_id, RecentSearchItem.type == RecentSearchItemType.RECENTLY_VIEWED_PROJECT.value ) .order_by(RecentSearchItem.searched_at.desc()) .offset(MAX_RECENTLY_VIEWED_PROJECTS) .subquery() ) self.__clean_old_items(ids_to_delete) def projects_by_campaign(self, campaign_id: int) -> List[Project]: return ( db.session.query(Project) .outerjoin(user_label, and_(user_label.c.label_id == Project.label_id, user_label.c.user_id == g.user_id)) .outerjoin( ProjectCampaign, and_( Project.id == ProjectCampaign.project_id, ProjectCampaign.status != ProjectCampaignStatus.REJECTED.value, ), ) .outerjoin(Campaign, or_(Project.id == Campaign.project_id, ProjectCampaign.campaign_id == Campaign.id)) .outerjoin(UserProject, UserProject.user_id == g.user_id) .filter( Campaign.id == campaign_id, Project.is_deleted.is_(False), or_( # We need to determine that user has access to this project UserProject.id.isnot(None), and_(user_label.c.user_id.isnot(None), user_label.c.label_id.isnot(None)), ), ) .all() ) def is_campaign_pending_for_project(self, project_id: int, campaign_id: int) -> bool: return db.session.query( exists().where( and_( ProjectCampaign.campaign_id == campaign_id, ProjectCampaign.status == ProjectCampaignStatus.PENDING.value, ProjectCampaign.project_id == project_id, ) ) ).scalar()