diff --git a/openpype/client/entity_links.py b/openpype/client/entity_links.py index e42ac58aff..b74b4ce7f6 100644 --- a/openpype/client/entity_links.py +++ b/openpype/client/entity_links.py @@ -164,7 +164,6 @@ def get_linked_representation_id( # Recursive graph lookup for inputs {"$graphLookup": graph_lookup} ] - conn = get_project_connection(project_name) result = conn.aggregate(query_pipeline) referenced_version_ids = _process_referenced_pipeline_result( @@ -213,7 +212,7 @@ def _process_referenced_pipeline_result(result, link_type): for output in sorted(outputs_recursive, key=lambda o: o["depth"]): output_links = output.get("data", {}).get("inputLinks") - if not output_links: + if not output_links and output["type"] != "hero_version": continue # Leaf @@ -232,6 +231,9 @@ def _process_referenced_pipeline_result(result, link_type): def _filter_input_links(input_links, link_type, correctly_linked_ids): + if not input_links: # to handle hero versions + return + for input_link in input_links: if link_type and input_link["type"] != link_type: continue diff --git a/openpype/hosts/blender/plugins/load/import_workfile.py b/openpype/hosts/blender/plugins/load/import_workfile.py index 618fb83e31..bbdf1c7ea0 100644 --- a/openpype/hosts/blender/plugins/load/import_workfile.py +++ b/openpype/hosts/blender/plugins/load/import_workfile.py @@ -44,7 +44,7 @@ class AppendBlendLoader(plugin.AssetLoader): """ representations = ["blend"] - families = ["*"] + families = ["workfile"] label = "Append Workfile" order = 9 @@ -68,7 +68,7 @@ class ImportBlendLoader(plugin.AssetLoader): """ representations = ["blend"] - families = ["*"] + families = ["workfile"] label = "Import Workfile" order = 9 diff --git a/openpype/plugins/load/add_site.py b/openpype/plugins/load/add_site.py index ac931e41db..e31f746f51 100644 --- a/openpype/plugins/load/add_site.py +++ b/openpype/plugins/load/add_site.py @@ -34,12 +34,24 @@ class AddSyncSite(load.LoaderPlugin): return self._sync_server def load(self, context, name=None, namespace=None, data=None): - self.log.info("Adding {} to representation: {}".format( - data["site_name"], data["_id"])) - family = context["representation"]["context"]["family"] - project_name = data["project_name"] - repre_id = data["_id"] + """"Adds site skeleton information on representation_id + + Looks for loaded containers for workfile, adds them site skeleton too + (eg. they should be downloaded too). + Args: + context (dict): + name (str): + namespace (str): + data (dict): expects {"site_name": SITE_NAME_TO_ADD} + """ + # self.log wont propagate + project_name = context["project"]["name"] + repre_doc = context["representation"] + family = repre_doc["context"]["family"] + repre_id = repre_doc["_id"] site_name = data["site_name"] + print("Adding {} to representation: {}".format( + data["site_name"], repre_id)) self.sync_server.add_site(project_name, repre_id, site_name, force=True) @@ -52,6 +64,8 @@ class AddSyncSite(load.LoaderPlugin): ) for link_repre_id in links: try: + print("Adding {} to linked representation: {}".format( + data["site_name"], link_repre_id)) self.sync_server.add_site(project_name, link_repre_id, site_name, force=False) diff --git a/openpype/plugins/load/remove_site.py b/openpype/plugins/load/remove_site.py index c5f442b2f5..bea8b1b346 100644 --- a/openpype/plugins/load/remove_site.py +++ b/openpype/plugins/load/remove_site.py @@ -3,7 +3,10 @@ from openpype.pipeline import load class RemoveSyncSite(load.LoaderPlugin): - """Remove sync site and its files on representation""" + """Remove sync site and its files on representation. + + Removes files only on local site! + """ representations = ["*"] families = ["*"] @@ -24,13 +27,18 @@ class RemoveSyncSite(load.LoaderPlugin): return self._sync_server def load(self, context, name=None, namespace=None, data=None): - self.log.info("Removing {} on representation: {}".format( - data["site_name"], data["_id"])) - self.sync_server.remove_site(data["project_name"], - data["_id"], - data["site_name"], + project_name = context["project"]["name"] + repre_doc = context["representation"] + repre_id = repre_doc["_id"] + site_name = data["site_name"] + + print("Removing {} on representation: {}".format(site_name, repre_id)) + + self.sync_server.remove_site(project_name, + repre_id, + site_name, True) - self.log.debug("Site added.") + self.log.debug("Site removed.") def filepath_from_context(self, context): """No real file loading""" diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7b73943c37..b117006871 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -506,6 +506,43 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return version_doc + def _validate_repre_files(self, files, is_sequence_representation): + """Validate representation files before transfer preparation. + + Check if files contain only filenames instead of full paths and check + if sequence don't contain more than one sequence or has remainders. + + Args: + files (Union[str, List[str]]): Files from representation. + is_sequence_representation (bool): Files are for sequence. + + Raises: + KnownPublishError: If validations don't pass. + """ + + if not files: + return + + if not is_sequence_representation: + files = [files] + + if any(os.path.isabs(fname) for fname in files): + raise KnownPublishError("Given file names contain full paths") + + if not is_sequence_representation: + return + + src_collections, remainders = clique.assemble(files) + if len(files) < 2 or len(src_collections) != 1 or remainders: + raise KnownPublishError(( + "Files of representation does not contain proper" + " sequence files.\nCollected collections: {}" + "\nCollected remainders: {}" + ).format( + ", ".join([str(col) for col in src_collections]), + ", ".join([str(rem) for rem in remainders]) + )) + def prepare_representation(self, repre, template_name, existing_repres_by_name, @@ -587,7 +624,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): is_udim = bool(repre.get("udim")) # handle publish in place - if "originalDirname" in template: + if "{originalDirname}" in template: # store as originalDirname only original value without project root # if instance collected originalDirname is present, it should be # used for all represe @@ -606,24 +643,64 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["originalDirname"] = without_root is_sequence_representation = isinstance(files, (list, tuple)) - if is_sequence_representation: - # Collection of files (sequence) - if any(os.path.isabs(fname) for fname in files): - raise KnownPublishError("Given file names contain full paths") + self._validate_repre_files(files, is_sequence_representation) + # Output variables of conditions below: + # - transfers (List[Tuple[str, str]]): src -> dst filepaths to copy + # - repre_context (Dict[str, Any]): context data used to fill template + # - template_data (Dict[str, Any]): source data used to fill template + # - to add required data to 'repre_context' not used for + # formatting + # - anatomy_filled (Dict[str, Any]): filled anatomy of last file + # - to fill 'publishDir' on instance.data -> not ideal + + # Treat template with 'orignalBasename' in special way + if "{originalBasename}" in template: + # Remove 'frame' from template data + template_data.pop("frame", None) + + # Find out first frame string value + first_index_padded = None + if not is_udim and is_sequence_representation: + col = clique.assemble(files)[0][0] + sorted_frames = tuple(sorted(col.indexes)) + # First frame used for end value + first_frame = sorted_frames[0] + # Get last frame for padding + last_frame = sorted_frames[-1] + # Use padding from collection of length of last frame as string + padding = max(col.padding, len(str(last_frame))) + first_index_padded = get_frame_padded( + frame=first_frame, + padding=padding + ) + + # Convert files to list for single file as remaining part is only + # transfers creation (iteration over files) + if not is_sequence_representation: + files = [files] + + repre_context = None + transfers = [] + for src_file_name in files: + template_data["originalBasename"], _ = os.path.splitext( + src_file_name) + + anatomy_filled = anatomy.format(template_data) + dst = anatomy_filled[template_name]["path"] + src = os.path.join(stagingdir, src_file_name) + transfers.append((src, dst)) + if repre_context is None: + repre_context = dst.used_values + + if not is_udim and first_index_padded is not None: + repre_context["frame"] = first_index_padded + + elif is_sequence_representation: + # Collection of files (sequence) src_collections, remainders = clique.assemble(files) - if len(files) < 2 or len(src_collections) != 1 or remainders: - raise KnownPublishError(( - "Files of representation does not contain proper" - " sequence files.\nCollected collections: {}" - "\nCollected remainders: {}" - ).format( - ", ".join([str(col) for col in src_collections]), - ", ".join([str(rem) for rem in remainders]) - )) src_collection = src_collections[0] - template_data["originalBasename"] = src_collection.head[:-1] destination_indexes = list(src_collection.indexes) # Use last frame for minimum padding # - that should cover both 'udim' and 'frame' minimum padding @@ -645,11 +722,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # In case source are published in place we need to # skip renumbering repre_frame_start = repre.get("frameStart") - if ( - "originalBasename" not in template - and repre_frame_start is not None - ): - index_frame_start = int(repre["frameStart"]) + if repre_frame_start is not None: + index_frame_start = int(repre_frame_start) # Shift destination sequence to the start frame destination_indexes = [ index_frame_start + idx @@ -705,15 +779,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): else: # Single file - fname = files - if os.path.isabs(fname): - self.log.error( - "Filename in representation is filepath {}".format(fname) - ) - raise KnownPublishError( - "This is a bug. Representation file name is full path" - ) - template_data["originalBasename"], _ = os.path.splitext(fname) # Manage anatomy template data template_data.pop("frame", None) if is_udim: @@ -725,7 +790,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): dst = os.path.normpath(template_filled) # Single file transfer - src = os.path.join(stagingdir, fname) + src = os.path.join(stagingdir, files) transfers = [(src, dst)] # todo: Are we sure the assumption each representation diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index c0e68fcc7a..0c5c9391cf 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -1480,23 +1480,21 @@ class RepresentationWidget(QtWidgets.QWidget): repre_ids = [] data_by_repre_id = {} selected_side = action_representation.get("selected_side") + site_name = "{}_site_name".format(selected_side) is_sync_loader = tools_lib.is_sync_loader(loader) for item in items: - item_id = item.get("_id") - repre_ids.append(item_id) + repre_id = item["_id"] + repre_ids.append(repre_id) if not is_sync_loader: continue - site_name = "{}_site_name".format(selected_side) data_site_name = item.get(site_name) if not data_site_name: continue - data_by_repre_id[item_id] = { - "_id": item_id, - "site_name": data_site_name, - "project_name": self.dbcon.active_project() + data_by_repre_id[repre_id] = { + "site_name": data_site_name } repre_contexts = get_repres_contexts(repre_ids, self.dbcon) @@ -1586,8 +1584,8 @@ def _load_representations_by_loader(loader, repre_contexts, version_name = version_doc.get("name") try: if data_by_repre_id: - _id = repre_context["representation"]["_id"] - data = data_by_repre_id.get(_id) + repre_id = repre_context["representation"]["_id"] + data = data_by_repre_id.get(repre_id) options.update(data) load_with_repre_context( loader,