Exemplo n.º 1
0
class AssetsRemover(BaseAction):
    '''Edit meta data action.'''

    #: Action identifier.
    identifier = 'remove.assets'
    #: Action label.
    label = "Pype Admin"
    variant = '- Delete Assets by Name'
    #: Action description.
    description = 'Removes assets from Ftrack and Avalon db with all childs'
    #: roles that are allowed to register this action
    role_list = ['Pypeclub', 'Administrator']
    icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', ''))
    #: Db
    db = DbConnector()

    def discover(self, session, entities, event):
        ''' Validation '''
        if len(entities) != 1:
            return False

        valid = ["show", "task"]
        entityType = event["data"]["selection"][0].get("entityType", "")
        if entityType.lower() not in valid:
            return False

        return True

    def interface(self, session, entities, event):
        if not event['data'].get('values', {}):
            title = 'Enter Asset names to delete'

            items = []
            for i in range(15):

                item = {
                    'label': 'Asset {}'.format(i + 1),
                    'name': 'asset_{}'.format(i + 1),
                    'type': 'text',
                    'value': ''
                }
                items.append(item)

            return {'items': items, 'title': title}

    def launch(self, session, entities, event):
        entity = entities[0]
        if entity.entity_type.lower() != 'Project':
            project = entity['project']
        else:
            project = entity

        if 'values' not in event['data']:
            return

        values = event['data']['values']
        if len(values) <= 0:
            return {'success': True, 'message': 'No Assets to delete!'}

        asset_names = []

        for k, v in values.items():
            if v.replace(' ', '') != '':
                asset_names.append(v)

        self.db.install()
        self.db.Session['AVALON_PROJECT'] = project["full_name"]

        assets = self.find_assets(asset_names)

        all_ids = []
        for asset in assets:
            all_ids.append(asset['_id'])
            all_ids.extend(self.find_child(asset))

        if len(all_ids) == 0:
            self.db.uninstall()
            return {'success': True, 'message': 'None of assets'}

        or_subquery = []
        for id in all_ids:
            or_subquery.append({'_id': id})
        delete_query = {'$or': or_subquery}
        self.db.delete_many(delete_query)

        self.db.uninstall()
        return {'success': True, 'message': 'All assets were deleted!'}

    def find_child(self, entity):
        output = []
        id = entity['_id']
        visuals = [x for x in self.db.find({'data.visualParent': id})]
        assert len(visuals) == 0, 'This asset has another asset as child'
        childs = self.db.find({'parent': id})
        for child in childs:
            output.append(child['_id'])
            output.extend(self.find_child(child))
        return output

    def find_assets(self, asset_names):
        assets = []
        for name in asset_names:
            entity = self.db.find_one({'type': 'asset', 'name': name})
            if entity is not None and entity not in assets:
                assets.append(entity)
        return assets
Exemplo n.º 2
0
class PypeUpdateFromV2_2_0(BaseAction):
    """This action is to remove silo field from database and changes asset
    schema to newer version

    WARNING: it is NOT for situations when you want to switch from avalon-core
    to Pype's avalon-core!!!

    """
    #: Action identifier.
    identifier = "silos.doctor"
    #: Action label.
    label = "Pype Update"
    variant = "- v2.2.0 to v2.3.0 or higher"
    #: Action description.
    description = "Use when Pype was updated from v2.2.0 to v2.3.0 or higher"

    #: roles that are allowed to register this action
    role_list = ["Pypeclub", "Administrator"]
    icon = "{}/ftrack/action_icons/PypeUpdate.svg".format(
        os.environ.get("PYPE_STATICS_SERVER", ""))
    # connector to MongoDB (Avalon mongo)
    db_con = DbConnector()

    def discover(self, session, entities, event):
        """ Validation """
        if len(entities) != 1:
            return False

        if entities[0].entity_type.lower() != "project":
            return False

        return True

    def interface(self, session, entities, event):
        if event['data'].get('values', {}):
            return

        items = []
        item_splitter = {'type': 'label', 'value': '---'}
        title = "Updated Pype from v 2.2.0 to v2.3.0 or higher"

        items.append({
            "type":
            "label",
            "value":
            ("NOTE: This doctor action should be used ONLY when Pype"
             " was updated from v2.2.0 to v2.3.0 or higher.<br><br><br>")
        })

        items.append({
            "type":
            "label",
            "value":
            ("Select if want to process <b>all synchronized projects</b>"
             " or <b>selection</b>.")
        })

        items.append({
            "type":
            "enumerator",
            "name":
            "__process_all__",
            "data": [{
                "label": "All synchronized projects",
                "value": True
            }, {
                "label": "Selection",
                "value": False
            }],
            "value":
            False
        })

        items.append({
            "type":
            "label",
            "value":
            ("<br/><br/><h2>Synchronized projects:</h2>"
             "<i>(ignore if <strong>\"ALL projects\"</strong> selected)</i>")
        })

        self.log.debug("Getting all Ftrack projects")
        # Get all Ftrack projects
        all_ftrack_projects = [
            project["full_name"] for project in session.query("Project").all()
        ]

        self.log.debug("Getting Avalon projects that are also in the Ftrack")
        # Get Avalon projects that are in Ftrack
        self.db_con.install()
        possible_projects = [
            project["name"] for project in self.db_con.projects()
            if project["name"] in all_ftrack_projects
        ]

        for project in possible_projects:
            item_label = {"type": "label", "value": project}
            item = {
                "label": "- process",
                "name": project,
                "type": 'boolean',
                "value": False
            }
            items.append(item_splitter)
            items.append(item_label)
            items.append(item)

        if len(possible_projects) == 0:
            return {
                "success":
                False,
                "message": ("Nothing to process."
                            " There are not projects synchronized to avalon.")
            }
        else:
            return {"items": items, "title": title}

    def launch(self, session, entities, event):
        if 'values' not in event['data']:
            return

        projects_selection = {True: [], False: []}
        process_all = None

        values = event['data']['values']
        for key, value in values.items():
            if key == "__process_all__":
                process_all = value
                continue

            projects_selection[value].append(key)

        # Skip if process_all value is not boolean
        # - may happen when user delete string line in combobox
        if not isinstance(process_all, bool):
            self.log.warning(
                "Nothing was processed. User didn't select if want to process"
                " selection or all projects!")
            return {
                "success":
                False,
                "message":
                ("Nothing was processed. You must select if want to process"
                 " \"selection\" or \"all projects\"!")
            }

        projects_to_process = projects_selection[True]
        if process_all:
            projects_to_process.extend(projects_selection[False])

        self.db_con.install()
        for project in projects_to_process:
            self.log.debug("Processing project \"{}\"".format(project))
            self.db_con.Session["AVALON_PROJECT"] = project

            self.log.debug("- Unsetting silos on assets")
            self.db_con.update_many({"type": "asset"},
                                    {"$unset": {
                                        "silo": ""
                                    }})

            self.log.debug("- setting schema of assets to v.3")
            self.db_con.update_many(
                {"type": "asset"},
                {"$set": {
                    "schema": "avalon-core:asset-3.0"
                }})

        return True
class StoreThumbnailsToAvalon(BaseAction):
    # Action identifier
    identifier = "store.thubmnail.to.avalon"
    # Action label
    label = "Pype Admin"
    # Action variant
    variant = "- Store Thumbnails to avalon"
    # Action description
    description = 'Test action'
    # roles that are allowed to register this action
    role_list = ["Pypeclub", "Administrator", "Project Manager"]

    icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', '')
    )

    thumbnail_key = "AVALON_THUMBNAIL_ROOT"
    db_con = DbConnector()

    def discover(self, session, entities, event):
        for entity in entities:
            if entity.entity_type.lower() == "assetversion":
                return True
        return False

    def launch(self, session, entities, event):
        # DEBUG LINE
        # root_path = r"C:\Users\jakub.trllo\Desktop\Tests\ftrack_thumbnails"

        user = session.query(
            "User where username is '{0}'".format(session.api_user)
        ).one()
        action_job = session.create("Job", {
            "user": user,
            "status": "running",
            "data": json.dumps({
                "description": "Storing thumbnails to avalon."
            })
        })
        session.commit()

        project = self.get_project_from_entity(entities[0])
        project_name = project["full_name"]
        anatomy = Anatomy(project_name)

        if "publish" not in anatomy.templates:
            msg = "Anatomy does not have set publish key!"

            action_job["status"] = "failed"
            session.commit()

            self.log.warning(msg)

            return {
                "success": False,
                "message": msg
            }

        if "thumbnail" not in anatomy.templates["publish"]:
            msg = (
                "There is not set \"thumbnail\""
                " template in Antomy for project \"{}\""
            ).format(project_name)

            action_job["status"] = "failed"
            session.commit()

            self.log.warning(msg)

            return {
                "success": False,
                "message": msg
            }

        thumbnail_roots = os.environ.get(self.thumbnail_key)
        if (
            "{thumbnail_root}" in anatomy.templates["publish"]["thumbnail"]
            and not thumbnail_roots
        ):
            msg = "`{}` environment is not set".format(self.thumbnail_key)

            action_job["status"] = "failed"
            session.commit()

            self.log.warning(msg)

            return {
                "success": False,
                "message": msg
            }

        existing_thumbnail_root = None
        for path in thumbnail_roots.split(os.pathsep):
            if os.path.exists(path):
                existing_thumbnail_root = path
                break

        if existing_thumbnail_root is None:
            msg = (
                "Can't access paths, set in `{}` ({})"
            ).format(self.thumbnail_key, thumbnail_roots)

            action_job["status"] = "failed"
            session.commit()

            self.log.warning(msg)

            return {
                "success": False,
                "message": msg
            }

        example_template_data = {
            "_id": "ID",
            "thumbnail_root": "THUBMNAIL_ROOT",
            "thumbnail_type": "THUMBNAIL_TYPE",
            "ext": ".EXT",
            "project": {
                "name": "PROJECT_NAME",
                "code": "PROJECT_CODE"
            },
            "asset": "ASSET_NAME",
            "subset": "SUBSET_NAME",
            "version": "VERSION_NAME",
            "hierarchy": "HIERARCHY"
        }
        tmp_filled = anatomy.format_all(example_template_data)
        thumbnail_result = tmp_filled["publish"]["thumbnail"]
        if not thumbnail_result.solved:
            missing_keys = thumbnail_result.missing_keys
            invalid_types = thumbnail_result.invalid_types
            submsg = ""
            if missing_keys:
                submsg += "Missing keys: {}".format(", ".join(
                    ["\"{}\"".format(key) for key in missing_keys]
                ))

            if invalid_types:
                items = []
                for key, value in invalid_types.items():
                    items.append("{}{}".format(str(key), str(value)))
                submsg += "Invalid types: {}".format(", ".join(items))

            msg = (
                "Thumbnail Anatomy template expects more keys than action"
                " can offer. {}"
            ).format(submsg)

            action_job["status"] = "failed"
            session.commit()

            self.log.warning(msg)

            return {
                "success": False,
                "message": msg
            }

        thumbnail_template = anatomy.templates["publish"]["thumbnail"]

        self.db_con.install()

        for entity in entities:
            # Skip if entity is not AssetVersion (never should happend, but..)
            if entity.entity_type.lower() != "assetversion":
                continue

            # Skip if AssetVersion don't have thumbnail
            thumbnail_ent = entity["thumbnail"]
            if thumbnail_ent is None:
                self.log.debug((
                    "Skipping. AssetVersion don't "
                    "have set thumbnail. {}"
                ).format(entity["id"]))
                continue

            avalon_ents_result = self.get_avalon_entities_for_assetversion(
                entity, self.db_con
            )
            version_full_path = (
                "Asset: \"{project_name}/{asset_path}\""
                " | Subset: \"{subset_name}\""
                " | Version: \"{version_name}\""
            ).format(**avalon_ents_result)

            version = avalon_ents_result["version"]
            if not version:
                self.log.warning((
                    "AssetVersion does not have version in avalon. {}"
                ).format(version_full_path))
                continue

            thumbnail_id = version["data"].get("thumbnail_id")
            if thumbnail_id:
                self.log.info((
                    "AssetVersion skipped, already has thubmanil set. {}"
                ).format(version_full_path))
                continue

            # Get thumbnail extension
            file_ext = thumbnail_ent["file_type"]
            if not file_ext.startswith("."):
                file_ext = ".{}".format(file_ext)

            avalon_project = avalon_ents_result["project"]
            avalon_asset = avalon_ents_result["asset"]
            hierarchy = ""
            parents = avalon_asset["data"].get("parents") or []
            if parents:
                hierarchy = "/".join(parents)

            # Prepare anatomy template fill data
            # 1. Create new id for thumbnail entity
            thumbnail_id = ObjectId()

            template_data = {
                "_id": str(thumbnail_id),
                "thumbnail_root": existing_thumbnail_root,
                "thumbnail_type": "thumbnail",
                "ext": file_ext,
                "project": {
                    "name": avalon_project["name"],
                    "code": avalon_project["data"].get("code")
                },
                "asset": avalon_ents_result["asset_name"],
                "subset": avalon_ents_result["subset_name"],
                "version": avalon_ents_result["version_name"],
                "hierarchy": hierarchy
            }

            anatomy_filled = anatomy.format(template_data)
            thumbnail_path = anatomy_filled["publish"]["thumbnail"]
            thumbnail_path = thumbnail_path.replace("..", ".")
            thumbnail_path = os.path.normpath(thumbnail_path)

            downloaded = False
            for loc in (thumbnail_ent.get("component_locations") or []):
                res_id = loc.get("resource_identifier")
                if not res_id:
                    continue

                thubmnail_url = self.get_thumbnail_url(res_id)
                if self.download_file(thubmnail_url, thumbnail_path):
                    downloaded = True
                    break

            if not downloaded:
                self.log.warning(
                    "Could not download thumbnail for {}".format(
                        version_full_path
                    )
                )
                continue

            # Clean template data from keys that are dynamic
            template_data.pop("_id")
            template_data.pop("thumbnail_root")

            thumbnail_entity = {
                "_id": thumbnail_id,
                "type": "thumbnail",
                "schema": "pype:thumbnail-1.0",
                "data": {
                    "template": thumbnail_template,
                    "template_data": template_data
                }
            }

            # Create thumbnail entity
            self.db_con.insert_one(thumbnail_entity)
            self.log.debug(
                "Creating entity in database {}".format(str(thumbnail_entity))
            )

            # Set thumbnail id for version
            self.db_con.update_one(
                {"_id": version["_id"]},
                {"$set": {"data.thumbnail_id": thumbnail_id}}
            )

            self.db_con.update_one(
                {"_id": avalon_asset["_id"]},
                {"$set": {"data.thumbnail_id": thumbnail_id}}
            )

        action_job["status"] = "done"
        session.commit()

        return True

    def get_thumbnail_url(self, resource_identifier, size=None):
        # TODO use ftrack_api method rather (find way how to use it)
        url_string = (
            u'{url}/component/thumbnail?id={id}&username={username}'
            u'&apiKey={apiKey}'
        )
        url = url_string.format(
            url=self.session.server_url,
            id=resource_identifier,
            username=self.session.api_user,
            apiKey=self.session.api_key
        )
        if size:
            url += u'&size={0}'.format(size)

        return url

    def download_file(self, source_url, dst_file_path):
        dir_path = os.path.dirname(dst_file_path)
        try:
            os.makedirs(dir_path)
        except OSError as exc:
            if exc.errno != errno.EEXIST:
                self.log.warning(
                    "Could not create folder: \"{}\"".format(dir_path)
                )
                return False

        self.log.debug(
            "Downloading file \"{}\" -> \"{}\"".format(
                source_url, dst_file_path
            )
        )
        file_open = open(dst_file_path, "wb")
        try:
            file_open.write(requests.get(source_url).content)
        except Exception:
            self.log.warning(
                "Download of image `{}` failed.".format(source_url)
            )
            return False
        finally:
            file_open.close()
        return True

    def get_avalon_entities_for_assetversion(self, asset_version, db_con):
        output = {
            "success": True,
            "message": None,
            "project": None,
            "project_name": None,
            "asset": None,
            "asset_name": None,
            "asset_path": None,
            "subset": None,
            "subset_name": None,
            "version": None,
            "version_name": None,
            "representations": None
        }

        db_con.install()

        ft_asset = asset_version["asset"]
        subset_name = ft_asset["name"]
        version = asset_version["version"]
        parent = ft_asset["parent"]
        ent_path = "/".join(
            [ent["name"] for ent in parent["link"]]
        )
        project = self.get_project_from_entity(asset_version)
        project_name = project["full_name"]

        output["project_name"] = project_name
        output["asset_name"] = parent["name"]
        output["asset_path"] = ent_path
        output["subset_name"] = subset_name
        output["version_name"] = version

        db_con.Session["AVALON_PROJECT"] = project_name

        avalon_project = db_con.find_one({"type": "project"})
        output["project"] = avalon_project

        if not avalon_project:
            output["success"] = False
            output["message"] = (
                "Project not synchronized to avalon `{}`".format(project_name)
            )
            return output

        asset_ent = None
        asset_mongo_id = parent["custom_attributes"].get(CustAttrIdKey)
        if asset_mongo_id:
            try:
                asset_mongo_id = ObjectId(asset_mongo_id)
                asset_ent = db_con.find_one({
                    "type": "asset",
                    "_id": asset_mongo_id
                })
            except Exception:
                pass

        if not asset_ent:
            asset_ent = db_con.find_one({
                "type": "asset",
                "data.ftrackId": parent["id"]
            })

        output["asset"] = asset_ent

        if not asset_ent:
            output["success"] = False
            output["message"] = (
                "Not synchronized entity to avalon `{}`".format(ent_path)
            )
            return output

        asset_mongo_id = asset_ent["_id"]

        subset_ent = db_con.find_one({
            "type": "subset",
            "parent": asset_mongo_id,
            "name": subset_name
        })

        output["subset"] = subset_ent

        if not subset_ent:
            output["success"] = False
            output["message"] = (
                "Subset `{}` does not exist under Asset `{}`"
            ).format(subset_name, ent_path)
            return output

        version_ent = db_con.find_one({
            "type": "version",
            "name": version,
            "parent": subset_ent["_id"]
        })

        output["version"] = version_ent

        if not version_ent:
            output["success"] = False
            output["message"] = (
                "Version `{}` does not exist under Subset `{}` | Asset `{}`"
            ).format(version, subset_name, ent_path)
            return output

        repre_ents = list(db_con.find({
            "type": "representation",
            "parent": version_ent["_id"]
        }))

        output["representations"] = repre_ents
        return output
class AttributesRemapper(BaseAction):
    '''Edit meta data action.'''

    ignore_me = True
    #: Action identifier.
    identifier = 'attributes.remapper'
    #: Action label.
    label = "Pype Doctor"
    variant = '- Attributes Remapper'
    #: Action description.
    description = 'Remaps attributes in avalon DB'

    #: roles that are allowed to register this action
    role_list = ["Pypeclub", "Administrator"]
    icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', ''))

    db_con = DbConnector()
    keys_to_change = {
        "fstart": "frameStart",
        "startFrame": "frameStart",
        "edit_in": "frameStart",
        "fend": "frameEnd",
        "endFrame": "frameEnd",
        "edit_out": "frameEnd",
        "handle_start": "handleStart",
        "handle_end": "handleEnd",
        "handles": ["handleEnd", "handleStart"],
        "frameRate": "fps",
        "framerate": "fps",
        "resolution_width": "resolutionWidth",
        "resolution_height": "resolutionHeight",
        "pixel_aspect": "pixelAspect"
    }

    def discover(self, session, entities, event):
        ''' Validation '''

        return True

    def interface(self, session, entities, event):
        if event['data'].get('values', {}):
            return

        title = 'Select Projects where attributes should be remapped'

        items = []

        selection_enum = {
            'label':
            'Process type',
            'type':
            'enumerator',
            'name':
            'process_type',
            'data': [{
                'label': 'Selection',
                'value': 'selection'
            }, {
                'label': 'Inverted selection',
                'value': 'except'
            }],
            'value':
            'selection'
        }
        selection_label = {
            'type':
            'label',
            'value': ('Selection based variants:<br/>'
                      '- `Selection` - '
                      'NOTHING is processed when nothing is selected<br/>'
                      '- `Inverted selection` - '
                      'ALL Projects are processed when nothing is selected')
        }

        items.append(selection_enum)
        items.append(selection_label)

        item_splitter = {'type': 'label', 'value': '---'}

        all_projects = session.query('Project').all()
        for project in all_projects:
            item_label = {
                'type':
                'label',
                'value':
                '{} (<i>{}</i>)'.format(project['full_name'], project['name'])
            }
            item = {'name': project['id'], 'type': 'boolean', 'value': False}
            if len(items) > 0:
                items.append(item_splitter)
            items.append(item_label)
            items.append(item)

        if len(items) == 0:
            return {'success': False, 'message': 'Didn\'t found any projects'}
        else:
            return {'items': items, 'title': title}

    def launch(self, session, entities, event):
        if 'values' not in event['data']:
            return

        values = event['data']['values']
        process_type = values.pop('process_type')

        selection = True
        if process_type == 'except':
            selection = False

        interface_messages = {}

        projects_to_update = []
        for project_id, update_bool in values.items():
            if not update_bool and selection:
                continue

            if update_bool and not selection:
                continue

            project = session.query(
                'Project where id is "{}"'.format(project_id)).one()
            projects_to_update.append(project)

        if not projects_to_update:
            self.log.debug('Nothing to update')
            return {'success': True, 'message': 'Nothing to update'}

        self.db_con.install()

        relevant_types = ["project", "asset", "version"]

        for ft_project in projects_to_update:
            self.log.debug("Processing project \"{}\"".format(
                ft_project["full_name"]))

            self.db_con.Session["AVALON_PROJECT"] = ft_project["full_name"]
            project = self.db_con.find_one({'type': 'project'})
            if not project:
                key = "Projects not synchronized to db"
                if key not in interface_messages:
                    interface_messages[key] = []
                interface_messages[key].append(ft_project["full_name"])
                continue

            # Get all entities in project collection from MongoDB
            _entities = self.db_con.find({})
            for _entity in _entities:
                ent_t = _entity.get("type", "*unknown type")
                name = _entity.get("name", "*unknown name")

                self.log.debug("- {} ({})".format(name, ent_t))

                # Skip types that do not store keys to change
                if ent_t.lower() not in relevant_types:
                    self.log.debug("-- skipping - type is not relevant")
                    continue

                # Get data which will change
                updating_data = {}
                source_data = _entity["data"]

                for key_from, key_to in self.keys_to_change.items():
                    # continue if final key already exists
                    if type(key_to) == list:
                        for key in key_to:
                            # continue if final key was set in update_data
                            if key in updating_data:
                                continue

                            # continue if source key not exist or value is None
                            value = source_data.get(key_from)
                            if value is None:
                                continue

                            self.log.debug("-- changing key {} to {}".format(
                                key_from, key))

                            updating_data[key] = value
                    else:
                        if key_to in source_data:
                            continue

                        # continue if final key was set in update_data
                        if key_to in updating_data:
                            continue

                        # continue if source key not exist or value is None
                        value = source_data.get(key_from)
                        if value is None:
                            continue

                        self.log.debug("-- changing key {} to {}".format(
                            key_from, key_to))
                        updating_data[key_to] = value

                # Pop out old keys from entity
                is_obsolete = False
                for key in self.keys_to_change:
                    if key not in source_data:
                        continue
                    is_obsolete = True
                    source_data.pop(key)

                # continue if there is nothing to change
                if not is_obsolete and not updating_data:
                    self.log.debug("-- nothing to change")
                    continue

                source_data.update(updating_data)

                self.db_con.update_many({"_id": _entity["_id"]},
                                        {"$set": {
                                            "data": source_data
                                        }})

        self.db_con.uninstall()

        if interface_messages:
            self.show_interface_from_dict(
                messages=interface_messages,
                title="Errors during remapping attributes",
                event=event)

        return True

    def show_interface_from_dict(self, event, messages, title=""):
        items = []

        for key, value in messages.items():
            if not value:
                continue
            subtitle = {'type': 'label', 'value': '# {}'.format(key)}
            items.append(subtitle)
            if isinstance(value, list):
                for item in value:
                    message = {
                        'type': 'label',
                        'value': '<p>{}</p>'.format(item)
                    }
                    items.append(message)
            else:
                message = {'type': 'label', 'value': '<p>{}</p>'.format(value)}
                items.append(message)

        self.show_interface(items=items, title=title, event=event)
Exemplo n.º 5
0
class UserAssigmentEvent(BaseEvent):
    """
    This script will intercept user assigment / de-assigment event and
    run shell script, providing as much context as possible.

    It expects configuration file ``presets/ftrack/user_assigment_event.json``.
    In it, you define paths to scripts to be run for user assigment event and
    for user-deassigment::
        {
            "add": [
                "/path/to/script1",
                "/path/to/script2"
            ],
            "remove": [
                "/path/to/script3",
                "/path/to/script4"
            ]
        }

    Those scripts are executed in shell. Three arguments will be passed to
    to them:
        1) user name of user (de)assigned
        2) path to workfiles of task user was (de)assigned to
        3) path to publish files of task user was (de)assigned to
    """

    db_con = DbConnector()

    def error(self, *err):
        for e in err:
            self.log.error(e)

    def _run_script(self, script, args):
        """
        Run shell script with arguments as subprocess

        :param script: script path
        :type script: str
        :param args: list of arguments passed to script
        :type args: list
        :returns: return code
        :rtype: int
        """
        p = subprocess.call([script, args], shell=True)
        return p

    def _get_task_and_user(self, session, action, changes):
        """
        Get Task and User entities from Ftrack session

        :param session: ftrack session
        :type session: ftrack_api.session
        :param action: event action
        :type action: str
        :param changes: what was changed by event
        :type changes: dict
        :returns: User and Task entities
        :rtype: tuple
        """
        if not changes:
            return None, None

        if action == 'add':
            task_id = changes.get('context_id', {}).get('new')
            user_id = changes.get('resource_id', {}).get('new')

        elif action == 'remove':
            task_id = changes.get('context_id', {}).get('old')
            user_id = changes.get('resource_id', {}).get('old')

        if not task_id:
            return None, None

        if not user_id:
            return None, None

        task = session.query('Task where id is "{}"'.format(task_id)).one()
        user = session.query('User where id is "{}"'.format(user_id)).one()

        return task, user

    def _get_asset(self, task):
        """
        Get asset from task entity

        :param task: Task entity
        :type task: dict
        :returns: Asset entity
        :rtype: dict
        """
        parent = task['parent']
        self.db_con.install()
        self.db_con.Session['AVALON_PROJECT'] = task['project']['full_name']

        avalon_entity = None
        parent_id = parent['custom_attributes'].get(CustAttrIdKey)
        if parent_id:
            parent_id = ObjectId(parent_id)
            avalon_entity = self.db_con.find_one({
                '_id': parent_id,
                'type': 'asset'
            })

        if not avalon_entity:
            avalon_entity = self.db_con.find_one({
                'type': 'asset',
                'name': parent['name']
            })

        if not avalon_entity:
            self.db_con.uninstall()
            msg = 'Entity "{}" not found in avalon database'.format(
                parent['name'])
            self.error(msg)
            return {'success': False, 'message': msg}
        self.db_con.uninstall()
        return avalon_entity

    def _get_hierarchy(self, asset):
        """
        Get hierarchy from Asset entity

        :param asset: Asset entity
        :type asset: dict
        :returns: hierarchy string
        :rtype: str
        """
        return asset['data']['hierarchy']

    def _get_template_data(self, task):
        """
        Get data to fill template from task

        .. seealso:: :mod:`pypeapp.Anatomy`

        :param task: Task entity
        :type task: dict
        :returns: data for anatomy template
        :rtype: dict
        """
        project_name = task['project']['full_name']
        project_code = task['project']['name']
        try:
            root = os.environ['PYPE_STUDIO_PROJECTS_PATH']
        except KeyError:
            msg = 'Project ({}) root not set'.format(project_name)
            self.log.error(msg)
            return {'success': False, 'message': msg}

        # fill in template data
        asset = self._get_asset(task)
        t_data = {
            'root': root,
            'project': {
                'name': project_name,
                'code': project_code
            },
            'asset': asset['name'],
            'task': task['name'],
            'hierarchy': self._get_hierarchy(asset)
        }

        return t_data

    def launch(self, session, event):
        # load shell scripts presets
        presets = config.get_presets()['ftrack'].get("user_assigment_event")
        if not presets:
            return
        for entity in event.get('data', {}).get('entities', []):
            if entity.get('entity_type') != 'Appointment':
                continue

            task, user = self._get_task_and_user(session, entity.get('action'),
                                                 entity.get('changes'))

            if not task or not user:
                self.log.error('Task or User was not found.')
                continue

            data = self._get_template_data(task)
            # format directories to pass to shell script
            anatomy = Anatomy(data["project"]["name"])
            # formatting work dir is easiest part as we can use whole path
            work_dir = anatomy.format(data)['avalon']['work']
            # we also need publish but not whole
            filled_all = anatomy.format_all(data)
            publish = filled_all['avalon']['publish']

            # now find path to {asset}
            m = re.search("(^.+?{})".format(data['asset']), publish)

            if not m:
                msg = 'Cannot get part of publish path {}'.format(publish)
                self.log.error(msg)
                return {'success': False, 'message': msg}
            publish_dir = m.group(1)

            for script in presets.get(entity.get('action')):
                self.log.info('[{}] : running script for user {}'.format(
                    entity.get('action'), user["username"]))
                self._run_script(script,
                                 [user["username"], work_dir, publish_dir])

        return True
Exemplo n.º 6
0
class SyncHierarchicalAttrs(BaseAction):

    db_con = DbConnector()
    ca_mongoid = lib.get_ca_mongoid()

    #: Action identifier.
    identifier = 'sync.hierarchical.attrs'
    #: Action label.
    label = "Pype Admin"
    variant = '- Sync Hier Attrs (Server)'
    #: Action description.
    description = 'Synchronize hierarchical attributes'
    #: Icon
    icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
        os.environ.get(
            'PYPE_STATICS_SERVER',
            'http://localhost:{}'.format(config.get_presets().get(
                'services', {}).get('statics_server',
                                    {}).get('default_port', 8021))))

    def register(self):
        self.session.event_hub.subscribe('topic=ftrack.action.discover',
                                         self._discover)

        self.session.event_hub.subscribe(
            'topic=ftrack.action.launch and data.actionIdentifier={}'.format(
                self.identifier), self._launch)

    def discover(self, session, entities, event):
        ''' Validation '''
        role_check = False
        discover = False
        role_list = ['Pypeclub', 'Administrator', 'Project Manager']
        user = session.query('User where id is "{}"'.format(
            event['source']['user']['id'])).one()

        for role in user['user_security_roles']:
            if role['security_role']['name'] in role_list:
                role_check = True
                break

        if role_check is True:
            for entity in entities:
                context_type = entity.get('context_type', '').lower()
                if (context_type in ('show', 'task')
                        and entity.entity_type.lower() != 'task'):
                    discover = True
                    break

        return discover

    def launch(self, session, entities, event):
        self.interface_messages = {}

        user = session.query('User where id is "{}"'.format(
            event['source']['user']['id'])).one()

        job = session.create(
            'Job', {
                'user': user,
                'status': 'running',
                'data': json.dumps(
                    {'description': 'Sync Hierachical attributes'})
            })
        session.commit()
        self.log.debug('Job with id "{}" created'.format(job['id']))

        process_session = ftrack_api.Session(server_url=session.server_url,
                                             api_key=session.api_key,
                                             api_user=session.api_user,
                                             auto_connect_event_hub=True)
        try:
            # Collect hierarchical attrs
            self.log.debug('Collecting Hierarchical custom attributes started')
            custom_attributes = {}
            all_avalon_attr = process_session.query(
                'CustomAttributeGroup where name is "avalon"').one()

            error_key = (
                'Hierarchical attributes with set "default" value (not allowed)'
            )

            for cust_attr in all_avalon_attr[
                    'custom_attribute_configurations']:
                if 'avalon_' in cust_attr['key']:
                    continue

                if not cust_attr['is_hierarchical']:
                    continue

                if cust_attr['default']:
                    if error_key not in self.interface_messages:
                        self.interface_messages[error_key] = []
                    self.interface_messages[error_key].append(
                        cust_attr['label'])

                    self.log.warning(
                        ('Custom attribute "{}" has set default value.'
                         ' This attribute can\'t be synchronized').format(
                             cust_attr['label']))
                    continue

                custom_attributes[cust_attr['key']] = cust_attr

            self.log.debug(
                'Collecting Hierarchical custom attributes has finished')

            if not custom_attributes:
                msg = 'No hierarchical attributes to sync.'
                self.log.debug(msg)
                return {'success': True, 'message': msg}

            entity = entities[0]
            if entity.entity_type.lower() == 'project':
                project_name = entity['full_name']
            else:
                project_name = entity['project']['full_name']

            self.db_con.install()
            self.db_con.Session['AVALON_PROJECT'] = project_name

            _entities = self._get_entities(event, process_session)

            for entity in _entities:
                self.log.debug(30 * '-')
                self.log.debug('Processing entity "{}"'.format(
                    entity.get('name', entity)))

                ent_name = entity.get('name', entity)
                if entity.entity_type.lower() == 'project':
                    ent_name = entity['full_name']

                for key in custom_attributes:
                    self.log.debug(30 * '*')
                    self.log.debug(
                        'Processing Custom attribute key "{}"'.format(key))
                    # check if entity has that attribute
                    if key not in entity['custom_attributes']:
                        error_key = 'Missing key on entities'
                        if error_key not in self.interface_messages:
                            self.interface_messages[error_key] = []

                        self.interface_messages[error_key].append(
                            '- key: "{}" - entity: "{}"'.format(key, ent_name))

                        self.log.error(('- key "{}" not found on "{}"').format(
                            key, entity.get('name', entity)))
                        continue

                    value = self.get_hierarchical_value(key, entity)
                    if value is None:
                        error_key = (
                            'Missing value for key on entity'
                            ' and its parents (synchronization was skipped)')
                        if error_key not in self.interface_messages:
                            self.interface_messages[error_key] = []

                        self.interface_messages[error_key].append(
                            '- key: "{}" - entity: "{}"'.format(key, ent_name))

                        self.log.warning(
                            ('- key "{}" not set on "{}" or its parents'
                             ).format(key, ent_name))
                        continue

                    self.update_hierarchical_attribute(entity, key, value)

            job['status'] = 'done'
            session.commit()

        except Exception:
            self.log.error('Action "{}" failed'.format(self.label),
                           exc_info=True)

        finally:
            self.db_con.uninstall()

            if job['status'] in ('queued', 'running'):
                job['status'] = 'failed'
            session.commit()

            if self.interface_messages:
                self.show_interface_from_dict(messages=self.interface_messages,
                                              title="something went wrong",
                                              event=event)

        return True

    def get_hierarchical_value(self, key, entity):
        value = entity['custom_attributes'][key]
        if (value is not None or entity.entity_type.lower() == 'project'):
            return value

        return self.get_hierarchical_value(key, entity['parent'])

    def update_hierarchical_attribute(self, entity, key, value):
        if (entity['context_type'].lower() not in ('show', 'task')
                or entity.entity_type.lower() == 'task'):
            return

        ent_name = entity.get('name', entity)
        if entity.entity_type.lower() == 'project':
            ent_name = entity['full_name']

        hierarchy = '/'.join([a['name'] for a in entity.get('ancestors', [])])
        if hierarchy:
            hierarchy = '/'.join(
                [entity['project']['full_name'], hierarchy, entity['name']])
        elif entity.entity_type.lower() == 'project':
            hierarchy = entity['full_name']
        else:
            hierarchy = '/'.join(
                [entity['project']['full_name'], entity['name']])

        self.log.debug('- updating entity "{}"'.format(hierarchy))

        # collect entity's custom attributes
        custom_attributes = entity.get('custom_attributes')
        if not custom_attributes:
            return

        mongoid = custom_attributes.get(self.ca_mongoid)
        if not mongoid:
            error_key = 'Missing MongoID on entities (try SyncToAvalon first)'
            if error_key not in self.interface_messages:
                self.interface_messages[error_key] = []

            if ent_name not in self.interface_messages[error_key]:
                self.interface_messages[error_key].append(ent_name)

            self.log.warning(
                '-- entity "{}" is not synchronized to avalon. Skipping'.
                format(ent_name))
            return

        try:
            mongoid = ObjectId(mongoid)
        except Exception:
            error_key = 'Invalid MongoID on entities (try SyncToAvalon)'
            if error_key not in self.interface_messages:
                self.interface_messages[error_key] = []

            if ent_name not in self.interface_messages[error_key]:
                self.interface_messages[error_key].append(ent_name)

            self.log.warning(
                '-- entity "{}" has stored invalid MongoID. Skipping'.format(
                    ent_name))
            return
        # Find entity in Mongo DB
        mongo_entity = self.db_con.find_one({'_id': mongoid})
        if not mongo_entity:
            error_key = 'Entities not found in Avalon DB (try SyncToAvalon)'
            if error_key not in self.interface_messages:
                self.interface_messages[error_key] = []

            if ent_name not in self.interface_messages[error_key]:
                self.interface_messages[error_key].append(ent_name)

            self.log.warning(
                '-- entity "{}" was not found in DB by id "{}". Skipping'.
                format(ent_name, str(mongoid)))
            return

        # Change value if entity has set it's own
        entity_value = custom_attributes[key]
        if entity_value is not None:
            value = entity_value

        data = mongo_entity.get('data') or {}

        data[key] = value
        self.db_con.update_many({'_id': mongoid}, {'$set': {'data': data}})

        for child in entity.get('children', []):
            self.update_hierarchical_attribute(child, key, value)
Exemplo n.º 7
0
class SyncHierarchicalAttrs(BaseEvent):
    # After sync to avalon event!
    priority = 101
    db_con = DbConnector()
    ca_mongoid = lib.get_ca_mongoid()

    def launch(self, session, event):
        # Filter entities and changed values if it makes sence to run script
        processable = []
        processable_ent = {}
        for ent in event['data']['entities']:
            # Ignore entities that are not tasks or projects
            if ent['entityType'].lower() not in ['task', 'show']:
                continue

            action = ent.get("action")
            # skip if remove (Entity does not exist in Ftrack)
            if action == "remove":
                continue

            # When entity was add we don't care about keys
            if action != "add":
                keys = ent.get('keys')
                if not keys:
                    continue

            entity = session.get(self._get_entity_type(ent), ent['entityId'])
            processable.append(ent)

            processable_ent[ent['entityId']] = {
                "entity": entity,
                "action": action,
                "link": entity["link"]
            }

        if not processable:
            return True

        # Find project of entities
        ft_project = None
        for entity_dict in processable_ent.values():
            try:
                base_proj = entity_dict['link'][0]
            except Exception:
                continue
            ft_project = session.get(base_proj['type'], base_proj['id'])
            break

        # check if project is set to auto-sync
        if (ft_project is None
                or 'avalon_auto_sync' not in ft_project['custom_attributes'] or
                ft_project['custom_attributes']['avalon_auto_sync'] is False):
            return True

        # Get hierarchical custom attributes from "avalon" group
        custom_attributes = {}
        query = 'CustomAttributeGroup where name is "avalon"'
        all_avalon_attr = session.query(query).one()
        for cust_attr in all_avalon_attr['custom_attribute_configurations']:
            if 'avalon_' in cust_attr['key']:
                continue
            if not cust_attr['is_hierarchical']:
                continue
            custom_attributes[cust_attr['key']] = cust_attr

        if not custom_attributes:
            return True

        self.db_con.install()
        self.db_con.Session['AVALON_PROJECT'] = ft_project['full_name']

        for ent in processable:
            entity_dict = processable_ent[ent['entityId']]

            entity = entity_dict["entity"]
            ent_path = "/".join([ent["name"] for ent in entity_dict['link']])
            action = entity_dict["action"]

            keys_to_process = {}
            if action == "add":
                # Store all custom attributes when entity was added
                for key in custom_attributes:
                    keys_to_process[key] = entity['custom_attributes'][key]
            else:
                # Update only updated keys
                for key in ent['keys']:
                    if key in custom_attributes:
                        keys_to_process[key] = entity['custom_attributes'][key]

            processed_keys = self.get_hierarchical_values(
                keys_to_process, entity)
            # Do the processing of values
            self.update_hierarchical_attribute(entity, processed_keys,
                                               ent_path)

        self.db_con.uninstall()

        return True

    def get_hierarchical_values(self, keys_dict, entity):
        # check already set values
        _set_keys = []
        for key, value in keys_dict.items():
            if value is not None:
                _set_keys.append(key)

        # pop set values from keys_dict
        set_keys = {}
        for key in _set_keys:
            set_keys[key] = keys_dict.pop(key)

        # find if entity has set values and pop them out
        keys_to_pop = []
        for key in keys_dict.keys():
            _val = entity["custom_attributes"][key]
            if _val:
                keys_to_pop.append(key)
                set_keys[key] = _val

        for key in keys_to_pop:
            keys_dict.pop(key)

        # if there are not keys to find value return found
        if not keys_dict:
            return set_keys

        # end recursion if entity is project
        if entity.entity_type.lower() == "project":
            for key, value in keys_dict.items():
                set_keys[key] = value

        else:
            result = self.get_hierarchical_values(keys_dict, entity["parent"])
            for key, value in result.items():
                set_keys[key] = value

        return set_keys

    def update_hierarchical_attribute(self, entity, keys_dict, ent_path):
        # TODO store all keys at once for entity
        custom_attributes = entity.get('custom_attributes')
        if not custom_attributes:
            return

        mongoid = custom_attributes.get(self.ca_mongoid)
        if not mongoid:
            return

        try:
            mongoid = ObjectId(mongoid)
        except Exception:
            return

        mongo_entity = self.db_con.find_one({'_id': mongoid})
        if not mongo_entity:
            return

        changed_keys = {}
        data = mongo_entity.get('data') or {}
        for key, value in keys_dict.items():
            cur_value = data.get(key)
            if cur_value:
                if cur_value == value:
                    continue
            changed_keys[key] = value
            data[key] = value

        if not changed_keys:
            return

        self.log.debug("{} - updated hierarchical attributes: {}".format(
            ent_path, str(changed_keys)))

        self.db_con.update_many({'_id': mongoid}, {'$set': {'data': data}})

        for child in entity.get('children', []):
            _keys_dict = {}
            for key, value in keys_dict.items():
                if key not in child.get('custom_attributes', {}):
                    continue
                child_value = child['custom_attributes'][key]
                if child_value is not None:
                    continue
                _keys_dict[key] = value

            if not _keys_dict:
                continue
            child_path = "/".join([ent["name"] for ent in child['link']])
            self.update_hierarchical_attribute(child, _keys_dict, child_path)
Exemplo n.º 8
0
class CreateFolders(BaseAction):
    '''Custom action.'''

    #: Action identifier.
    identifier = 'create.folders'

    #: Action label.
    label = 'Create Folders'

    #: Action Icon.
    icon = '{}/ftrack/action_icons/CreateFolders.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', ''))

    db = DbConnector()

    def discover(self, session, entities, event):
        ''' Validation '''
        if len(entities) != 1:
            return False

        not_allowed = ['assetversion', 'project']
        if entities[0].entity_type.lower() in not_allowed:
            return False

        return True

    def interface(self, session, entities, event):
        if event['data'].get('values', {}):
            return
        entity = entities[0]
        without_interface = True
        for child in entity['children']:
            if child['object_type']['name'].lower() != 'task':
                without_interface = False
                break
        self.without_interface = without_interface
        if without_interface:
            return
        title = 'Create folders'

        entity_name = entity['name']
        msg = ('<h2>Do you want create folders also'
               ' for all children of "{}"?</h2>')
        if entity.entity_type.lower() == 'project':
            entity_name = entity['full_name']
            msg = msg.replace(' also', '')
            msg += '<h3>(Project root won\'t be created if not checked)</h3>'
        items = []
        item_msg = {'type': 'label', 'value': msg.format(entity_name)}
        item_label = {'type': 'label', 'value': 'With all chilren entities'}
        item = {'name': 'children_included', 'type': 'boolean', 'value': False}
        items.append(item_msg)
        items.append(item_label)
        items.append(item)

        if len(items) == 0:
            return {
                'success': False,
                'message': 'Didn\'t found any running jobs'
            }
        else:
            return {'items': items, 'title': title}

    def launch(self, session, entities, event):
        '''Callback method for custom action.'''
        with_childrens = True
        if self.without_interface is False:
            if 'values' not in event['data']:
                return
            with_childrens = event['data']['values']['children_included']
        entity = entities[0]
        if entity.entity_type.lower() == 'project':
            proj = entity
        else:
            proj = entity['project']
        project_name = proj['full_name']
        project_code = proj['name']
        if entity.entity_type.lower() == 'project' and with_childrens == False:
            return {'success': True, 'message': 'Nothing was created'}
        data = {
            "root": os.environ["AVALON_PROJECTS"],
            "project": {
                "name": project_name,
                "code": project_code
            }
        }
        all_entities = []
        all_entities.append(entity)
        if with_childrens:
            all_entities = self.get_notask_children(entity)

        av_project = None
        try:
            self.db.install()
            self.db.Session['AVALON_PROJECT'] = project_name
            av_project = self.db.find_one({'type': 'project'})
            template_work = av_project['config']['template']['work']
            template_publish = av_project['config']['template']['publish']
            self.db.uninstall()
        except Exception:
            templates = Anatomy().templates
            template_work = templates["avalon"]["work"]
            template_publish = templates["avalon"]["publish"]

        collected_paths = []
        presets = config.get_presets()['tools']['sw_folders']
        for entity in all_entities:
            if entity.entity_type.lower() == 'project':
                continue
            ent_data = data.copy()

            asset_name = entity['name']
            ent_data['asset'] = asset_name

            parents = entity['link']
            hierarchy_names = [p['name'] for p in parents[1:-1]]
            hierarchy = ''
            if hierarchy_names:
                hierarchy = os.path.sep.join(hierarchy_names)
            ent_data['hierarchy'] = hierarchy

            tasks_created = False
            if entity['children']:
                for child in entity['children']:
                    if child['object_type']['name'].lower() != 'task':
                        continue
                    tasks_created = True
                    task_type_name = child['type']['name'].lower()
                    task_data = ent_data.copy()
                    task_data['task'] = child['name']
                    possible_apps = presets.get(task_type_name, [])
                    template_work_created = False
                    template_publish_created = False
                    apps = []
                    for app in possible_apps:
                        try:
                            app_data = avalonlib.get_application(app)
                            app_dir = app_data['application_dir']
                        except ValueError:
                            app_dir = app
                        apps.append(app_dir)

                    # Template wok
                    if '{app}' in template_work:
                        for app in apps:
                            template_work_created = True
                            app_data = task_data.copy()
                            app_data['app'] = app
                            collected_paths.append(
                                self.compute_template(template_work, app_data))
                    if template_work_created is False:
                        collected_paths.append(
                            self.compute_template(template_work, task_data))
                    # Template publish
                    if '{app}' in template_publish:
                        for app in apps:
                            template_publish_created = True
                            app_data = task_data.copy()
                            app_data['app'] = app
                            collected_paths.append(
                                self.compute_template(template_publish,
                                                      app_data, True))
                    if template_publish_created is False:
                        collected_paths.append(
                            self.compute_template(template_publish, task_data,
                                                  True))

            if not tasks_created:
                # create path for entity
                collected_paths.append(
                    self.compute_template(template_work, ent_data))
                collected_paths.append(
                    self.compute_template(template_publish, ent_data))
        if len(collected_paths) > 0:
            self.log.info('Creating folders:')
        for path in set(collected_paths):
            self.log.info(path)
            if not os.path.exists(path):
                os.makedirs(path)

        return {'success': True, 'message': 'Created Folders Successfully!'}

    def get_notask_children(self, entity):
        output = []
        if entity.get('object_type',
                      {}).get('name', entity.entity_type).lower() == 'task':
            return output
        else:
            output.append(entity)
        if entity['children']:
            for child in entity['children']:
                output.extend(self.get_notask_children(child))
        return output

    def template_format(self, template, data):

        partial_data = PartialDict(data)

        # remove subdict items from string (like 'project[name]')
        subdict = PartialDict()
        count = 1
        store_pattern = 5 * '_' + '{:0>3}'
        regex_patern = "\{\w*\[[^\}]*\]\}"
        matches = re.findall(regex_patern, template)

        for match in matches:
            key = store_pattern.format(count)
            subdict[key] = match
            template = template.replace(match, '{' + key + '}')
            count += 1
        # solve fillind keys with optional keys
        solved = self._solve_with_optional(template, partial_data)
        # try to solve subdict and replace them back to string
        for k, v in subdict.items():
            try:
                v = v.format_map(data)
            except (KeyError, TypeError):
                pass
            subdict[k] = v

        return solved.format_map(subdict)

    def _solve_with_optional(self, template, data):
        # Remove optional missing keys
        pattern = re.compile(r"(<.*?[^{0]*>)[^0-9]*?")
        invalid_optionals = []
        for group in pattern.findall(template):
            try:
                group.format(**data)
            except KeyError:
                invalid_optionals.append(group)
        for group in invalid_optionals:
            template = template.replace(group, "")

        solved = template.format_map(data)

        # solving after format optional in second round
        for catch in re.compile(r"(<.*?[^{0]*>)[^0-9]*?").findall(solved):
            if "{" in catch:
                # remove all optional
                solved = solved.replace(catch, "")
            else:
                # Remove optional symbols
                solved = solved.replace(catch, catch[1:-1])

        return solved

    def compute_template(self, str, data, task=False):
        first_result = self.template_format(str, data)
        if first_result == first_result.split('{')[0]:
            return os.path.normpath(first_result)
        if task:
            return os.path.normpath(first_result.split('{')[0])

        index = first_result.index('{')

        regex = '\{\w*[^\}]*\}'
        match = re.findall(regex, first_result[index:])[0]
        without_missing = str.split(match)[0].split('}')
        output_items = []
        for part in without_missing:
            if '{' in part:
                output_items.append(part + '}')
        return os.path.normpath(
            self.template_format(''.join(output_items), data))
Exemplo n.º 9
0
class DeleteOldVersions(BaseAction):

    identifier = "delete.old.versions"
    label = "Pype Admin"
    variant = "- Delete old versions"
    description = ("Delete files from older publishes so project can be"
                   " archived with only lates versions.")
    role_list = ["Pypeclub", "Project Manager", "Administrator"]
    icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', ''))

    dbcon = DbConnector()

    inteface_title = "Choose your preferences"
    splitter_item = {"type": "label", "value": "---"}
    sequence_splitter = "__sequence_splitter__"

    def discover(self, session, entities, event):
        ''' Validation '''
        selection = event["data"].get("selection") or []
        for entity in selection:
            entity_type = (entity.get("entityType") or "").lower()
            if entity_type == "assetversion":
                return True
        return False

    def interface(self, session, entities, event):
        items = []
        root = os.environ.get("AVALON_PROJECTS")
        if not root:
            msg = "Root path to projects is not set."
            items.append({
                "type": "label",
                "value": "<i><b>ERROR:</b> {}</i>".format(msg)
            })
            self.show_interface(items=items,
                                title=self.inteface_title,
                                event=event)
            return {"success": False, "message": msg}

        if not os.path.exists(root):
            msg = "Root path does not exists \"{}\".".format(str(root))
            items.append({
                "type": "label",
                "value": "<i><b>ERROR:</b> {}</i>".format(msg)
            })
            self.show_interface(items=items,
                                title=self.inteface_title,
                                event=event)
            return {"success": False, "message": msg}

        values = event["data"].get("values")
        if values:
            versions_count = int(values["last_versions_count"])
            if versions_count >= 1:
                return
            items.append({
                "type": "label",
                "value": ("# You have to keep at least 1 version!")
            })

        items.append({
            "type":
            "label",
            "value":
            ("<i><b>WARNING:</b> This will remove published files of older"
             " versions from disk so we don't recommend use"
             " this action on \"live\" project.</i>")
        })

        items.append(self.splitter_item)

        # How many versions to keep
        items.append({
            "type": "label",
            "value": "## Choose how many versions you want to keep:"
        })
        items.append({
            "type":
            "label",
            "value":
            ("<i><b>NOTE:</b> We do recommend to keep 2 versions.</i>")
        })
        items.append({
            "type": "number",
            "name": "last_versions_count",
            "label": "Versions",
            "value": 2
        })

        items.append(self.splitter_item)

        items.append({
            "type":
            "label",
            "value": ("## Remove publish folder even if there"
                      " are other than published files:")
        })
        items.append({
            "type":
            "label",
            "value":
            ("<i><b>WARNING:</b> This may remove more than you want.</i>")
        })
        items.append({
            "type": "boolean",
            "name": "force_delete_publish_folder",
            "label": "Are You sure?",
            "value": False
        })

        return {"items": items, "title": self.inteface_title}

    def launch(self, session, entities, event):
        values = event["data"].get("values")
        if not values:
            return

        versions_count = int(values["last_versions_count"])
        force_to_remove = values["force_delete_publish_folder"]

        _val1 = "OFF"
        if force_to_remove:
            _val1 = "ON"

        _val3 = "s"
        if versions_count == 1:
            _val3 = ""

        self.log.debug(
            ("Process started. Force to delete publish folder is set to [{0}]"
             " and will keep {1} latest version{2}.").format(
                 _val1, versions_count, _val3))

        self.dbcon.install()

        project = None
        avalon_asset_names = []
        asset_versions_by_parent_id = collections.defaultdict(list)
        subset_names_by_asset_name = collections.defaultdict(list)

        ftrack_assets_by_name = {}
        for entity in entities:
            ftrack_asset = entity["asset"]

            parent_ent = ftrack_asset["parent"]
            parent_ftrack_id = parent_ent["id"]
            parent_name = parent_ent["name"]

            if parent_name not in avalon_asset_names:
                avalon_asset_names.append(parent_name)

            # Group asset versions by parent entity
            asset_versions_by_parent_id[parent_ftrack_id].append(entity)

            # Get project
            if project is None:
                project = parent_ent["project"]

            # Collect subset names per asset
            subset_name = ftrack_asset["name"]
            subset_names_by_asset_name[parent_name].append(subset_name)

            if subset_name not in ftrack_assets_by_name:
                ftrack_assets_by_name[subset_name] = ftrack_asset

        # Set Mongo collection
        project_name = project["full_name"]
        self.dbcon.Session["AVALON_PROJECT"] = project_name
        self.log.debug("Project is set to {}".format(project_name))

        # Get Assets from avalon database
        assets = list(
            self.dbcon.find({
                "type": "asset",
                "name": {
                    "$in": avalon_asset_names
                }
            }))
        asset_id_to_name_map = {
            asset["_id"]: asset["name"]
            for asset in assets
        }
        asset_ids = list(asset_id_to_name_map.keys())

        self.log.debug("Collected assets ({})".format(len(asset_ids)))

        # Get Subsets
        subsets = list(
            self.dbcon.find({
                "type": "subset",
                "parent": {
                    "$in": asset_ids
                }
            }))
        subsets_by_id = {}
        subset_ids = []
        for subset in subsets:
            asset_id = subset["parent"]
            asset_name = asset_id_to_name_map[asset_id]
            available_subsets = subset_names_by_asset_name[asset_name]

            if subset["name"] not in available_subsets:
                continue

            subset_ids.append(subset["_id"])
            subsets_by_id[subset["_id"]] = subset

        self.log.debug("Collected subsets ({})".format(len(subset_ids)))

        # Get Versions
        versions = list(
            self.dbcon.find({
                "type": "version",
                "parent": {
                    "$in": subset_ids
                }
            }))

        versions_by_parent = collections.defaultdict(list)
        for ent in versions:
            versions_by_parent[ent["parent"]].append(ent)

        def sort_func(ent):
            return int(ent["name"])

        all_last_versions = []
        for parent_id, _versions in versions_by_parent.items():
            for idx, version in enumerate(
                    sorted(_versions, key=sort_func, reverse=True)):
                if idx >= versions_count:
                    break
                all_last_versions.append(version)

        self.log.debug("Collected versions ({})".format(len(versions)))

        # Filter latest versions
        for version in all_last_versions:
            versions.remove(version)

        # Update versions_by_parent without filtered versions
        versions_by_parent = collections.defaultdict(list)
        for ent in versions:
            versions_by_parent[ent["parent"]].append(ent)

        # Filter already deleted versions
        versions_to_pop = []
        for version in versions:
            version_tags = version["data"].get("tags")
            if version_tags and "deleted" in version_tags:
                versions_to_pop.append(version)

        for version in versions_to_pop:
            subset = subsets_by_id[version["parent"]]
            asset_id = subset["parent"]
            asset_name = asset_id_to_name_map[asset_id]
            msg = "Asset: \"{}\" | Subset: \"{}\" | Version: \"{}\"".format(
                asset_name, subset["name"], version["name"])
            self.log.warning(
                ("Skipping version. Already tagged as `deleted`. < {} >"
                 ).format(msg))
            versions.remove(version)

        version_ids = [ent["_id"] for ent in versions]

        self.log.debug("Filtered versions to delete ({})".format(
            len(version_ids)))

        if not version_ids:
            msg = "Skipping processing. Nothing to delete."
            self.log.debug(msg)
            return {"success": True, "message": msg}

        repres = list(
            self.dbcon.find({
                "type": "representation",
                "parent": {
                    "$in": version_ids
                }
            }))

        self.log.debug("Collected representations to remove ({})".format(
            len(repres)))

        dir_paths = {}
        file_paths_by_dir = collections.defaultdict(list)
        for repre in repres:
            file_path, seq_path = self.path_from_represenation(repre)
            if file_path is None:
                self.log.warning(
                    ("Could not format path for represenation \"{}\"").format(
                        str(repre)))
                continue

            dir_path = os.path.dirname(file_path)
            dir_id = None
            for _dir_id, _dir_path in dir_paths.items():
                if _dir_path == dir_path:
                    dir_id = _dir_id
                    break

            if dir_id is None:
                dir_id = uuid.uuid4()
                dir_paths[dir_id] = dir_path

            file_paths_by_dir[dir_id].append([file_path, seq_path])

        dir_ids_to_pop = []
        for dir_id, dir_path in dir_paths.items():
            if os.path.exists(dir_path):
                continue

            dir_ids_to_pop.append(dir_id)

        # Pop dirs from both dictionaries
        for dir_id in dir_ids_to_pop:
            dir_paths.pop(dir_id)
            paths = file_paths_by_dir.pop(dir_id)
            # TODO report of missing directories?
            paths_msg = ", ".join(
                ["'{}'".format(path[0].replace("\\", "/")) for path in paths])
            self.log.warning(
                ("Folder does not exist. Deleting it's files skipped: {}"
                 ).format(paths_msg))

        if force_to_remove:
            self.delete_whole_dir_paths(dir_paths.values())
        else:
            self.delete_only_repre_files(dir_paths, file_paths_by_dir)

        mongo_changes_bulk = []
        for version in versions:
            orig_version_tags = version["data"].get("tags") or []
            version_tags = [tag for tag in orig_version_tags]
            if "deleted" not in version_tags:
                version_tags.append("deleted")

            if version_tags == orig_version_tags:
                continue

            update_query = {"_id": version["_id"]}
            update_data = {"$set": {"data.tags": version_tags}}
            mongo_changes_bulk.append(UpdateOne(update_query, update_data))

        if mongo_changes_bulk:
            self.dbcon.bulk_write(mongo_changes_bulk)

        self.dbcon.uninstall()

        # Set attribute `is_published` to `False` on ftrack AssetVersions
        for subset_id, _versions in versions_by_parent.items():
            subset_name = None
            for subset in subsets:
                if subset["_id"] == subset_id:
                    subset_name = subset["name"]
                    break

            if subset_name is None:
                self.log.warning("Subset with ID `{}` was not found.".format(
                    str(subset_id)))
                continue

            ftrack_asset = ftrack_assets_by_name.get(subset_name)
            if not ftrack_asset:
                self.log.warning(("Could not find Ftrack asset with name `{}`"
                                  ).format(subset_name))
                continue

            version_numbers = [int(ver["name"]) for ver in _versions]
            for version in ftrack_asset["versions"]:
                if int(version["version"]) in version_numbers:
                    version["is_published"] = False

        try:
            session.commit()

        except Exception:
            msg = ("Could not set `is_published` attribute to `False`"
                   " for selected AssetVersions.")
            self.log.warning(msg, exc_info=True)

            return {"success": False, "message": msg}

        return True

    def delete_whole_dir_paths(self, dir_paths):
        for dir_path in dir_paths:
            # Delete all files and fodlers in dir path
            for root, dirs, files in os.walk(dir_path, topdown=False):
                for name in files:
                    os.remove(os.path.join(root, name))

                for name in dirs:
                    os.rmdir(os.path.join(root, name))

            # Delete even the folder and it's parents folders if they are empty
            while True:
                if not os.path.exists(dir_path):
                    dir_path = os.path.dirname(dir_path)
                    continue

                if len(os.listdir(dir_path)) != 0:
                    break

                os.rmdir(os.path.join(dir_path))

    def delete_only_repre_files(self, dir_paths, file_paths):
        for dir_id, dir_path in dir_paths.items():
            dir_files = os.listdir(dir_path)
            collections, remainders = clique.assemble(dir_files)
            for file_path, seq_path in file_paths[dir_id]:
                file_path_base = os.path.split(file_path)[1]
                # Just remove file if `frame` key was not in context or
                # filled path is in remainders (single file sequence)
                if not seq_path or file_path_base in remainders:
                    if not os.path.exists(file_path):
                        self.log.warning(
                            "File was not found: {}".format(file_path))
                        continue
                    os.remove(file_path)
                    self.log.debug("Removed file: {}".format(file_path))
                    remainders.remove(file_path_base)
                    continue

                seq_path_base = os.path.split(seq_path)[1]
                head, tail = seq_path_base.split(self.sequence_splitter)

                final_col = None
                for collection in collections:
                    if head != collection.head or tail != collection.tail:
                        continue
                    final_col = collection
                    break

                if final_col is not None:
                    # Fill full path to head
                    final_col.head = os.path.join(dir_path, final_col.head)
                    for _file_path in final_col:
                        if os.path.exists(_file_path):
                            os.remove(_file_path)
                    _seq_path = final_col.format("{head}{padding}{tail}")
                    self.log.debug("Removed files: {}".format(_seq_path))
                    collections.remove(final_col)

                elif os.path.exists(file_path):
                    os.remove(file_path)
                    self.log.debug("Removed file: {}".format(file_path))

                else:
                    self.log.warning(
                        "File was not found: {}".format(file_path))

        # Delete as much as possible parent folders
        for dir_path in dir_paths.values():
            while True:
                if not os.path.exists(dir_path):
                    dir_path = os.path.dirname(dir_path)
                    continue

                if len(os.listdir(dir_path)) != 0:
                    break

                self.log.debug("Removed folder: {}".format(dir_path))
                os.rmdir(dir_path)

    def path_from_represenation(self, representation):
        try:
            template = representation["data"]["template"]

        except KeyError:
            return (None, None)

        root = os.environ["AVALON_PROJECTS"]
        if not root:
            return (None, None)

        sequence_path = None
        try:
            context = representation["context"]
            context["root"] = root
            path = avalon.pipeline.format_template_with_optional_keys(
                context, template)
            if "frame" in context:
                context["frame"] = self.sequence_splitter
                sequence_path = os.path.normpath(
                    avalon.pipeline.format_template_with_optional_keys(
                        context, template))

        except KeyError:
            # Template references unavailable data
            return (None, None)

        return (os.path.normpath(path), sequence_path)
Exemplo n.º 10
0
class DeleteAssetSubset(BaseAction):
    '''Edit meta data action.'''

    #: Action identifier.
    identifier = "delete.asset.subset"
    #: Action label.
    label = "Delete Asset/Subsets"
    #: Action description.
    description = "Removes from Avalon with all childs and asset from Ftrack"
    icon = "{}/ftrack/action_icons/DeleteAsset.svg".format(
        os.environ.get("PYPE_STATICS_SERVER", ""))
    #: roles that are allowed to register this action
    role_list = ["Pypeclub", "Administrator", "Project Manager"]
    #: Db connection
    dbcon = DbConnector()

    splitter = {"type": "label", "value": "---"}
    action_data_by_id = {}
    asset_prefix = "asset:"
    subset_prefix = "subset:"

    def discover(self, session, entities, event):
        """ Validation """
        task_ids = []
        for ent_info in event["data"]["selection"]:
            entType = ent_info.get("entityType", "")
            if entType == "task":
                task_ids.append(ent_info["entityId"])

        for entity in entities:
            ftrack_id = entity["id"]
            if ftrack_id not in task_ids:
                continue
            if entity.entity_type.lower() != "task":
                return True
        return False

    def _launch(self, event):
        try:
            args = self._translate_event(self.session, event)
            if "values" not in event["data"]:
                self.dbcon.install()
                return self._interface(self.session, *args)

            confirmation = self.confirm_delete(*args)
            if confirmation:
                return confirmation

            self.dbcon.install()
            response = self.launch(self.session, *args)
        finally:
            self.dbcon.uninstall()

        return self._handle_result(self.session, response, *args)

    def interface(self, session, entities, event):
        self.show_message(event, "Preparing data...", True)
        items = []
        title = "Choose items to delete"

        # Filter selection and get ftrack ids
        selection = event["data"].get("selection") or []
        ftrack_ids = []
        project_in_selection = False
        for entity in selection:
            entity_type = (entity.get("entityType") or "").lower()
            if entity_type != "task":
                if entity_type == "show":
                    project_in_selection = True
                continue

            ftrack_id = entity.get("entityId")
            if not ftrack_id:
                continue

            ftrack_ids.append(ftrack_id)

        if project_in_selection:
            msg = "It is not possible to use this action on project entity."
            self.show_message(event, msg, True)

        # Filter event even more (skip task entities)
        # - task entities are not relevant for avalon
        entity_mapping = {}
        for entity in entities:
            ftrack_id = entity["id"]
            if ftrack_id not in ftrack_ids:
                continue

            if entity.entity_type.lower() == "task":
                ftrack_ids.remove(ftrack_id)

            entity_mapping[ftrack_id] = entity

        if not ftrack_ids:
            # It is bug if this happens!
            return {
                "success": False,
                "message": "Invalid selection for this action (Bug)"
            }

        if entities[0].entity_type.lower() == "project":
            project = entities[0]
        else:
            project = entities[0]["project"]

        project_name = project["full_name"]
        self.dbcon.Session["AVALON_PROJECT"] = project_name

        selected_av_entities = list(
            self.dbcon.find({
                "type": "asset",
                "data.ftrackId": {
                    "$in": ftrack_ids
                }
            }))
        found_without_ftrack_id = {}
        if len(selected_av_entities) != len(ftrack_ids):
            found_ftrack_ids = [
                ent["data"]["ftrackId"] for ent in selected_av_entities
            ]
            for ftrack_id, entity in entity_mapping.items():
                if ftrack_id in found_ftrack_ids:
                    continue

                av_ents_by_name = list(
                    self.dbcon.find({
                        "type": "asset",
                        "name": entity["name"]
                    }))
                if not av_ents_by_name:
                    continue

                ent_path_items = [ent["name"] for ent in entity["link"]]
                parents = ent_path_items[1:len(ent_path_items) - 1:]
                # TODO we should say to user that
                # few of them are missing in avalon
                for av_ent in av_ents_by_name:
                    if av_ent["data"]["parents"] != parents:
                        continue

                    # TODO we should say to user that found entity
                    # with same name does not match same ftrack id?
                    if "ftrackId" not in av_ent["data"]:
                        selected_av_entities.append(av_ent)
                        found_without_ftrack_id[str(av_ent["_id"])] = ftrack_id
                        break

        if not selected_av_entities:
            return {
                "success": False,
                "message": "Didn't found entities in avalon"
            }

        # Remove cached action older than 2 minutes
        old_action_ids = []
        for id, data in self.action_data_by_id.items():
            created_at = data.get("created_at")
            if not created_at:
                old_action_ids.append(id)
                continue
            cur_time = datetime.now()
            existing_in_sec = (created_at - cur_time).total_seconds()
            if existing_in_sec > 60 * 2:
                old_action_ids.append(id)

        for id in old_action_ids:
            self.action_data_by_id.pop(id, None)

        # Store data for action id
        action_id = str(uuid.uuid1())
        self.action_data_by_id[action_id] = {
            "attempt": 1,
            "created_at": datetime.now(),
            "project_name": project_name,
            "subset_ids_by_name": {},
            "subset_ids_by_parent": {},
            "without_ftrack_id": found_without_ftrack_id
        }

        id_item = {"type": "hidden", "name": "action_id", "value": action_id}

        items.append(id_item)
        asset_ids = [ent["_id"] for ent in selected_av_entities]
        subsets_for_selection = self.dbcon.find({
            "type": "subset",
            "parent": {
                "$in": asset_ids
            }
        })

        asset_ending = ""
        if len(selected_av_entities) > 1:
            asset_ending = "s"

        asset_title = {
            "type": "label",
            "value": "# Delete asset{}:".format(asset_ending)
        }
        asset_note = {
            "type":
            "label",
            "value": ("<p><i>NOTE: Action will delete checked entities"
                      " in Ftrack and Avalon with all children entities and"
                      " published content.</i></p>")
        }

        items.append(asset_title)
        items.append(asset_note)

        asset_items = collections.defaultdict(list)
        for asset in selected_av_entities:
            ent_path_items = [project_name]
            ent_path_items.extend(asset.get("data", {}).get("parents") or [])
            ent_path_to_parent = "/".join(ent_path_items) + "/"
            asset_items[ent_path_to_parent].append(asset)

        for asset_parent_path, assets in sorted(asset_items.items()):
            items.append({
                "type": "label",
                "value": "## <b>- {}</b>".format(asset_parent_path)
            })
            for asset in assets:
                items.append({
                    "label":
                    asset["name"],
                    "name":
                    "{}{}".format(self.asset_prefix, str(asset["_id"])),
                    "type":
                    'boolean',
                    "value":
                    False
                })

        subset_ids_by_name = collections.defaultdict(list)
        subset_ids_by_parent = collections.defaultdict(list)
        for subset in subsets_for_selection:
            subset_id = subset["_id"]
            name = subset["name"]
            parent_id = subset["parent"]
            subset_ids_by_name[name].append(subset_id)
            subset_ids_by_parent[parent_id].append(subset_id)

        if not subset_ids_by_name:
            return {"items": items, "title": title}

        subset_ending = ""
        if len(subset_ids_by_name.keys()) > 1:
            subset_ending = "s"

        subset_title = {
            "type": "label",
            "value": "# Subset{} to delete:".format(subset_ending)
        }
        subset_note = {
            "type":
            "label",
            "value": ("<p><i>WARNING: Subset{} will be removed"
                      " for all <b>selected</b> entities.</i></p>"
                      ).format(subset_ending)
        }

        items.append(self.splitter)
        items.append(subset_title)
        items.append(subset_note)

        for name in subset_ids_by_name:
            items.append({
                "label": "<b>{}</b>".format(name),
                "name": "{}{}".format(self.subset_prefix, name),
                "type": "boolean",
                "value": False
            })

        self.action_data_by_id[action_id]["subset_ids_by_parent"] = (
            subset_ids_by_parent)
        self.action_data_by_id[action_id]["subset_ids_by_name"] = (
            subset_ids_by_name)

        return {"items": items, "title": title}

    def confirm_delete(self, entities, event):
        values = event["data"]["values"]
        action_id = values.get("action_id")
        spec_data = self.action_data_by_id.get(action_id)
        if not spec_data:
            # it is a bug if this happens!
            return {
                "success": False,
                "message": "Something bad has happened. Please try again."
            }

        # Process Delete confirmation
        delete_key = values.get("delete_key")
        if delete_key:
            delete_key = delete_key.lower().strip()
            # Go to launch part if user entered `delete`
            if delete_key == "delete":
                return
            # Skip whole process if user didn't enter any text
            elif delete_key == "":
                self.action_data_by_id.pop(action_id, None)
                return {
                    "success": True,
                    "message": "Deleting cancelled (delete entry was empty)"
                }
            # Get data to show again
            to_delete = spec_data["to_delete"]

        else:
            to_delete = collections.defaultdict(list)
            for key, value in values.items():
                if not value:
                    continue
                if key.startswith(self.asset_prefix):
                    _key = key.replace(self.asset_prefix, "")
                    to_delete["assets"].append(_key)

                elif key.startswith(self.subset_prefix):
                    _key = key.replace(self.subset_prefix, "")
                    to_delete["subsets"].append(_key)

            self.action_data_by_id[action_id]["to_delete"] = to_delete

        asset_to_delete = len(to_delete.get("assets") or []) > 0
        subset_to_delete = len(to_delete.get("subsets") or []) > 0

        if not asset_to_delete and not subset_to_delete:
            self.action_data_by_id.pop(action_id, None)
            return {
                "success": True,
                "message": "Nothing was selected to delete"
            }

        attempt = spec_data["attempt"]
        if attempt > 3:
            self.action_data_by_id.pop(action_id, None)
            return {
                "success": False,
                "message": "You didn't enter \"DELETE\" properly 3 times!"
            }

        self.action_data_by_id[action_id]["attempt"] += 1

        title = "Confirmation of deleting"

        if asset_to_delete:
            asset_len = len(to_delete["assets"])
            asset_ending = ""
            if asset_len > 1:
                asset_ending = "s"
            title += " {} Asset{}".format(asset_len, asset_ending)
            if subset_to_delete:
                title += " and"

        if subset_to_delete:
            sub_len = len(to_delete["subsets"])
            type_ending = ""
            sub_ending = ""
            if sub_len == 1:
                subset_ids_by_name = spec_data["subset_ids_by_name"]
                if len(subset_ids_by_name[to_delete["subsets"][0]]) > 1:
                    sub_ending = "s"

            elif sub_len > 1:
                type_ending = "s"
                sub_ending = "s"

            title += " {} type{} of subset{}".format(sub_len, type_ending,
                                                     sub_ending)

        items = []

        id_item = {"type": "hidden", "name": "action_id", "value": action_id}
        delete_label = {
            'type': 'label',
            'value': '# Please enter "DELETE" to confirm #'
        }
        delete_item = {
            "name": "delete_key",
            "type": "text",
            "value": "",
            "empty_text": "Type Delete here..."
        }

        items.append(id_item)
        items.append(delete_label)
        items.append(delete_item)

        return {"items": items, "title": title}

    def launch(self, session, entities, event):
        self.show_message(event, "Processing...", True)
        values = event["data"]["values"]
        action_id = values.get("action_id")
        spec_data = self.action_data_by_id.get(action_id)
        if not spec_data:
            # it is a bug if this happens!
            return {
                "success": False,
                "message": "Something bad has happened. Please try again."
            }

        report_messages = collections.defaultdict(list)

        project_name = spec_data["project_name"]
        to_delete = spec_data["to_delete"]
        self.dbcon.Session["AVALON_PROJECT"] = project_name

        assets_to_delete = to_delete.get("assets") or []
        subsets_to_delete = to_delete.get("subsets") or []

        # Convert asset ids to ObjectId obj
        assets_to_delete = [ObjectId(id) for id in assets_to_delete if id]

        subset_ids_by_parent = spec_data["subset_ids_by_parent"]
        subset_ids_by_name = spec_data["subset_ids_by_name"]

        subset_ids_to_archive = []
        asset_ids_to_archive = []
        ftrack_ids_to_delete = []
        if len(assets_to_delete) > 0:
            map_av_ftrack_id = spec_data["without_ftrack_id"]
            # Prepare data when deleting whole avalon asset
            avalon_assets = self.dbcon.find({"type": "asset"})
            avalon_assets_by_parent = collections.defaultdict(list)
            for asset in avalon_assets:
                asset_id = asset["_id"]
                parent_id = asset["data"]["visualParent"]
                avalon_assets_by_parent[parent_id].append(asset)
                if asset_id in assets_to_delete:
                    ftrack_id = map_av_ftrack_id.get(str(asset_id))
                    if not ftrack_id:
                        ftrack_id = asset["data"].get("ftrackId")

                    if not ftrack_id:
                        continue
                    ftrack_ids_to_delete.append(ftrack_id)

            children_queue = Queue()
            for mongo_id in assets_to_delete:
                children_queue.put(mongo_id)

            while not children_queue.empty():
                mongo_id = children_queue.get()
                if mongo_id in asset_ids_to_archive:
                    continue

                asset_ids_to_archive.append(mongo_id)
                for subset_id in subset_ids_by_parent.get(mongo_id, []):
                    if subset_id not in subset_ids_to_archive:
                        subset_ids_to_archive.append(subset_id)

                children = avalon_assets_by_parent.get(mongo_id)
                if not children:
                    continue

                for child in children:
                    child_id = child["_id"]
                    if child_id not in asset_ids_to_archive:
                        children_queue.put(child_id)

        # Prepare names of assets in ftrack and ids of subsets in mongo
        asset_names_to_delete = []
        if len(subsets_to_delete) > 0:
            for name in subsets_to_delete:
                asset_names_to_delete.append(name)
                for subset_id in subset_ids_by_name[name]:
                    if subset_id in subset_ids_to_archive:
                        continue
                    subset_ids_to_archive.append(subset_id)

        # Get ftrack ids of entities where will be delete only asset
        not_deleted_entities_id = []
        ftrack_id_name_map = {}
        if asset_names_to_delete:
            for entity in entities:
                ftrack_id = entity["id"]
                ftrack_id_name_map[ftrack_id] = entity["name"]
                if ftrack_id in ftrack_ids_to_delete:
                    continue
                not_deleted_entities_id.append(ftrack_id)

        mongo_proc_txt = "MongoProcessing: "
        ftrack_proc_txt = "Ftrack processing: "
        if asset_ids_to_archive:
            self.log.debug("{}Archivation of assets <{}>".format(
                mongo_proc_txt,
                ", ".join([str(id) for id in asset_ids_to_archive])))
            self.dbcon.update_many(
                {
                    "_id": {
                        "$in": asset_ids_to_archive
                    },
                    "type": "asset"
                }, {"$set": {
                    "type": "archived_asset"
                }})

        if subset_ids_to_archive:
            self.log.debug("{}Archivation of subsets <{}>".format(
                mongo_proc_txt,
                ", ".join([str(id) for id in subset_ids_to_archive])))
            self.dbcon.update_many(
                {
                    "_id": {
                        "$in": subset_ids_to_archive
                    },
                    "type": "subset"
                }, {"$set": {
                    "type": "archived_subset"
                }})

        if ftrack_ids_to_delete:
            self.log.debug("{}Deleting Ftrack Entities <{}>".format(
                ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)))

            joined_ids_to_delete = ", ".join(
                ["\"{}\"".format(id) for id in ftrack_ids_to_delete])
            ftrack_ents_to_delete = self.session.query(
                "select id, link from TypedContext where id in ({})".format(
                    joined_ids_to_delete)).all()
            for entity in ftrack_ents_to_delete:
                self.session.delete(entity)
                try:
                    self.session.commit()
                except Exception:
                    ent_path = "/".join(
                        [ent["name"] for ent in entity["link"]])
                    msg = "Failed to delete entity"
                    report_messages[msg].append(ent_path)
                    self.session.rollback()
                    self.log.warning("{} <{}>".format(msg, ent_path),
                                     exc_info=True)

        if not_deleted_entities_id:
            joined_not_deleted = ", ".join([
                "\"{}\"".format(ftrack_id)
                for ftrack_id in not_deleted_entities_id
            ])
            joined_asset_names = ", ".join(
                ["\"{}\"".format(name) for name in asset_names_to_delete])
            # Find assets of selected entities with names of checked subsets
            assets = self.session.query(
                ("select id from Asset where"
                 " context_id in ({}) and name in ({})").format(
                     joined_not_deleted, joined_asset_names)).all()

            self.log.debug("{}Deleting Ftrack Assets <{}>".format(
                ftrack_proc_txt, ", ".join([asset["id"] for asset in assets])))
            for asset in assets:
                self.session.delete(asset)
                try:
                    self.session.commit()
                except Exception:
                    self.session.rollback()
                    msg = "Failed to delete asset"
                    report_messages[msg].append(asset["id"])
                    self.log.warning("{} <{}>".format(asset["id"]),
                                     exc_info=True)

        return self.report_handle(report_messages, project_name, event)

    def report_handle(self, report_messages, project_name, event):
        if not report_messages:
            return {"success": True, "message": "Deletion was successful!"}

        title = "Delete report ({}):".format(project_name)
        items = []
        items.append({
            "type": "label",
            "value": "# Deleting was not completely successful"
        })
        items.append({
            "type": "label",
            "value": "<p><i>Check logs for more information</i></p>"
        })
        for msg, _items in report_messages.items():
            if not _items or not msg:
                continue

            items.append({"type": "label", "value": "# {}".format(msg)})

            if isinstance(_items, str):
                _items = [_items]
            items.append({
                "type": "label",
                "value": '<p>{}</p>'.format("<br>".join(_items))
            })
            items.append(self.splitter)

        self.show_interface(items, title, event)

        return {
            "success": False,
            "message": "Deleting finished. Read report messages."
        }
Exemplo n.º 11
0
class DeleteAsset(BaseAction):
    '''Edit meta data action.'''

    #: Action identifier.
    identifier = 'delete.asset'
    #: Action label.
    label = 'Delete Asset/Subsets'
    #: Action description.
    description = 'Removes from Avalon with all childs and asset from Ftrack'
    icon = '{}/ftrack/action_icons/DeleteAsset.svg'.format(
        os.environ.get('PYPE_STATICS_SERVER', ''))
    #: roles that are allowed to register this action
    role_list = ['Pypeclub', 'Administrator']
    #: Db
    db = DbConnector()

    value = None

    def discover(self, session, entities, event):
        ''' Validation '''
        if len(entities) != 1:
            return False

        valid = ["task"]
        entityType = event["data"]["selection"][0].get("entityType", "")
        if entityType.lower() not in valid:
            return False

        return True

    def _launch(self, event):
        self.reset_session()
        try:
            self.db.install()
            args = self._translate_event(self.session, event)

            interface = self._interface(self.session, *args)

            confirmation = self.confirm_delete(True, *args)

            if interface:
                return interface

            if confirmation:
                return confirmation

            response = self.launch(self.session, *args)
        finally:
            self.db.uninstall()

        return self._handle_result(self.session, response, *args)

    def interface(self, session, entities, event):
        if not event['data'].get('values', {}):
            self.attempt = 1
            items = []
            entity = entities[0]
            title = 'Choose items to delete from "{}"'.format(entity['name'])
            project = entity['project']

            self.db.Session['AVALON_PROJECT'] = project["full_name"]

            av_entity = self.db.find_one({
                'type': 'asset',
                'name': entity['name']
            })

            if av_entity is None:
                return {
                    'success': False,
                    'message': 'Didn\'t found assets in avalon'
                }

            asset_label = {
                'type': 'label',
                'value': '## Delete whole asset: ##'
            }
            asset_item = {
                'label': av_entity['name'],
                'name': 'whole_asset',
                'type': 'boolean',
                'value': False
            }
            splitter = {'type': 'label', 'value': '{}'.format(200 * "-")}
            subset_label = {'type': 'label', 'value': '## Subsets: ##'}
            if av_entity is not None:
                items.append(asset_label)
                items.append(asset_item)
                items.append(splitter)

                all_subsets = self.db.find({
                    'type': 'subset',
                    'parent': av_entity['_id']
                })

                subset_items = []
                for subset in all_subsets:
                    item = {
                        'label': subset['name'],
                        'name': str(subset['_id']),
                        'type': 'boolean',
                        'value': False
                    }
                    subset_items.append(item)
                if len(subset_items) > 0:
                    items.append(subset_label)
                    items.extend(subset_items)
            else:
                return {
                    'success': False,
                    'message': 'Didn\'t found assets in avalon'
                }

            return {'items': items, 'title': title}

    def confirm_delete(self, first_attempt, entities, event):
        if first_attempt is True:
            if 'values' not in event['data']:
                return

            values = event['data']['values']

            if len(values) <= 0:
                return
            if 'whole_asset' not in values:
                return
        else:
            values = self.values

        title = 'Confirmation of deleting {}'
        if values['whole_asset'] is True:
            title = title.format('whole asset {}'.format(entities[0]['name']))
        else:
            subsets = []
            for key, value in values.items():
                if value is True:
                    subsets.append(key)
            len_subsets = len(subsets)
            if len_subsets == 0:
                return {
                    'success': True,
                    'message': 'Nothing was selected to delete'
                }
            elif len_subsets == 1:
                title = title.format('{} subset'.format(len_subsets))
            else:
                title = title.format('{} subsets'.format(len_subsets))

        self.values = values
        items = []

        delete_label = {
            'type': 'label',
            'value': '# Please enter "DELETE" to confirm #'
        }

        delete_item = {
            'name': 'delete_key',
            'type': 'text',
            'value': '',
            'empty_text': 'Type Delete here...'
        }
        items.append(delete_label)
        items.append(delete_item)

        return {'items': items, 'title': title}

    def launch(self, session, entities, event):
        if 'values' not in event['data']:
            return

        values = event['data']['values']
        if len(values) <= 0:
            return
        if 'delete_key' not in values:
            return

        if values['delete_key'].lower() != 'delete':
            if values['delete_key'].lower() == '':
                return {'success': False, 'message': 'Deleting cancelled'}
            if self.attempt < 3:
                self.attempt += 1
                return_dict = self.confirm_delete(False, entities, event)
                return_dict['title'] = '{} ({} attempt)'.format(
                    return_dict['title'], self.attempt)
                return return_dict
            return {
                'success': False,
                'message': 'You didn\'t enter "DELETE" properly 3 times!'
            }

        entity = entities[0]
        project = entity['project']

        self.db.Session['AVALON_PROJECT'] = project["full_name"]

        all_ids = []
        if self.values.get('whole_asset', False) is True:
            av_entity = self.db.find_one({
                'type': 'asset',
                'name': entity['name']
            })

            if av_entity is not None:
                all_ids.append(av_entity['_id'])
                all_ids.extend(self.find_child(av_entity))

            session.delete(entity)
            session.commit()
        else:
            subset_names = []
            for key, value in self.values.items():
                if key == 'delete_key' or value is False:
                    continue

                entity_id = ObjectId(key)
                av_entity = self.db.find_one({'_id': entity_id})
                subset_names.append(av_entity['name'])
                if av_entity is None:
                    continue
                all_ids.append(entity_id)
                all_ids.extend(self.find_child(av_entity))

            for ft_asset in entity['assets']:
                if ft_asset['name'] in subset_names:
                    session.delete(ft_asset)
                    session.commit()

        if len(all_ids) == 0:
            return {
                'success': True,
                'message': 'No entities to delete in avalon'
            }

        or_subquery = []
        for id in all_ids:
            or_subquery.append({'_id': id})
        delete_query = {'$or': or_subquery}
        self.db.delete_many(delete_query)

        return {'success': True, 'message': 'All assets were deleted!'}

    def find_child(self, entity):
        output = []
        id = entity['_id']
        visuals = [x for x in self.db.find({'data.visualParent': id})]
        assert len(visuals) == 0, 'This asset has another asset as child'
        childs = self.db.find({'parent': id})
        for child in childs:
            output.append(child['_id'])
            output.extend(self.find_child(child))
        return output

    def find_assets(self, asset_names):
        assets = []
        for name in asset_names:
            entity = self.db.find_one({'type': 'asset', 'name': name})
            if entity is not None and entity not in assets:
                assets.append(entity)
        return assets