def test_stalker_entity_decoder_will_not_create_existing_tasks(create_db, create_empty_project): """testing if JSON decoder will not recreate existing data """ from stalker import Task project = create_empty_project import json from anima.utils import task_hierarchy_io global __here__ file_path = os.path.join(__here__, "data", "test_template3.json") with open(file_path) as f: data = json.load(f) decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data) from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() # now there should be only one Assets task from stalker import Task assets_tasks = Task.query.filter(Task.name=='Assets').all() assert len(assets_tasks) == 1
def test_stalker_entity_decoder_will_create_versions(create_db, create_empty_project): """testing if JSON decoder will create new versions along with tasks """ from stalker import Task project = create_empty_project import json from anima.utils import task_hierarchy_io global __here__ file_path = os.path.join(__here__, "data", "test_template5.json") with open(file_path) as f: data = json.load(f) decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data) from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() from stalker import Asset, Task ananas_asset = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Ananas')\ .first() ananas_look_dev = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='lookDev')\ .first() # check versions are created normally assert len(ananas_look_dev.versions) == 1
def show_context_menu(self, position): """the custom context menu """ # convert the position to global screen position global_position = self.mapToGlobal(position) index = self.indexAt(position) model = self.model() item = model.itemFromIndex(index) logger.debug('itemAt(position) : %s' % item) task_id = None entity = None # if not item: # return # if item and not hasattr(item, 'task'): # return # from anima.ui.models.task import TaskItem # if not isinstance(item, TaskItem): # return if item: try: if item.task: task_id = item.task.id except AttributeError: return # if not task_id: # return from anima import utils file_browser_name = utils.file_browser_name() # create the menu menu = QtWidgets.QMenu() # Open in browser # ----------------------------------- # actions created in different scopes create_time_log_action = None create_project_action = None update_task_action = None upload_thumbnail_action = None create_child_task_action = None duplicate_task_hierarchy_action = None delete_task_action = None export_to_json_action = None import_from_json_action = None no_deps_action = None create_project_structure_action = None create_task_structure_action = None update_project_action = None assign_users_action = None open_in_web_browser_action = None open_in_file_browser_action = None copy_url_action = None copy_id_to_clipboard = None fix_task_status_action = None change_status_menu_actions = [] from anima import defaults from stalker import LocalSession local_session = LocalSession() logged_in_user = local_session.logged_in_user from stalker import SimpleEntity, Task, Project # TODO: Update this to use only task_id if task_id: entity = SimpleEntity.query.get(task_id) reload_action = menu.addAction(u'\uf0e8 Reload') # sub menus create_sub_menu = menu.addMenu('Create') update_sub_menu = menu.addMenu('Update') if defaults.is_power_user(logged_in_user): # create the Create Project menu item create_project_action = \ create_sub_menu.addAction(u'\uf0e8 Create Project...') if isinstance(entity, Project): # this is a project! if defaults.is_power_user(logged_in_user): update_project_action = \ update_sub_menu.addAction(u'\uf044 Update Project...') assign_users_action = \ menu.addAction(u'\uf0c0 Assign Users...') create_project_structure_action = \ create_sub_menu.addAction( u'\uf115 Create Project Structure' ) create_child_task_action = \ create_sub_menu.addAction( u'\uf0ae Create Child Task...' ) # Export and Import JSON create_sub_menu.addSeparator() # export_to_json_action = \ # create_sub_menu.addAction(u'\uf1f8 Export To JSON...') import_from_json_action = \ create_sub_menu.addAction(u'\uf1f8 Import From JSON...') if entity: # separate the Project and Task related menu items menu.addSeparator() open_in_web_browser_action = \ menu.addAction(u'\uf14c Open In Web Browser...') open_in_file_browser_action = \ menu.addAction(u'\uf07c Browse Folders...') copy_url_action = menu.addAction(u'\uf0c5 Copy URL') copy_id_to_clipboard = \ menu.addAction(u'\uf0c5 Copy ID to clipboard') if isinstance(entity, Task): # this is a task create_task_structure_action = \ create_sub_menu.addAction( u'\uf115 Create Task Folder Structure' ) task = entity from stalker import Status status_wfd = Status.query.filter(Status.code == 'WFD').first() status_prev = \ Status.query.filter(Status.code == 'PREV').first() status_cmpl = \ Status.query.filter(Status.code == 'CMPL').first() if logged_in_user in task.resources \ and task.status not in [status_wfd, status_prev, status_cmpl]: create_sub_menu.addSeparator() create_time_log_action = \ create_sub_menu.addAction(u'\uf073 Create TimeLog...') # Add Depends To menu menu.addSeparator() depends = task.depends if depends: depends_to_menu = menu.addMenu(u'\uf090 Depends To') for dTask in depends: action = depends_to_menu.addAction(dTask.name) action.task = dTask # Add Dependent Of Menu dependent_of = task.dependent_of if dependent_of: dependent_of_menu = menu.addMenu(u'\uf08b Dependent Of') for dTask in dependent_of: action = dependent_of_menu.addAction(dTask.name) action.task = dTask if not depends and not dependent_of: no_deps_action = menu.addAction(u'\uf00d No Dependencies') no_deps_action.setEnabled(False) # update task and create child task menu items menu.addSeparator() if defaults.is_power_user(logged_in_user): create_sub_menu.addSeparator() update_task_action = \ update_sub_menu.addAction(u'\uf044 Update Task...') upload_thumbnail_action = \ update_sub_menu.addAction( u'\uf03e Upload Thumbnail...' ) # Export and Import JSON create_sub_menu.addSeparator() export_to_json_action = \ create_sub_menu.addAction(u'\uf1f8 Export To JSON...') import_from_json_action = \ create_sub_menu.addAction(u'\uf1f8 Import From JSON...') create_sub_menu.addSeparator() create_child_task_action = \ create_sub_menu.addAction( u'\uf0ae Create Child Task...' ) duplicate_task_hierarchy_action = \ create_sub_menu.addAction( u'\uf0c5 Duplicate Task Hierarchy...' ) delete_task_action = \ menu.addAction(u'\uf1f8 Delete Task...') menu.addSeparator() # create the status_menu status_menu = update_sub_menu.addMenu('Status') fix_task_status_action = \ status_menu.addAction(u'\uf0e8 Fix Task Status') assert isinstance(status_menu, QtWidgets.QMenu) status_menu.addSeparator() # get all task statuses from anima import defaults menu_style_sheet = '' defaults_status_colors = defaults.status_colors for status_code in defaults.status_colors: change_status_menu_action = \ status_menu.addAction(status_code) change_status_menu_action.setObjectName( 'status_%s' % status_code ) change_status_menu_actions.append( change_status_menu_action ) menu_style_sheet = "%s %s" % ( menu_style_sheet, "QMenu#status_%s { background: %s %s %s}" % ( status_code, defaults_status_colors[status_code][0], defaults_status_colors[status_code][1], defaults_status_colors[status_code][2], ) ) # change the BG Color of the status status_menu.setStyleSheet(menu_style_sheet) try: # PySide and PySide2 accepted = QtWidgets.QDialog.DialogCode.Accepted except AttributeError: # PyQt4 accepted = QtWidgets.QDialog.Accepted selected_item = menu.exec_(global_position) if selected_item: if selected_item is reload_action: if isinstance(entity, Project): self.fill() self.find_and_select_entity_item(item.task) else: item.reload() if create_project_action \ and selected_item is create_project_action: from anima.ui import project_dialog project_main_dialog = project_dialog.MainDialog( parent=self, project=None ) project_main_dialog.exec_() result = project_main_dialog.result() # refresh the task list if result == accepted: self.fill() project_main_dialog.deleteLater() if entity: from anima import defaults url = 'http://%s/%ss/%s/view' % ( defaults.stalker_server_internal_address, entity.entity_type.lower(), entity.id ) if selected_item is open_in_web_browser_action: import webbrowser webbrowser.open(url) elif selected_item is open_in_file_browser_action: from anima import utils try: utils.open_browser_in_location(entity.absolute_path) except IOError as e: QtWidgets.QMessageBox.critical( self, "Error", "%s" % e, QtWidgets.QMessageBox.Ok ) elif selected_item is copy_url_action: clipboard = QtWidgets.QApplication.clipboard() clipboard.setText(url) # and warn the user about a new version is created and the # clipboard is set to the new version full path QtWidgets.QMessageBox.warning( self, "URL Copied To Clipboard", "URL:<br><br>%s<br><br>is copied to clipboard!" % url, QtWidgets.QMessageBox.Ok ) elif selected_item is copy_id_to_clipboard: clipboard = QtWidgets.QApplication.clipboard() clipboard.setText('%s' % entity.id) # and warn the user about a new version is created and the # clipboard is set to the new version full path QtWidgets.QMessageBox.warning( self, "ID Copied To Clipboard", "ID %s is copied to clipboard!" % entity.id, QtWidgets.QMessageBox.Ok ) elif selected_item is create_time_log_action: from anima.ui import time_log_dialog time_log_dialog_main_dialog = time_log_dialog.MainDialog( parent=self, task=entity, ) time_log_dialog_main_dialog.exec_() result = time_log_dialog_main_dialog.result() time_log_dialog_main_dialog.deleteLater() if result == accepted: # refresh the task list if item.parent: item.parent.reload() else: self.fill() # reselect the same task self.find_and_select_entity_item(entity) elif selected_item is update_task_action: from anima.ui import task_dialog task_main_dialog = task_dialog.MainDialog( parent=self, task=entity ) task_main_dialog.exec_() result = task_main_dialog.result() task_main_dialog.deleteLater() # refresh the task list if result == accepted: # just reload the same item if item.parent: item.parent.reload() else: # reload the entire self.fill() self.find_and_select_entity_item(entity) elif selected_item is upload_thumbnail_action: from anima.ui import utils as ui_utils thumbnail_full_path = ui_utils.choose_thumbnail(self) # if the thumbnail_full_path is empty do not do anything if thumbnail_full_path == "": return # get the current task ui_utils.upload_thumbnail(entity, thumbnail_full_path) elif selected_item is create_child_task_action: from anima.ui import task_dialog task_main_dialog = task_dialog.MainDialog( parent=self, parent_task=entity ) task_main_dialog.exec_() result = task_main_dialog.result() task = task_main_dialog.task task_main_dialog.deleteLater() if result == accepted and task: # reload the parent item if item.parent: item.parent.reload() else: self.fill() self.find_and_select_entity_item(task) elif selected_item is duplicate_task_hierarchy_action: duplicate_task_hierarchy_dialog = \ DuplicateTaskHierarchyDialog( parent=self, duplicated_task_name=item.task.name ) duplicate_task_hierarchy_dialog.exec_() result = duplicate_task_hierarchy_dialog.result() if result == accepted: new_task_name = \ duplicate_task_hierarchy_dialog.line_edit.text() keep_resources = \ duplicate_task_hierarchy_dialog\ .check_box.checkState() from anima import utils from stalker import Task task = Task.query.get(item.task.id) new_task = utils.duplicate_task_hierarchy( task, None, new_task_name, description='Duplicated from Task(%s)' % task.id, user=logged_in_user, keep_resources=keep_resources ) if new_task: from stalker.db.session import DBSession DBSession.commit() item.parent.reload() self.find_and_select_entity_item(new_task) elif selected_item is delete_task_action: answer = QtWidgets.QMessageBox.question( self, 'Delete Task?', "Delete the task and children?<br><br>(NO UNDO!!!!)", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) if answer == QtWidgets.QMessageBox.Yes: from stalker.db.session import DBSession from stalker import Task task = Task.query.get(item.task.id) DBSession.delete(task) DBSession.commit() # reload the parent if item.parent: item.parent.reload() else: self.fill() self.find_and_select_entity_item(item.parent.task) elif selected_item is export_to_json_action: # show a file browser dialog = QtWidgets.QFileDialog(self, "Choose file") dialog.setNameFilter("JSON Files (*.json)") dialog.setFileMode(QtWidgets.QFileDialog.AnyFile) if dialog.exec_(): file_path = dialog.selectedFiles()[0] if file_path: import os import json from anima.utils import task_hierarchy_io # check file extension parts = os.path.splitext(file_path) if not parts[1]: file_path = '%s%s' % (parts[0], '.json') data = json.dumps( entity, cls=task_hierarchy_io.StalkerEntityEncoder, check_circular=False, indent=4 ) try: with open(file_path, 'w') as f: f.write(data) except Exception as e: pass finally: QtWidgets.QMessageBox.information( self, 'Task data Export to JSON!', 'Task data Export to JSON!', ) elif selected_item is import_from_json_action: # show a file browser dialog = QtWidgets.QFileDialog(self, "Choose file") dialog.setNameFilter("JSON Files (*.json)") dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) if dialog.exec_(): file_path = dialog.selectedFiles()[0] if file_path: import json with open(file_path) as f: data = json.load(f) from anima.utils import task_hierarchy_io if isinstance(entity, Task): project = entity.project elif isinstance(entity, Project): project = entity parent = None if isinstance(entity, Task): parent = entity decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data, parent=parent) try: from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() except Exception as e: QtWidgets.QMessageBox.critical( self, "Error!", "%s" % e, QtWidgets.QMessageBox.Ok ) else: item.reload() QtWidgets.QMessageBox.information( self, 'New Tasks are created!', 'New Tasks are created', ) elif selected_item is create_project_structure_action: answer = QtWidgets.QMessageBox.question( self, 'Create Project Folder Structure?', "This will create project folders, OK?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) if answer == QtWidgets.QMessageBox.Yes: from anima import utils try: utils.create_project_structure(entity) except Exception as e: pass finally: QtWidgets.QMessageBox.information( self, 'Project Folder Structure is created!', 'Project Folder Structure is created!', ) else: return elif selected_item is create_task_structure_action: answer = QtWidgets.QMessageBox.question( self, 'Create Folder Structure?', "This will create task folders, OK?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) if answer == QtWidgets.QMessageBox.Yes: from anima import utils try: utils.create_task_structure(entity) except Exception as e: pass finally: QtWidgets.QMessageBox.information( self, 'Folder Structure is created!', 'Folder Structure is created!', ) else: return elif selected_item is fix_task_status_action: from stalker import Task if isinstance(entity, Task): from anima import utils utils.fix_task_statuses(entity) from stalker.db.session import DBSession DBSession.add(entity) DBSession.commit() if item.parent: item.parent.reload() elif selected_item is update_project_action: from anima.ui import project_dialog project_main_dialog = project_dialog.MainDialog( parent=self, project=entity ) project_main_dialog.exec_() result = project_main_dialog.result() # refresh the task list if result == accepted: self.fill() # reselect the same task self.find_and_select_entity_item(entity) project_main_dialog.deleteLater() elif selected_item is assign_users_action: from anima.ui import project_users_dialog project_users_main_dialog = \ project_users_dialog.MainDialog( parent=self, project=entity ) project_users_main_dialog.exec_() result = project_users_main_dialog.result() project_users_main_dialog.deleteLater() elif selected_item in change_status_menu_actions: # get the status code status_code = selected_item.text() from sqlalchemy import func status = \ Status.query.filter( func.lower(Status.code) == func.lower(status_code) ).first() # change the status of the entity # if it is a leaf task # if it doesn't have any dependent_of # assert isinstance(entity, Task) if isinstance(entity, Task): if entity.is_leaf and not entity.dependent_of: # then we can update it entity.status = status # # fix other task statuses # from anima import utils # utils.fix_task_statuses(entity) # refresh the tree from stalker.db.session import DBSession DBSession.add(entity) DBSession.commit() if item.parent: item.parent.reload() self.find_and_select_entity_item(entity) else: try: # go to the dependencies dep_task = selected_item.task self.find_and_select_entity_item( dep_task, self ) except AttributeError: pass
def test_stalker_entity_decoder_will_not_recreate_versions_2(create_db, create_empty_project): """testing if JSON decoder will not recreate already created versions when the versions data is not oredered to the version number """ from stalker import Task project = create_empty_project import json from anima.utils import task_hierarchy_io global __here__ file_path = os.path.join(__here__, "data", "test_template6.json") with open(file_path) as f: data = json.load(f) import copy data_backup = copy.deepcopy(data) decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data) from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() from stalker import Asset, Task kutu_assets = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Kutu')\ .all() assert len(kutu_assets) == 1 kutu_asset = kutu_assets[0] kutu_look_devs = Task.query\ .filter(Task.parent==kutu_asset)\ .filter(Task.name=='lookDev')\ .all() assert len(kutu_look_devs) == 1 kutu_look_dev = kutu_look_devs[0] assert len(kutu_look_dev.versions) == 9 current_version = kutu_look_dev.versions[0] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[1] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[2] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[3] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[4] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[5] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[6] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[7] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) current_version = kutu_look_dev.versions[8] assert current_version.version_number == \ int(current_version.filename.split("_v")[-1].split(".")[0]) # load a couple times more # 1 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 2) data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 3 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 4 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() from stalker import Asset, Task kutu_assets = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Kutu')\ .all() assert len(kutu_assets) == 1 kutu_asset = kutu_assets[0] kutu_look_devs = Task.query\ .filter(Task.parent==kutu_asset)\ .filter(Task.name=='lookDev')\ .all() assert len(kutu_look_devs) == 1 kutu_look_dev = kutu_look_devs[0] assert len(kutu_look_dev.versions) == 9 assert kutu_look_dev.versions[-1].version_number == 8
def test_stalker_entity_decoder_will_not_recreate_versions(create_db, create_empty_project): """testing if JSON decoder will not recreate already created versions """ from stalker import Task project = create_empty_project import json from anima.utils import task_hierarchy_io global __here__ file_path = os.path.join(__here__, "data", "test_template5.json") with open(file_path) as f: data = json.load(f) import copy data_backup = copy.deepcopy(data) decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data) from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() from stalker import Asset, Task ananas_assets = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Ananas')\ .all() assert len(ananas_assets) == 1 ananas_asset = ananas_assets[0] ananas_look_devs = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='lookDev')\ .all() assert len(ananas_look_devs) == 1 ananas_look_dev = ananas_look_devs[0] assert len(ananas_look_dev.versions) == 1 # load a couple times more # 1 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 2) data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 3 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # 4 data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() from stalker import Asset, Task ananas_assets = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Ananas')\ .all() assert len(ananas_assets) == 1 ananas_asset = ananas_assets[0] ananas_look_devs = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='lookDev')\ .all() assert len(ananas_look_devs) == 1 ananas_look_dev = ananas_look_devs[0] assert len(ananas_look_dev.versions) == 1 assert ananas_look_dev.versions[0].version_number == 1
def test_stalker_entity_decoder_will_append_new_data(create_db, create_empty_project): """testing if JSON decoder will append new data on top of the existing one even when the JSON contains data about the already existing tasks """ from stalker import Task project = create_empty_project import json from anima.utils import task_hierarchy_io global __here__ file_path = os.path.join(__here__, "data", "test_template5.json") with open(file_path) as f: data = json.load(f) # create backup of the data import copy data_backup = copy.deepcopy(data) decoder = \ task_hierarchy_io.StalkerEntityDecoder( project=project ) loaded_entity = decoder.loads(data) from stalker.db.session import DBSession DBSession.add(loaded_entity) DBSession.commit() # check if they are loaded normally from stalker import Asset, Task ananas_asset = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Ananas')\ .first() assert ananas_asset is not None assert isinstance(ananas_asset, Asset) ananas_look_dev = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='lookDev')\ .first() assert ananas_look_dev is not None assert isinstance(ananas_look_dev, Task) ananas_model = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='model')\ .first() assert ananas_model is not None assert isinstance(ananas_model, Task) # now load it again data = copy.deepcopy(data_backup) loaded_entity = decoder.loads(data) DBSession.add(loaded_entity) DBSession.commit() # now there should be only one Assets task from stalker import Task assets_tasks = Task.query.filter(Task.name=='Assets').all() assert len(assets_tasks) == 1 # check if there is only one Ananas asset ananas_assets = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Ananas')\ .all() assert len(ananas_assets) == 1 # check if there is only one LookDev task under the Ananas asset ananas_asset = ananas_assets[0] ananas_look_devs = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='lookDev')\ .all() assert len(ananas_look_devs) == 1 ananas_models = Task.query\ .filter(Task.parent==ananas_asset)\ .filter(Task.name=='model')\ .all() assert len(ananas_models) == 1 # check if there is a Peach asset peach_asset = Asset.query\ .filter(Asset.project==project)\ .filter(Asset.name=='Peach')\ .first() assert peach_asset is not None assert isinstance(peach_asset, Asset) # check peach child tasks peach_model = Task.query\ .filter(Task.parent==peach_asset)\ .filter(Task.name=='model')\ .first() assert peach_model is not None assert isinstance(peach_model, Task) peach_look_dev = Task.query\ .filter(Task.parent==peach_asset)\ .filter(Task.name=='lookDev')\ .first() assert peach_look_dev is not None assert isinstance(peach_look_dev, Task)