diff --git a/igniter/tools.py b/igniter/tools.py index c934289064..04d7451335 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -1,18 +1,12 @@ # -*- coding: utf-8 -*- -"""Tools used in **Igniter** GUI. - -Functions ``compose_url()`` and ``decompose_url()`` are the same as in -``openpype.lib`` and they are here to avoid importing OpenPype module before its -version is decided. - -""" -import sys +"""Tools used in **Igniter** GUI.""" import os -from typing import Dict, Union +from typing import Union from urllib.parse import urlparse, parse_qs from pathlib import Path import platform +import certifi from pymongo import MongoClient from pymongo.errors import ( ServerSelectionTimeoutError, @@ -22,89 +16,32 @@ from pymongo.errors import ( ) -def decompose_url(url: str) -> Dict: - """Decompose mongodb url to its separate components. - - Args: - url (str): Mongodb url. - - Returns: - dict: Dictionary of components. +def should_add_certificate_path_to_mongo_url(mongo_url): + """Check if should add ca certificate to mongo url. + Since 30.9.2021 cloud mongo requires newer certificates that are not + available on most of workstation. This adds path to certifi certificate + which is valid for it. To add the certificate path url must have scheme + 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. """ - components = { - "scheme": None, - "host": None, - "port": None, - "username": None, - "password": None, - "auth_db": None - } + parsed = urlparse(mongo_url) + query = parse_qs(parsed.query) + lowered_query_keys = set(key.lower() for key in query.keys()) + add_certificate = False + # Check if url 'ssl' or 'tls' are set to 'true' + for key in ("ssl", "tls"): + if key in query and "true" in query["ssl"]: + add_certificate = True + break - result = urlparse(url) - if result.scheme is None: - _url = "mongodb://{}".format(url) - result = urlparse(_url) + # Check if url contains 'mongodb+srv' + if not add_certificate and parsed.scheme == "mongodb+srv": + add_certificate = True - components["scheme"] = result.scheme - components["host"] = result.hostname - try: - components["port"] = result.port - except ValueError: - raise RuntimeError("invalid port specified") - components["username"] = result.username - components["password"] = result.password - - try: - components["auth_db"] = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - - return components - - -def compose_url(scheme: str = None, - host: str = None, - username: str = None, - password: str = None, - port: int = None, - auth_db: str = None) -> str: - """Compose mongodb url from its individual components. - - Args: - scheme (str, optional): - host (str, optional): - username (str, optional): - password (str, optional): - port (str, optional): - auth_db (str, optional): - - Returns: - str: mongodb url - - """ - - url = "{scheme}://" - - if username and password: - url += "{username}:{password}@" - - url += "{host}" - if port: - url += ":{port}" - - if auth_db: - url += "?authSource={auth_db}" - - return url.format(**{ - "scheme": scheme, - "host": host, - "username": username, - "password": password, - "port": port, - "auth_db": auth_db - }) + # Check if url does already contain certificate path + if add_certificate and "tlscafile" in lowered_query_keys: + add_certificate = False + return add_certificate def validate_mongo_connection(cnx: str) -> (bool, str): @@ -121,12 +58,18 @@ def validate_mongo_connection(cnx: str) -> (bool, str): if parsed.scheme not in ["mongodb", "mongodb+srv"]: return False, "Not mongodb schema" + kwargs = { + "serverSelectionTimeoutMS": 2000 + } + # Add certificate path if should be required + if should_add_certificate_path_to_mongo_url(cnx): + kwargs["ssl_ca_certs"] = certifi.where() + try: - client = MongoClient( - cnx, - serverSelectionTimeoutMS=2000 - ) + client = MongoClient(cnx, **kwargs) client.server_info() + with client.start_session(): + pass client.close() except ServerSelectionTimeoutError as e: return False, f"Cannot connect to server {cnx} - {e}" @@ -152,10 +95,7 @@ def validate_mongo_string(mongo: str) -> (bool, str): """ if not mongo: return True, "empty string" - parsed = urlparse(mongo) - if parsed.scheme in ["mongodb", "mongodb+srv"]: - return validate_mongo_connection(mongo) - return False, "not valid mongodb schema" + return validate_mongo_connection(mongo) def validate_path_string(path: str) -> (bool, str): @@ -195,21 +135,13 @@ def get_openpype_global_settings(url: str) -> dict: Returns: dict: With settings data. Empty dictionary is returned if not found. """ - try: - components = decompose_url(url) - except RuntimeError: - return {} - mongo_kwargs = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 2000 - } - port = components.get("port") - if port is not None: - mongo_kwargs["port"] = int(port) + kwargs = {} + if should_add_certificate_path_to_mongo_url(url): + kwargs["ssl_ca_certs"] = certifi.where() try: # Create mongo connection - client = MongoClient(**mongo_kwargs) + client = MongoClient(url, **kwargs) # Access settings collection col = client["openpype"]["settings"] # Query global settings diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index 3eb9a5be31..e148e44a27 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -3,4 +3,17 @@ from avalon.tvpaint import pipeline class Creator(PypeCreatorMixin, pipeline.Creator): - pass + @classmethod + def get_dynamic_data(cls, *args, **kwargs): + dynamic_data = super(Creator, cls).get_dynamic_data(*args, **kwargs) + + # Change asset and name by current workfile context + workfile_context = pipeline.get_current_workfile_context() + asset_name = workfile_context.get("asset") + task_name = workfile_context.get("task") + if "asset" not in dynamic_data and asset_name: + dynamic_data["asset"] = asset_name + + if "task" not in dynamic_data and task_name: + dynamic_data["task"] = task_name + return dynamic_data diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 36f0b0c954..c45ff53c3c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -606,7 +606,7 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath - elif pre_behavior == "loop": + elif pre_behavior in ("loop", "repeat"): # Loop backwards from last frame of layer for frame_idx in reversed(range(mark_in_index, frame_start_index)): eq_frame_idx_offset = ( @@ -678,7 +678,7 @@ class ExtractSequence(pyblish.api.Extractor): self._copy_image(eq_frame_filepath, new_filepath) layer_files_by_frame[frame_idx] = new_filepath - elif post_behavior == "loop": + elif post_behavior in ("loop", "repeat"): # Loop backwards from last frame of layer for frame_idx in range(frame_end_index + 1, mark_out_index + 1): eq_frame_idx = frame_idx % frame_count diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index 943cd9fcaf..5735cbc99d 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -1,9 +1,11 @@ """Functions useful for delivery action or loader""" import os import shutil +import glob import clique import collections + def collect_frames(files): """ Returns dict of source path and its frame, if from sequence @@ -228,7 +230,16 @@ def process_sequence( Returns: (collections.defaultdict , int) """ - if not os.path.exists(src_path): + + def hash_path_exist(myPath): + res = myPath.replace('#', '*') + glob_search_results = glob.glob(res) + if len(glob_search_results) > 0: + return True + else: + return False + + if not hash_path_exist(src_path): msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) report_items["Source file was not found"].append(msg) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 8bfaba75d6..0fd4517b5b 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -3,6 +3,7 @@ import sys import time import logging import pymongo +import certifi if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs @@ -85,12 +86,33 @@ def get_default_components(): return decompose_url(mongo_url) -def extract_port_from_url(url): - parsed_url = urlparse(url) - if parsed_url.scheme is None: - _url = "mongodb://{}".format(url) - parsed_url = urlparse(_url) - return parsed_url.port +def should_add_certificate_path_to_mongo_url(mongo_url): + """Check if should add ca certificate to mongo url. + + Since 30.9.2021 cloud mongo requires newer certificates that are not + available on most of workstation. This adds path to certifi certificate + which is valid for it. To add the certificate path url must have scheme + 'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query. + """ + parsed = urlparse(mongo_url) + query = parse_qs(parsed.query) + lowered_query_keys = set(key.lower() for key in query.keys()) + add_certificate = False + # Check if url 'ssl' or 'tls' are set to 'true' + for key in ("ssl", "tls"): + if key in query and "true" in query["ssl"]: + add_certificate = True + break + + # Check if url contains 'mongodb+srv' + if not add_certificate and parsed.scheme == "mongodb+srv": + add_certificate = True + + # Check if url does already contain certificate path + if add_certificate and "tlscafile" in lowered_query_keys: + add_certificate = False + + return add_certificate def validate_mongo_connection(mongo_uri): @@ -106,26 +128,9 @@ def validate_mongo_connection(mongo_uri): passed so probably couldn't connect to mongo server. """ - parsed = urlparse(mongo_uri) - # Force validation of scheme - if parsed.scheme not in ["mongodb", "mongodb+srv"]: - raise pymongo.errors.InvalidURI(( - "Invalid URI scheme:" - " URI must begin with 'mongodb://' or 'mongodb+srv://'" - )) - # we have mongo connection string. Let's try if we can connect. - components = decompose_url(mongo_uri) - mongo_args = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 1000 - } - port = components.get("port") - if port is not None: - mongo_args["port"] = int(port) - - # Create connection - client = pymongo.MongoClient(**mongo_args) - client.server_info() + client = OpenPypeMongoConnection.create_connection( + mongo_uri, retry_attempts=1 + ) client.close() @@ -151,6 +156,8 @@ class OpenPypeMongoConnection: # Naive validation of existing connection try: connection.server_info() + with connection.start_session(): + pass except Exception: connection = None @@ -162,38 +169,53 @@ class OpenPypeMongoConnection: return connection @classmethod - def create_connection(cls, mongo_url, timeout=None): + def create_connection(cls, mongo_url, timeout=None, retry_attempts=None): + parsed = urlparse(mongo_url) + # Force validation of scheme + if parsed.scheme not in ["mongodb", "mongodb+srv"]: + raise pymongo.errors.InvalidURI(( + "Invalid URI scheme:" + " URI must begin with 'mongodb://' or 'mongodb+srv://'" + )) + if timeout is None: timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000) kwargs = { - "host": mongo_url, "serverSelectionTimeoutMS": timeout } + if should_add_certificate_path_to_mongo_url(mongo_url): + kwargs["ssl_ca_certs"] = certifi.where() - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) + mongo_client = pymongo.MongoClient(mongo_url, **kwargs) - mongo_client = pymongo.MongoClient(**kwargs) + if retry_attempts is None: + retry_attempts = 3 - for _retry in range(3): + elif not retry_attempts: + retry_attempts = 1 + + last_exc = None + valid = False + t1 = time.time() + for attempt in range(1, retry_attempts + 1): try: - t1 = time.time() mongo_client.server_info() - - except Exception: - cls.log.warning("Retrying...") - time.sleep(1) - timeout *= 1.5 - - else: + with mongo_client.start_session(): + pass + valid = True break - else: - raise IOError(( - "ERROR: Couldn't connect to {} in less than {:.3f}ms" - ).format(mongo_url, timeout)) + except Exception as exc: + last_exc = exc + if attempt < retry_attempts: + cls.log.warning( + "Attempt {} failed. Retrying... ".format(attempt) + ) + time.sleep(1) + + if not valid: + raise last_exc cls.log.info("Connected to {}, delay {:.3f}s".format( mongo_url, time.time() - t1 diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py index 3869d8ad08..0bd243ab4c 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -10,6 +10,7 @@ from openpype_modules.ftrack.lib import ( CUST_ATTR_GROUP, CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT, default_custom_attributes_definition, app_definitions_from_app_manager, @@ -431,7 +432,7 @@ class CustomAttributes(BaseAction): intent_custom_attr_data = { "label": "Intent", - "key": "intent", + "key": CUST_ATTR_INTENT, "type": "enumerator", "entity_type": "assetversion", "group": CUST_ATTR_GROUP, diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index c73f9b100d..9cbf979239 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -230,7 +230,13 @@ class FtrackModule( return import ftrack_api - from openpype_modules.ftrack.lib import get_openpype_attr + from openpype_modules.ftrack.lib import ( + get_openpype_attr, + default_custom_attributes_definition, + CUST_ATTR_TOOLS, + CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT + ) try: session = self.create_ftrack_session() @@ -255,6 +261,15 @@ class FtrackModule( project_id = project_entity["id"] + ca_defs = default_custom_attributes_definition() + hierarchical_attrs = ca_defs.get("is_hierarchical") or {} + project_attrs = ca_defs.get("show") or {} + ca_keys = ( + set(hierarchical_attrs.keys()) + | set(project_attrs.keys()) + | {CUST_ATTR_TOOLS, CUST_ATTR_APPLICATIONS, CUST_ATTR_INTENT} + ) + cust_attr, hier_attr = get_openpype_attr(session) cust_attr_by_key = {attr["key"]: attr for attr in cust_attr} hier_attrs_by_key = {attr["key"]: attr for attr in hier_attr} @@ -262,6 +277,9 @@ class FtrackModule( failed = {} missing = {} for key, value in attributes_changes.items(): + if key not in ca_keys: + continue + configuration = hier_attrs_by_key.get(key) if not configuration: configuration = cust_attr_by_key.get(key) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index 075694d8f6..1a76905b38 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -17,7 +17,8 @@ from openpype.lib import ( get_pype_execute_args, OpenPypeMongoConnection, get_openpype_version, - get_build_version + get_build_version, + validate_mongo_connection ) from openpype_modules.ftrack import FTRACK_MODULE_DIR from openpype_modules.ftrack.lib import credentials @@ -36,11 +37,15 @@ class MongoPermissionsError(Exception): def check_mongo_url(mongo_uri, log_error=False): """Checks if mongo server is responding""" try: - client = pymongo.MongoClient(mongo_uri) - # Force connection on a request as the connect=True parameter of - # MongoClient seems to be useless here - client.server_info() - client.close() + validate_mongo_connection(mongo_uri) + + except pymongo.errors.InvalidURI as err: + if log_error: + print("Can't connect to MongoDB at {} because: {}".format( + mongo_uri, err + )) + return False + except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: print("Can't connect to MongoDB at {} because: {}".format( diff --git a/openpype/modules/default_modules/ftrack/lib/__init__.py b/openpype/modules/default_modules/ftrack/lib/__init__.py index 433a1f7881..80b4db9dd6 100644 --- a/openpype/modules/default_modules/ftrack/lib/__init__.py +++ b/openpype/modules/default_modules/ftrack/lib/__init__.py @@ -3,7 +3,8 @@ from .constants import ( CUST_ATTR_AUTO_SYNC, CUST_ATTR_GROUP, CUST_ATTR_TOOLS, - CUST_ATTR_APPLICATIONS + CUST_ATTR_APPLICATIONS, + CUST_ATTR_INTENT ) from .settings import ( get_ftrack_event_mongo_info diff --git a/openpype/modules/default_modules/ftrack/lib/constants.py b/openpype/modules/default_modules/ftrack/lib/constants.py index 73d5112e6d..e6e2013d2b 100644 --- a/openpype/modules/default_modules/ftrack/lib/constants.py +++ b/openpype/modules/default_modules/ftrack/lib/constants.py @@ -10,3 +10,5 @@ CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" CUST_ATTR_APPLICATIONS = "applications" # Environment tools custom attribute CUST_ATTR_TOOLS = "tools_env" +# Intent custom attribute name +CUST_ATTR_INTENT = "intent" diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py index ce63d35c8c..07450265e2 100644 --- a/openpype/modules/default_modules/sync_server/providers/sftp.py +++ b/openpype/modules/default_modules/sync_server/providers/sftp.py @@ -8,16 +8,14 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings -from openpype.modules.default_modules.sync_server.providers.abstract_provider \ - import AbstractProvider - +from .abstract_provider import AbstractProvider log = Logger().get_logger("SyncServer") +pysftp = None try: import pysftp except (ImportError, SyntaxError): - if six.PY3: - six.reraise(*sys.exc_info()) + pass # handle imports from Python 2 hosts - in those only basic methods are used log.warning("Import failed, imported from Python 2, operations will fail.") @@ -43,7 +41,7 @@ class SFTPHandler(AbstractProvider): self.project_name = project_name self.site_name = site_name self.root = None - self.conn = None + self._conn = None self.presets = presets if not self.presets: @@ -65,11 +63,17 @@ class SFTPHandler(AbstractProvider): self.sftp_key = provider_presets["sftp_key"] self.sftp_key_pass = provider_presets["sftp_key_pass"] - self.conn = self._get_conn() - self._tree = None self.active = True + @property + def conn(self): + """SFTP connection, cannot be used in all places though.""" + if not self._conn: + self._conn = self._get_conn() + + return self._conn + def is_active(self): """ Returns True if provider is activated, eg. has working credentials. @@ -323,6 +327,7 @@ class SFTPHandler(AbstractProvider): if not self.file_path_exists(path): raise FileNotFoundError("File {} to be deleted doesn't exist." .format(path)) + self.conn.remove(path) def list_folder(self, folder_path): @@ -396,6 +401,9 @@ class SFTPHandler(AbstractProvider): Returns: pysftp.Connection """ + if not pysftp: + raise ImportError + cnopts = pysftp.CnOpts() cnopts.hostkeys = None diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 39b54364d9..5b49bb58d0 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -1,26 +1,28 @@ import os -import subprocess from avalon import api +from openpype.api import ApplicationManager def existing_djv_path(): - djv_paths = os.environ.get("DJV_PATH") or "" - for path in djv_paths.split(os.pathsep): - if os.path.exists(path): - return path - return None + app_manager = ApplicationManager() + djv_list = [] + for app_name, app in app_manager.applications.items(): + if 'djv' in app_name and app.find_executable(): + djv_list.append(app_name) + + return djv_list class OpenInDJV(api.Loader): """Open Image Sequence with system default""" - djv_path = existing_djv_path() - families = ["*"] if djv_path else [] + djv_list = existing_djv_path() + families = ["*"] if djv_list else [] representations = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", - "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" + "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264", ] label = "Open in DJV" @@ -41,20 +43,18 @@ class OpenInDJV(api.Loader): ) if not remainder: - seqeunce = collections[0] - first_image = list(seqeunce)[0] + sequence = collections[0] + first_image = list(sequence)[0] else: first_image = self.fname filepath = os.path.normpath(os.path.join(directory, first_image)) self.log.info("Opening : {}".format(filepath)) - cmd = [ - # DJV path - os.path.normpath(self.djv_path), - # PATH TO COMPONENT - os.path.normpath(filepath) - ] + last_djv_version = sorted(self.djv_list)[-1] - # Run DJV with these commands - subprocess.Popen(cmd) + app_manager = ApplicationManager() + djv = app_manager.applications.get(last_djv_version) + djv.arguments.append(filepath) + + app_manager.launch(last_djv_version) diff --git a/openpype/tools/settings/settings/constants.py b/openpype/tools/settings/settings/constants.py new file mode 100644 index 0000000000..5c20bf1afe --- /dev/null +++ b/openpype/tools/settings/settings/constants.py @@ -0,0 +1,16 @@ +from Qt import QtCore + + +DEFAULT_PROJECT_LABEL = "< Default >" +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 +PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3 + + +__all__ = ( + "DEFAULT_PROJECT_LABEL", + + "PROJECT_NAME_ROLE", + "PROJECT_IS_ACTIVE_ROLE", + "PROJECT_IS_SELECTED_ROLE" +) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index a461f3e675..710884e9e5 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -7,6 +7,12 @@ from avalon.mongodb import ( ) from openpype.settings.lib import get_system_settings +from .constants import ( + DEFAULT_PROJECT_LABEL, + PROJECT_NAME_ROLE, + PROJECT_IS_ACTIVE_ROLE, + PROJECT_IS_SELECTED_ROLE +) class SettingsLineEdit(QtWidgets.QLineEdit): @@ -602,10 +608,63 @@ class NiceCheckbox(QtWidgets.QFrame): return super(NiceCheckbox, self).mouseReleaseEvent(event) -class ProjectListModel(QtGui.QStandardItemModel): - sort_role = QtCore.Qt.UserRole + 10 - filter_role = QtCore.Qt.UserRole + 11 - selected_role = QtCore.Qt.UserRole + 12 +class ProjectModel(QtGui.QStandardItemModel): + def __init__(self, only_active, *args, **kwargs): + super(ProjectModel, self).__init__(*args, **kwargs) + + self.dbcon = None + + self._only_active = only_active + self._default_item = None + self._items_by_name = {} + + def set_dbcon(self, dbcon): + self.dbcon = dbcon + + def refresh(self): + new_items = [] + if self._default_item is None: + item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL) + item.setData(None, PROJECT_NAME_ROLE) + item.setData(True, PROJECT_IS_ACTIVE_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) + new_items.append(item) + self._default_item = item + + project_names = set() + if self.dbcon is not None: + for project_doc in self.dbcon.projects( + projection={"name": 1, "data.active": 1}, + only_active=self._only_active + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) + + self._items_by_name[project_name] = item + new_items.append(item) + + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) + + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) + + root_item = self.invisibleRootItem() + for project_name in tuple(self._items_by_name.keys()): + if project_name not in project_names: + item = self._items_by_name.pop(project_name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) class ProjectListView(QtWidgets.QListView): @@ -618,19 +677,36 @@ class ProjectListView(QtWidgets.QListView): super(ProjectListView, self).mouseReleaseEvent(event) -class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): - +class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(ProjectListSortFilterProxy, self).__init__(*args, **kwargs) + super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) self._enable_filter = True + def lessThan(self, left_index, right_index): + if left_index.data(PROJECT_NAME_ROLE) is None: + return True + + if right_index.data(PROJECT_NAME_ROLE) is None: + return False + + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active == left_is_active: + return super(ProjectSortFilterProxy, self).lessThan( + left_index, right_index + ) + + if left_is_active: + return True + return False + def filterAcceptsRow(self, source_row, source_parent): if not self._enable_filter: return True index = self.sourceModel().index(source_row, 0, source_parent) is_active = bool(index.data(self.filterRole())) - is_selected = bool(index.data(ProjectListModel.selected_role)) + is_selected = bool(index.data(PROJECT_IS_SELECTED_ROLE)) return is_active or is_selected @@ -643,7 +719,6 @@ class ProjectListSortFilterProxy(QtCore.QSortFilterProxyModel): class ProjectListWidget(QtWidgets.QWidget): - default = "< Default >" project_changed = QtCore.Signal() def __init__(self, parent, only_active=False): @@ -657,13 +732,10 @@ class ProjectListWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel("Projects") project_list = ProjectListView(self) - project_model = ProjectListModel() - project_proxy = ProjectListSortFilterProxy() - - project_proxy.setFilterRole(ProjectListModel.filter_role) - project_proxy.setSortRole(ProjectListModel.sort_role) - project_proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + project_model = ProjectModel(only_active) + project_proxy = ProjectSortFilterProxy() + project_proxy.setFilterRole(PROJECT_IS_ACTIVE_ROLE) project_proxy.setSourceModel(project_model) project_list.setModel(project_proxy) @@ -693,13 +765,14 @@ class ProjectListWidget(QtWidgets.QWidget): project_list.left_mouse_released_at.connect(self.on_item_clicked) + self._default_project_item = None + self.project_list = project_list self.project_proxy = project_proxy self.project_model = project_model self.inactive_chk = inactive_chk self.dbcon = None - self._only_active = only_active def on_item_clicked(self, new_index): new_project_name = new_index.data(QtCore.Qt.DisplayRole) @@ -746,12 +819,12 @@ class ProjectListWidget(QtWidgets.QWidget): return not self._parent.entity.has_unsaved_changes def project_name(self): - if self.current_project == self.default: + if self.current_project == DEFAULT_PROJECT_LABEL: return None return self.current_project def select_default_project(self): - self.select_project(self.default) + self.select_project(DEFAULT_PROJECT_LABEL) def select_project(self, project_name): model = self.project_model @@ -759,10 +832,10 @@ class ProjectListWidget(QtWidgets.QWidget): found_items = model.findItems(project_name) if not found_items: - found_items = model.findItems(self.default) + found_items = model.findItems(DEFAULT_PROJECT_LABEL) index = model.indexFromItem(found_items[0]) - model.setData(index, True, ProjectListModel.selected_role) + model.setData(index, True, PROJECT_IS_SELECTED_ROLE) index = proxy.mapFromSource(index) @@ -777,9 +850,6 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(QtCore.Qt.DisplayRole) break - model = self.project_model - model.clear() - mongo_url = os.environ["OPENPYPE_MONGO"] # Force uninstall of whole avalon connection if url does not match @@ -797,35 +867,8 @@ class ProjectListWidget(QtWidgets.QWidget): self.dbcon = None self.current_project = None - items = [(self.default, True)] - - if self.dbcon: - - for doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - items.append( - (doc["name"], doc.get("data", {}).get("active", True)) - ) - - for project_name, is_active in items: - - row = QtGui.QStandardItem(project_name) - row.setData(is_active, ProjectListModel.filter_role) - row.setData(False, ProjectListModel.selected_role) - - if is_active: - row.setData(project_name, ProjectListModel.sort_role) - - else: - row.setData("~" + project_name, ProjectListModel.sort_role) - - font = row.font() - font.setItalic(True) - row.setFont(font) - - model.appendRow(row) + self.project_model.set_dbcon(self.dbcon) + self.project_model.refresh() self.project_proxy.sort(0) diff --git a/repos/avalon-core b/repos/avalon-core index 8aee68fa10..4b80f81e66 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 8aee68fa10ab4d79be1a91e7728a609748e7c3c6 +Subproject commit 4b80f81e66aca593784be8b299110a0b6541276f diff --git a/start.py b/start.py index 689efbdac1..ada613b4eb 100644 --- a/start.py +++ b/start.py @@ -102,9 +102,6 @@ import subprocess import site from pathlib import Path -from igniter.tools import get_openpype_global_settings - - # OPENPYPE_ROOT is variable pointing to build (or code) directory # WARNING `OPENPYPE_ROOT` must be defined before igniter import # - igniter changes cwd which cause that filepath of this script won't lead @@ -192,6 +189,7 @@ else: import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( + get_openpype_global_settings, get_openpype_path_from_db, validate_mongo_connection ) # noqa