From d5eb65100f5d27820b3995310e7c0242fa669da1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 11:51:20 +0100 Subject: [PATCH 01/53] added first version of clockifyAPI class --- pype/clockify/__init__.py | 3 + pype/clockify/clockify.py | 188 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 pype/clockify/__init__.py create mode 100644 pype/clockify/clockify.py diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py new file mode 100644 index 0000000000..620e7e8a5c --- /dev/null +++ b/pype/clockify/__init__.py @@ -0,0 +1,3 @@ +from .clockify import ClockifyAPI + +__all__ = ['ClockifyAPI'] diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py new file mode 100644 index 0000000000..50b261fd2f --- /dev/null +++ b/pype/clockify/clockify.py @@ -0,0 +1,188 @@ +import os +import requests +import json +import datetime +import appdirs + + +class ClockifyAPI: + endpoint = "https://api.clockify.me/api/" + headers = {"X-Api-Key": None} + app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) + file_name = 'clockify.json' + fpath = os.path.join(app_dir, file_name) + workspace = None + + def __init__(self, workspace=None, debug=False): + self.debug = debug + self.set_api() + if workspace is not None: + self.set_workspace(workspace) + + def set_api(self): + api_key = self.get_api_key() + if api_key is not None: + self.headers["X-Api-Key"] = api_key + return + + raise ValueError('Api key is not set') + + def set_workspace(self, name): + all_workspaces = self.get_workspaces() + if name in all_workspaces: + self.workspace = name + return + + def get_api_key(self): + credentials = None + try: + file = open(self.fpath, 'r') + credentials = json.load(file).get('api_key', None) + except Exception: + file = open(self.fpath, 'w') + file.close() + return credentials + + def get_workspaces(self): + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return { + workspace["name"]: workspace["id"] for workspace in response.json() + } + + def get_projects(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/projects/'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + project["name"]: project["id"] for project in response.json() + } + + def print_json(self, inputjson): + print(json.dumps(inputjson, indent=2)) + + def get_current_time(self): + return str(datetime.datetime.utcnow().isoformat())+'Z' + + def start_time_entry( + self, description, project_id, task_id, billable="true", workspace=None + ): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/'.format(workspace) + start = self.get_current_time() + body = { + "start": start, + "billable": billable, + "description": description, + "projectId": project_id, + "taskId": task_id, + "tagIds": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_in_progress(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/inProgress'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def finish_time_entry(self, workspace=None): + if workspace is None: + workspace = self.workspace + current = self.get_in_progress(workspace) + current_id = current["id"] + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace, current_id + ) + body = { + "start": current["timeInterval"]["start"], + "billable": current["billable"], + "description": current["description"], + "projectId": current["projectId"], + "taskId": current["taskId"], + "tagIds": current["tagIds"], + "end": self.get_current_time() + } + response = requests.put( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_time_entries(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json()[:10] + + def remove_time_entry(self, tid, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/{tid}'.format(workspace) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def add_project(self, name, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/projects/'.format(workspace) + body = { + "name": name, + "clientId": "", + "isPublic": "false", + "estimate": None, + "color": None, + "billable": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def add_workspace(self, name): + action_url = 'workspaces/' + body = {"name": name} + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + +def main(): + clockify = ClockifyAPI() + from pprint import pprint + pprint(clockify.get_workspaces()) + + +if __name__ == "__main__": + main() From 393d2acc37e2e5e1a64166c03fe89ad962a03cce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:01 +0100 Subject: [PATCH 02/53] added widget for settings --- pype/clockify/widget_settings.py | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 pype/clockify/widget_settings.py diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py new file mode 100644 index 0000000000..8b8abb25da --- /dev/null +++ b/pype/clockify/widget_settings.py @@ -0,0 +1,150 @@ +import os +from app.vendor.Qt import QtCore, QtGui, QtWidgets +from app import style + + +class ClockifySettings(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 130 + + loginSignal = QtCore.Signal(object, object, object) + + def __init__(self, main_parent=None, parent=None, optional=True): + + super(ClockifySettings, self).__init__() + + self.parent = parent + self.main_parent = main_parent + self.clockapi = parent.clockapi + self.optional = optional + self.validated = False + + # Icon + if hasattr(parent, 'icon'): + self.setWindowIcon(self.parent.icon) + elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): + self.setWindowIcon(self.parent.parent.icon) + else: + pype_setup = os.getenv('PYPE_SETUP_ROOT') + items = [pype_setup, "app", "resources", "icon.png"] + fname = os.path.sep.join(items) + icon = QtGui.QIcon(fname) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint | + QtCore.Qt.WindowMinimizeButtonHint + ) + + self._translate = QtCore.QCoreApplication.translate + + # Font + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + # Size setting + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.setWindowTitle('Clockify settings') + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + self.label_api_key = QtWidgets.QLabel("Clockify API key:") + self.label_api_key.setFont(self.font) + self.label_api_key.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.label_api_key.setTextFormat(QtCore.Qt.RichText) + self.label_api_key.setObjectName("label_api_key") + + self.input_api_key = QtWidgets.QLineEdit() + self.input_api_key.setEnabled(True) + self.input_api_key.setFrame(True) + self.input_api_key.setObjectName("input_api_key") + self.input_api_key.setPlaceholderText( + self._translate("main", "e.g. XX1XxXX2x3x4xXxx") + ) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setFont(self.font) + self.error_label.setTextFormat(QtCore.Qt.RichText) + self.error_label.setObjectName("error_label") + self.error_label.setWordWrap(True) + self.error_label.hide() + + self.form.addRow(self.label_api_key, self.input_api_key) + self.form.addRow(self.error_label) + + self.btn_group = QtWidgets.QHBoxLayout() + self.btn_group.addStretch(1) + self.btn_group.setObjectName("btn_group") + + self.btn_ok = QtWidgets.QPushButton("Ok") + self.btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') + self.btn_ok.clicked.connect(self.click_ok) + + self.btn_cancel = QtWidgets.QPushButton("Cancel") + cancel_tooltip = 'Application won\'t start' + if self.optional: + cancel_tooltip = 'Close this window' + self.btn_cancel.setToolTip(cancel_tooltip) + self.btn_cancel.clicked.connect(self._close_widget) + + self.btn_group.addWidget(self.btn_ok) + self.btn_group.addWidget(self.btn_cancel) + + self.main.addLayout(self.form) + self.main.addLayout(self.btn_group) + + return self.main + + def setError(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def invalid_input(self, entity): + entity.setStyleSheet("border: 1px solid red;") + + def click_ok(self): + api_key = self.input_api_key.text().strip() + if self.optional: + if api_key == '': + self.clockapi.save_api_key(None) + self.clockapi.set_api(api_key) + self.validated = False + self._close_widget() + return + + validation = self.clockapi.validate_api_key(api_key) + + if validation: + self.clockapi.save_api_key(api_key) + self.clockapi.set_api(api_key) + self.validated = True + self._close_widget() + else: + self.invalid_input(self.input_api_key) + self.validated = False + self.setError( + "Entered invalid API key" + ) + + def closeEvent(self, event): + event.ignore() + self._close_widget() + + def _close_widget(self): + self.hide() From cf0cebf42e13cb80fed67612b94a6020435c9e21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:17 +0100 Subject: [PATCH 03/53] just little fix in assetcreator --- pype/plugins/launcher/actions/AssetCreator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/launcher/actions/AssetCreator.py b/pype/plugins/launcher/actions/AssetCreator.py index d6875bd7ff..ff06895ae0 100644 --- a/pype/plugins/launcher/actions/AssetCreator.py +++ b/pype/plugins/launcher/actions/AssetCreator.py @@ -7,7 +7,7 @@ from pype.tools import assetcreator from pype.api import Logger -log = Logger.getLogger(__name__, "aport") +log = Logger.getLogger(__name__, "asset_creator") class AssetCreator(api.Action): From 2565b08542f09caf7f9cd93badbadb62659be1bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:30 +0100 Subject: [PATCH 04/53] added startclockify timer to launcher actions --- .../plugins/launcher/actions/StartClockify.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pype/plugins/launcher/actions/StartClockify.py diff --git a/pype/plugins/launcher/actions/StartClockify.py b/pype/plugins/launcher/actions/StartClockify.py new file mode 100644 index 0000000000..91adfe0185 --- /dev/null +++ b/pype/plugins/launcher/actions/StartClockify.py @@ -0,0 +1,40 @@ +from avalon import api, io +from pype.clockify import ClockifyAPI +from pype.api import Logger +log = Logger.getLogger(__name__, "start_clockify") + + +class StartClockify(api.Action): + + name = "start_clockify_timer" + label = "Start Timer - Clockify" + icon = "clockify_icon" + order = 500 + + def is_compatible(self, session): + """Return whether the action is compatible with the session""" + if "AVALON_TASK" in session: + return True + return False + + def process(self, session, **kwargs): + clockapi = ClockifyAPI() + project_name = session['AVALON_PROJECT'] + asset_name = session['AVALON_ASSET'] + task_name = session['AVALON_TASK'] + + description = asset_name + asset = io.find_one({ + 'type': 'asset', + 'name': asset_name + }) + if asset is not None: + desc_items = asset.get('data', {}).get('parents', []) + desc_items.append(asset_name) + description = '/'.join(desc_items) + + clockapi.start_time_entry( + description=description, + project_name=project_name, + task_name=task_name, + ) From 9c27a64f625ff31e14fcbb48f58fa469528c3a6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:48:05 +0100 Subject: [PATCH 05/53] separated clockify into api and module --- pype/clockify/__init__.py | 10 +- pype/clockify/clockify.py | 220 +++++---------------- pype/clockify/clockify_api.py | 349 ++++++++++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 172 deletions(-) create mode 100644 pype/clockify/clockify_api.py diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py index 620e7e8a5c..5f61acd751 100644 --- a/pype/clockify/__init__.py +++ b/pype/clockify/__init__.py @@ -1,3 +1,9 @@ -from .clockify import ClockifyAPI +from .clockify_api import ClockifyAPI +from .widget_settings import ClockifySettings +from .clockify import ClockifyModule -__all__ = ['ClockifyAPI'] +__all__ = [ + 'ClockifyAPI', + 'ClockifySettings', + 'ClockifyModule' +] diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 50b261fd2f..1541807a46 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,188 +1,68 @@ import os -import requests -import json -import datetime -import appdirs +from app import style +from app.vendor.Qt import QtWidgets +from pype.clockify import ClockifySettings, ClockifyAPI -class ClockifyAPI: - endpoint = "https://api.clockify.me/api/" - headers = {"X-Api-Key": None} - app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) - file_name = 'clockify.json' - fpath = os.path.join(app_dir, file_name) - workspace = None +class ClockifyModule: - def __init__(self, workspace=None, debug=False): - self.debug = debug - self.set_api() - if workspace is not None: - self.set_workspace(workspace) + def __init__(self, main_parent=None, parent=None): + self.main_parent = main_parent + self.parent = parent + self.clockapi = ClockifyAPI() + self.widget_settings = ClockifySettings(main_parent, self) - def set_api(self): - api_key = self.get_api_key() - if api_key is not None: - self.headers["X-Api-Key"] = api_key + # Bools + self.bool_api_key_set = False + self.bool_workspace_set = False + self.bool_timer_run = False + + def start_up(self): + self.bool_api_key_set = self.clockapi.set_api() + if self.bool_api_key_set is False: + self.show_settings() return - raise ValueError('Api key is not set') - - def set_workspace(self, name): - all_workspaces = self.get_workspaces() - if name in all_workspaces: - self.workspace = name + workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) + print(workspace) + self.bool_workspace_set = self.clockapi.set_workspace(workspace) + if self.bool_workspace_set is False: + # TODO show message to user + print("Nope Workspace: clockify.py - line 29") return + if self.clockapi.get_in_progress() is not None: + self.bool_timer_run = True - def get_api_key(self): - credentials = None - try: - file = open(self.fpath, 'r') - credentials = json.load(file).get('api_key', None) - except Exception: - file = open(self.fpath, 'w') - file.close() - return credentials + self.set_menu_visibility() - def get_workspaces(self): - action_url = 'workspaces/' - response = requests.get( - self.endpoint + action_url, - headers=self.headers + # Definition of Tray menu + def tray_menu(self, parent): + # Menu for Tray App + self.menu = QtWidgets.QMenu('Clockify', parent) + self.menu.setProperty('submenu', 'on') + self.menu.setStyleSheet(style.load_stylesheet()) + + # Actions + self.aShowSettings = QtWidgets.QAction( + "Settings", self.menu ) - return { - workspace["name"]: workspace["id"] for workspace in response.json() - } - - def get_projects(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/projects/'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers + self.aStopTimer = QtWidgets.QAction( + "Stop timer", self.menu ) - return { - project["name"]: project["id"] for project in response.json() - } + self.menu.addAction(self.aShowSettings) + self.menu.addAction(self.aStopTimer) - def print_json(self, inputjson): - print(json.dumps(inputjson, indent=2)) + self.aShowSettings.triggered.connect(self.show_settings) + self.aStopTimer.triggered.connect(self.clockapi.finish_time_entry) - def get_current_time(self): - return str(datetime.datetime.utcnow().isoformat())+'Z' + self.set_menu_visibility() - def start_time_entry( - self, description, project_id, task_id, billable="true", workspace=None - ): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/'.format(workspace) - start = self.get_current_time() - body = { - "start": start, - "billable": billable, - "description": description, - "projectId": project_id, - "taskId": task_id, - "tagIds": None - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() + return self.menu - def get_in_progress(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/inProgress'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers - ) - return response.json() + def show_settings(self): + self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) + self.widget_settings.show() - def finish_time_entry(self, workspace=None): - if workspace is None: - workspace = self.workspace - current = self.get_in_progress(workspace) - current_id = current["id"] - action_url = 'workspaces/{}/timeEntries/{}'.format( - workspace, current_id - ) - body = { - "start": current["timeInterval"]["start"], - "billable": current["billable"], - "description": current["description"], - "projectId": current["projectId"], - "taskId": current["taskId"], - "tagIds": current["tagIds"], - "end": self.get_current_time() - } - response = requests.put( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - def get_time_entries(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers - ) - return response.json()[:10] - - def remove_time_entry(self, tid, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/{tid}'.format(workspace) - response = requests.delete( - self.endpoint + action_url, - headers=self.headers - ) - return response.json() - - def add_project(self, name, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/projects/'.format(workspace) - body = { - "name": name, - "clientId": "", - "isPublic": "false", - "estimate": None, - "color": None, - "billable": None - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - def add_workspace(self, name): - action_url = 'workspaces/' - body = {"name": name} - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - -def main(): - clockify = ClockifyAPI() - from pprint import pprint - pprint(clockify.get_workspaces()) - - -if __name__ == "__main__": - main() + def set_menu_visibility(self): + self.aStopTimer.setVisible(self.bool_timer_run) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py new file mode 100644 index 0000000000..160afe914b --- /dev/null +++ b/pype/clockify/clockify_api.py @@ -0,0 +1,349 @@ +import os +import requests +import json +import datetime +import appdirs + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super( + Singleton, cls + ).__call__(*args, **kwargs) + return cls._instances[cls] + + +class ClockifyAPI(metaclass=Singleton): + endpoint = "https://api.clockify.me/api/" + headers = {"X-Api-Key": None} + app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) + file_name = 'clockify.json' + fpath = os.path.join(app_dir, file_name) + + def set_api(self, api_key=None): + if api_key is None: + api_key = self.get_api_key() + + if api_key is not None and self.validate_api_key(api_key) is True: + self.headers["X-Api-Key"] = api_key + return True + return False + + def validate_api_key(self, api_key): + test_headers = {'X-Api-Key': api_key} + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=test_headers + ) + if response.status_code != 200: + return False + return True + + def set_workspace(self, name=None): + if name is None: + self.workspace = None + self.workspace_id = None + return + result = self.validate_workspace(name) + if result is False: + self.workspace = None + self.workspace_id = None + return False + else: + self.workspace = name + self.workspace_id = result + return True + + def validate_workspace(self, name): + all_workspaces = self.get_workspaces() + if name in all_workspaces: + return all_workspaces[name] + return False + + def get_api_key(self): + api_key = None + try: + file = open(self.fpath, 'r') + api_key = json.load(file).get('api_key', None) + if api_key == '': + api_key = None + except Exception: + file = open(self.fpath, 'w') + file.close() + return api_key + + def save_api_key(self, api_key): + data = {'api_key': api_key} + file = open(self.fpath, 'w') + file.write(json.dumps(data)) + file.close() + + def get_workspaces(self): + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return { + workspace["name"]: workspace["id"] for workspace in response.json() + } + + def get_projects(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/projects/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + project["name"]: project["id"] for project in response.json() + } + + def get_tags( + self, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/tags/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + tag["name"]: tag["id"] for tag in response.json() + } + + def get_tasks( + self, + workspace_name=None, project_name=None, + workspace_id=None, project_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = 'workspaces/{}/projects/{}/tasks/'.format( + workspace_id, project_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + task["name"]: task["id"] for task in response.json() + } + + def get_workspace_id(self, workspace_name): + all_workspaces = self.get_workspaces() + if workspace_name not in all_workspaces: + return None + return all_workspaces[workspace_name] + + def get_project_id( + self, project_name, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + all_projects = self.get_projects(workspace_id=workspace_id) + if project_name not in all_projects: + return None + return all_projects[project_name] + + def get_tag_id(self, tag_name, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + all_tasks = self.get_tags(workspace_id=workspace_id) + if tag_name not in all_tasks: + return None + return all_tasks[tag_name] + + def get_task_id( + self, task_name, + project_name=None, workspace_name=None, + project_id=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + all_tasks = self.get_tasks( + workspace_id=workspace_id, project_id=project_id + ) + if task_name not in all_tasks: + return None + return all_tasks[task_name] + + def get_current_time(self): + return str(datetime.datetime.utcnow().isoformat())+'Z' + + def start_time_entry( + self, description, project_name=None, task_name=None, + billable=True, workspace_name=None, + project_id=None, task_id=None, workspace_id=None + ): + # Workspace + workspace_id = self.convert_input(workspace_id, workspace_name) + # Project + project_id = self.convert_input( + project_id, project_name, 'Project' + ) + # Task + task_id = self.convert_input(task_id, task_name, 'Task', project_id) + + # Check if is currently run another times and has same values + current = self.get_in_progress(workspace_id=workspace_id) + if current is not None: + if ( + current.get("description", None) == description and + current.get("projectId", None) == project_id and + current.get("taskId", None) == task_id + ): + self.bool_timer_run = True + return self.bool_timer_run + return False + + # Convert billable to strings + if billable: + billable = 'true' + else: + billable = 'false' + # Rest API Action + action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) + start = self.get_current_time() + body = { + "start": start, + "billable": billable, + "description": description, + "projectId": project_id, + "taskId": task_id, + "tagIds": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + + success = False + if response.status_code < 300: + success = True + return success + + def get_in_progress(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/inProgress'.format( + workspace_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + try: + output = response.json() + except json.decoder.JSONDecodeError: + output = None + return output + + def finish_time_entry(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + current = self.get_in_progress(workspace_id=workspace_id) + current_id = current["id"] + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace_id, current_id + ) + body = { + "start": current["timeInterval"]["start"], + "billable": current["billable"], + "description": current["description"], + "projectId": current["projectId"], + "taskId": current["taskId"], + "tagIds": current["tagIds"], + "end": self.get_current_time() + } + response = requests.put( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_time_entries( + self, quantity=10, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json()[:quantity] + + def remove_time_entry(self, tid, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace_id, tid + ) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def add_project(self, name, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/projects/'.format(workspace_id) + body = { + "name": name, + "clientId": "", + "isPublic": "false", + "estimate": None, + "color": None, + "billable": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def add_workspace(self, name): + action_url = 'workspaces/' + body = {"name": name} + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def convert_input( + self, entity_id, entity_name, mode='Workspace', project_id=None + ): + if entity_id is None: + error = False + error_msg = 'Missing information "{}"' + if mode.lower() == 'workspace': + if entity_id is None and entity_name is None: + if self.workspace_id is not None: + entity_id = self.workspace_id + else: + error = True + else: + entity_id = self.get_workspace_id(entity_name) + else: + if entity_id is None and entity_name is None: + error = True + elif mode.lower() == 'project': + entity_id = self.get_project_id(entity_name) + elif mode.lower() == 'task': + entity_id = self.get_task_id( + task_name=entity_name, project_id=project_id + ) + else: + raise TypeError('Unknown type') + # Raise error + if error: + raise ValueError(error_msg.format(mode)) + + return entity_id From c098d5b0aa83192cc022818250624a330d92c125 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:17:18 +0100 Subject: [PATCH 06/53] added ftrack action for start timer --- pype/ftrack/actions/action_start_clockify.py | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 pype/ftrack/actions/action_start_clockify.py diff --git a/pype/ftrack/actions/action_start_clockify.py b/pype/ftrack/actions/action_start_clockify.py new file mode 100644 index 0000000000..2a980de6c0 --- /dev/null +++ b/pype/ftrack/actions/action_start_clockify.py @@ -0,0 +1,102 @@ +import sys +import argparse +import logging + +import ftrack_api +from pype.ftrack import BaseAction +from pype.clockify import ClockifyAPI + + +class StartClockify(BaseAction): + '''Starts timer on clockify.''' + + #: Action identifier. + identifier = 'start.clockify.timer' + #: Action label. + label = 'Start timer' + #: Action description. + description = 'Starts timer on clockify' + #: roles that are allowed to register this action + icon = 'https://clockify.me/assets/images/clockify-logo.png' + #: Clockify api + clockapi = ClockifyAPI() + + def discover(self, session, entities, event): + if len(entities) != 1: + return False + if entities[0].entity_type.lower() != 'task': + return False + return True + + def launch(self, session, entities, event): + task = entities[0] + task_name = task['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + description = '/'.join(desc_items) + + self.clockapi.start_time_entry( + description=description, + project_name=project_name, + task_name=task_name, + ) + + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + StartClockify(session).register() + + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) From f051e61a0b305dc361917a1a49c4a7f439325b29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:17:53 +0100 Subject: [PATCH 07/53] changed clockify API when start timer when already is one running finish him --- pype/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 160afe914b..fce1691d43 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -200,7 +200,7 @@ class ClockifyAPI(metaclass=Singleton): ): self.bool_timer_run = True return self.bool_timer_run - return False + self.finish_time_entry(workspace_id=workspace_id) # Convert billable to strings if billable: From 40c2ccd9d789f1869e3f622eb082efd5940fe832 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:52:33 +0100 Subject: [PATCH 08/53] stop timer works now --- pype/clockify/clockify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 1541807a46..8710bf56eb 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -35,6 +35,9 @@ class ClockifyModule: self.set_menu_visibility() + def stop_timer(self): + self.clockapi.finish_time_entry() + # Definition of Tray menu def tray_menu(self, parent): # Menu for Tray App @@ -54,7 +57,7 @@ class ClockifyModule: self.menu.addAction(self.aStopTimer) self.aShowSettings.triggered.connect(self.show_settings) - self.aStopTimer.triggered.connect(self.clockapi.finish_time_entry) + self.aStopTimer.triggered.connect(self.stop_timer) self.set_menu_visibility() From 463c574e883712a3b1aa91b90c89ae3096b8d53c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:53:02 +0100 Subject: [PATCH 09/53] added thread that check each 5 seconds if any timer is running --- pype/clockify/clockify.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 8710bf56eb..1a67eeba92 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,4 +1,5 @@ import os +import threading from app import style from app.vendor.Qt import QtWidgets from pype.clockify import ClockifySettings, ClockifyAPI @@ -12,7 +13,9 @@ class ClockifyModule: self.clockapi = ClockifyAPI() self.widget_settings = ClockifySettings(main_parent, self) + self.thread_timer_check = None # Bools + self.bool_thread_check_running = False self.bool_api_key_set = False self.bool_workspace_set = False self.bool_timer_run = False @@ -24,17 +27,39 @@ class ClockifyModule: return workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) - print(workspace) self.bool_workspace_set = self.clockapi.set_workspace(workspace) if self.bool_workspace_set is False: # TODO show message to user print("Nope Workspace: clockify.py - line 29") return - if self.clockapi.get_in_progress() is not None: - self.bool_timer_run = True + + self.bool_thread_check_running = True + self.start_timer_check() self.set_menu_visibility() + def change_timer_run(self, bool_run): + self.bool_timer_run = bool_run + self.set_menu_visibility() + + def start_timer_check(self): + if self.thread_timer_check is None: + self.thread_timer_check = threading.Thread( + target=self.check_running + ) + self.thread_timer_check.daemon = True + self.thread_timer_check.start() + + def check_running(self): + import time + while self.bool_thread_check_running is True: + if self.clockapi.get_in_progress() is not None: + self.bool_timer_run = True + else: + self.bool_timer_run = False + self.set_menu_visibility() + time.sleep(5) + def stop_timer(self): self.clockapi.finish_time_entry() From 607e7c78b5efbc640790331158608d959038a46a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:50:52 +0100 Subject: [PATCH 10/53] renamed start_clockify to clockify_start --- .../{action_start_clockify.py => action_clockify_start.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pype/ftrack/actions/{action_start_clockify.py => action_clockify_start.py} (98%) diff --git a/pype/ftrack/actions/action_start_clockify.py b/pype/ftrack/actions/action_clockify_start.py similarity index 98% rename from pype/ftrack/actions/action_start_clockify.py rename to pype/ftrack/actions/action_clockify_start.py index 2a980de6c0..7fabf1d0b7 100644 --- a/pype/ftrack/actions/action_start_clockify.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -11,7 +11,7 @@ class StartClockify(BaseAction): '''Starts timer on clockify.''' #: Action identifier. - identifier = 'start.clockify.timer' + identifier = 'clockify.start.timer' #: Action label. label = 'Start timer' #: Action description. From 67241f9df0057f511865a88a426ae3c53d12c862 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:51:33 +0100 Subject: [PATCH 11/53] added add_task and delete_project methods to clockify api --- pype/clockify/clockify_api.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index fce1691d43..5ef976fcbe 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -317,6 +317,41 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() + def add_task( + self, name, project_name=None, project_id=None, + workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = 'workspaces/{}/projects/{}/tasks/'.format( + workspace_id, project_id + ) + body = { + "name": name, + "projectId": project_id + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def delete_project( + self, project_name=None, project_id=None, + workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = '/workspaces/{}/projects/{}'.format( + workspace_id, project_id + ) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers, + ) + return response.json() + def convert_input( self, entity_id, entity_name, mode='Workspace', project_id=None ): From 73c3501928810b3ccf4a0e28626b7a818b064667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:52:19 +0100 Subject: [PATCH 12/53] modified values in add_project so they dont include any None values (raises error) --- pype/clockify/clockify_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 5ef976fcbe..5bcb05c4e4 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -296,9 +296,12 @@ class ClockifyAPI(metaclass=Singleton): "name": name, "clientId": "", "isPublic": "false", - "estimate": None, - "color": None, - "billable": None + "estimate": { + # "estimate": "3600", + "type": "AUTO" + }, + "color": "#f44336", + "billable": "true" } response = requests.post( self.endpoint + action_url, From 16ca88c86a1277ad6261a969ecf2b6a4c8aa8453 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:53:50 +0100 Subject: [PATCH 13/53] added basic clockify sync - Ftrack action - validates if user can create project, then creates project and task types --- pype/ftrack/actions/action_clockify_sync.py | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 pype/ftrack/actions/action_clockify_sync.py diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py new file mode 100644 index 0000000000..afb3173e4a --- /dev/null +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -0,0 +1,156 @@ +import sys +import argparse +import logging +import json +import ftrack_api +from pype.ftrack import BaseAction +from pype.clockify import ClockifyAPI + + +class SyncClocify(BaseAction): + '''Synchronise project names and task types.''' + + #: Action identifier. + identifier = 'clockify.sync' + #: Action label. + label = 'Sync To Clockify' + #: Action description. + description = 'Synchronise data to Clockify workspace' + #: priority + priority = 100 + #: roles that are allowed to register this action + role_list = ['Pypecub', 'Administrator'] + #: icon + icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' + #: CLockifyApi + clockapi = ClockifyAPI() + + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def validate_auth_rights(self): + test_project = '__test__' + try: + self.clockapi.add_project(test_project) + except Exception: + return False + self.clockapi.delete_project(test_project) + return True + + def launch(self, session, entities, event): + authorization = self.validate_auth_rights() + if authorization is False: + return { + 'success': False, + 'message': ( + 'You don\'t have permission to modify Clockify' + ' workspace {}'.format(self.clockapi.workspace) + ) + } + + # JOB SETTINGS + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + + job = session.create('Job', { + 'user': user, + 'status': 'running', + 'data': json.dumps({ + 'description': 'Sync Ftrack to Clockify' + }) + }) + session.commit() + try: + projects_info = {} + for project in session.query('Project').all(): + task_types = [] + for task_type in project['project_schema']['_task_type_schema'][ + 'types' + ]: + task_types.append(task_type['name']) + projects_info[project['full_name']] = task_types + + clockify_projects = self.clockapi.get_projects() + for project_name, task_types in projects_info.items(): + if project_name not in clockify_projects: + response = self.clockapi.add_project(project_name) + if 'id' not in response: + self.log.error('Project {} can\'t be created'.format( + project_name + )) + continue + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks( + project_id=project_id + ) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id=project_id + ) + if 'id' not in response: + self.log.error('Task {} can\'t be created'.format( + task_type + )) + continue + except Exception: + job['status'] = 'failed' + session.commit() + return False + + job['status'] = 'done' + session.commit() + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + SyncClocify(session).register() + + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) From 65ee3a82177beecae3a0b0590c1bd7143ca57413 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 18:21:17 +0100 Subject: [PATCH 14/53] added interface, fixed role list, autorization check before register --- pype/ftrack/actions/action_clockify_sync.py | 84 +++++++++++++++++---- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index afb3173e4a..f3b1a49329 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -3,7 +3,7 @@ import argparse import logging import json import ftrack_api -from pype.ftrack import BaseAction +from pype.ftrack import BaseAction, MissingPermision from pype.clockify import ClockifyAPI @@ -19,16 +19,16 @@ class SyncClocify(BaseAction): #: priority priority = 100 #: roles that are allowed to register this action - role_list = ['Pypecub', 'Administrator'] + role_list = ['Pypeclub', 'Administrator'] #: icon icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' #: CLockifyApi clockapi = ClockifyAPI() - def discover(self, session, entities, event): - ''' Validation ''' - - return True + def register(self): + if self.validate_auth_rights() is False: + raise MissingPermision + super().register() def validate_auth_rights(self): test_project = '__test__' @@ -39,16 +39,57 @@ class SyncClocify(BaseAction): self.clockapi.delete_project(test_project) return True - def launch(self, session, entities, event): - authorization = self.validate_auth_rights() - if authorization is False: - return { - 'success': False, - 'message': ( - 'You don\'t have permission to modify Clockify' - ' workspace {}'.format(self.clockapi.workspace) - ) + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def interface(self, session, entities, event): + if not event['data'].get('values', {}): + title = 'Select projects to sync' + + projects = session.query('Project').all() + + items = [] + all_projects_label = { + 'type': 'label', + 'value': 'All projects' } + all_projects_value = { + 'name': '__all__', + 'type': 'boolean', + 'value': False + } + line = { + 'type': 'label', + 'value': '___' + } + items.append(all_projects_label) + items.append(all_projects_value) + items.append(line) + for project in projects: + label = project['full_name'] + item_label = { + 'type': 'label', + 'value': label + } + item_value = { + 'name': project['id'], + 'type': 'boolean', + 'value': False + } + items.append(item_label) + items.append(item_value) + + return { + 'items': items, + 'title': title + } + + def launch(self, session, entities, event): + values = event['data'].get('values', {}) + if not values: + return # JOB SETTINGS userId = event['source']['user']['id'] @@ -63,8 +104,19 @@ class SyncClocify(BaseAction): }) session.commit() try: + if values.get('__all__', False) is True: + projects_to_sync = session.query('Project').all() + else: + projects_to_sync = [] + project_query = 'Project where id is "{}"' + for project_id, sync in values.items(): + if sync is True: + projects_to_sync.append(session.query( + project_query.format(project_id) + ).one()) + projects_info = {} - for project in session.query('Project').all(): + for project in projects_to_sync: task_types = [] for task_type in project['project_schema']['_task_type_schema'][ 'types' From 1df4f0c148f1b99d459c4e0abbc257c20e8d3e94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Mar 2019 15:41:39 +0100 Subject: [PATCH 15/53] permission validation moved to clockify_api will be used in multiple actions --- pype/clockify/clockify_api.py | 17 +++++++++++++++++ pype/ftrack/actions/action_clockify_sync.py | 12 +----------- .../{StartClockify.py => ClockifyStart.py} | 12 ++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) rename pype/plugins/launcher/actions/{StartClockify.py => ClockifyStart.py} (80%) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 5bcb05c4e4..b250377756 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -43,6 +43,23 @@ class ClockifyAPI(metaclass=Singleton): return False return True + def validate_workspace_perm(self): + test_project = '__test__' + action_url = 'workspaces/{}/projects/'.format(self.workspace_id) + body = { + "name": test_project, "clientId": "", "isPublic": "false", + "estimate": {"type": "AUTO"}, + "color": "#f44336", "billable": "true" + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, json=body + ) + if response.status_code >= 300: + return False + self.delete_project(test_project) + return True + def set_workspace(self, name=None): if name is None: self.workspace = None diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index f3b1a49329..84e47005f5 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -26,22 +26,12 @@ class SyncClocify(BaseAction): clockapi = ClockifyAPI() def register(self): - if self.validate_auth_rights() is False: + if self.clockapi.validate_workspace_perm() is False: raise MissingPermision super().register() - def validate_auth_rights(self): - test_project = '__test__' - try: - self.clockapi.add_project(test_project) - except Exception: - return False - self.clockapi.delete_project(test_project) - return True - def discover(self, session, entities, event): ''' Validation ''' - return True def interface(self, session, entities, event): diff --git a/pype/plugins/launcher/actions/StartClockify.py b/pype/plugins/launcher/actions/ClockifyStart.py similarity index 80% rename from pype/plugins/launcher/actions/StartClockify.py rename to pype/plugins/launcher/actions/ClockifyStart.py index 91adfe0185..e1f17f2aa3 100644 --- a/pype/plugins/launcher/actions/StartClockify.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -1,15 +1,16 @@ from avalon import api, io from pype.clockify import ClockifyAPI from pype.api import Logger -log = Logger.getLogger(__name__, "start_clockify") +log = Logger.getLogger(__name__, "clockify_start") -class StartClockify(api.Action): +class ClockifyStart(api.Action): - name = "start_clockify_timer" - label = "Start Timer - Clockify" + name = "clockify_start_timer" + label = "Clockify - Start Timer" icon = "clockify_icon" order = 500 + clockapi = ClockifyAPI() def is_compatible(self, session): """Return whether the action is compatible with the session""" @@ -18,7 +19,6 @@ class StartClockify(api.Action): return False def process(self, session, **kwargs): - clockapi = ClockifyAPI() project_name = session['AVALON_PROJECT'] asset_name = session['AVALON_ASSET'] task_name = session['AVALON_TASK'] @@ -33,7 +33,7 @@ class StartClockify(api.Action): desc_items.append(asset_name) description = '/'.join(desc_items) - clockapi.start_time_entry( + self.clockapi.start_time_entry( description=description, project_name=project_name, task_name=task_name, From a88c14b7233e08ab26d7cbd48fc1e823e9d9588e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Mar 2019 15:53:33 +0100 Subject: [PATCH 16/53] added action to launcher that syncs to clockify --- pype/plugins/launcher/actions/ClockifySync.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 pype/plugins/launcher/actions/ClockifySync.py diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py new file mode 100644 index 0000000000..31ae1f3424 --- /dev/null +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -0,0 +1,61 @@ +from avalon import api, io +from pype.clockify import ClockifyAPI +from pype.api import Logger +log = Logger.getLogger(__name__, "clockify_sync") + + +class ClockifySync(api.Action): + + name = "sync_to_clockify" + label = "Sync to Clockify" + icon = "clockify_icon" + order = 500 + clockapi = ClockifyAPI() + have_permissions = clockapi.validate_workspace_perm() + + def is_compatible(self, session): + """Return whether the action is compatible with the session""" + return self.have_permissions + + def process(self, session, **kwargs): + project_name = session.get('AVALON_PROJECT', None) + + projects_to_sync = [] + if project_name.strip() == '' or project_name is None: + for project in io.projects(): + projects_to_sync.append(project) + else: + project = io.find_one({'type': 'project'}) + projects_to_sync.append(project) + + projects_info = {} + for project in projects_to_sync: + task_types = [task['name'] for task in project['config']['tasks']] + projects_info[project['name']] = task_types + + clockify_projects = self.clockapi.get_projects() + for project_name, task_types in projects_info.items(): + if project_name not in clockify_projects: + response = self.clockapi.add_project(project_name) + if 'id' not in response: + self.log.error('Project {} can\'t be created'.format( + project_name + )) + continue + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks( + project_id=project_id + ) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id=project_id + ) + if 'id' not in response: + self.log.error('Task {} can\'t be created'.format( + task_type + )) + continue From 3fb4f30e9b944a0e6ffdaf0b10e8a89a5d907134 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:43:00 +0100 Subject: [PATCH 17/53] added clockify check to app launch --- pype/ftrack/lib/ftrack_app_handler.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index fd5b758f22..7d773136e7 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -146,6 +146,25 @@ class AppAction(BaseHandler): entity = entities[0] project_name = entity['project']['full_name'] + # Validate Clockify settings if Clockify is required + clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) + if clockify_timer is not None: + from pype.clockify import ClockifyAPI + clockapi = ClockifyAPI() + if clockapi.verify_api() is False: + title = 'Launch message' + header = '# You Can\'t launch **any Application**' + message = ( + '

You don\'t have set Clockify API' + ' key in Clockify settings

' + ) + items = [ + {'type': 'label', 'value': header}, + {'type': 'label', 'value': message} + ] + self.show_interface(event, items, title) + return False + database = pypelib.get_avalon_database() # Get current environments @@ -293,6 +312,29 @@ class AppAction(BaseHandler): self.log.info('Starting timer for task: ' + task['name']) user.start_timer(task, force=True) + # RUN TIMER IN Clockify + if clockify_timer is not None: + task_name = task['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + description = '/'.join(desc_items) + + project_id = clockapi.get_project_id(project_name) + task_id = clockapi.get_task_id(task_name, project_id) + clockapi.start_time_entry( + description, project_id, task_id, + ) + # Change status of task to In progress config = get_config_data() From 2360b444041d2caf9cf22f4bb6f027ecce5d4ee8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:48:47 +0100 Subject: [PATCH 18/53] ftrack base handler can handle if items are result --- pype/ftrack/lib/ftrack_base_handler.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 9d94d50072..91fee3d2fc 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -289,13 +289,15 @@ class BaseHandler(object): } elif isinstance(result, dict): - for key in ('success', 'message'): - if key in result: - continue + items = 'items' in result + if items is False: + for key in ('success', 'message'): + if key in result: + continue - raise KeyError( - 'Missing required key: {0}.'.format(key) - ) + raise KeyError( + 'Missing required key: {0}.'.format(key) + ) else: self.log.error( From 1909d61f09046d9a711663014df0e66562a1239f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:52:45 +0100 Subject: [PATCH 19/53] removed _name attributes from clockify api functions --- pype/clockify/clockify_api.py | 108 ++++++++---------- pype/ftrack/actions/action_clockify_start.py | 7 +- pype/ftrack/actions/action_clockify_sync.py | 6 +- .../plugins/launcher/actions/ClockifyStart.py | 6 +- pype/plugins/launcher/actions/ClockifySync.py | 4 +- 5 files changed, 60 insertions(+), 71 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index b250377756..06acfa7bc5 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -109,8 +109,9 @@ class ClockifyAPI(metaclass=Singleton): workspace["name"]: workspace["id"] for workspace in response.json() } - def get_projects(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_projects(self, workspace_id): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -121,10 +122,9 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } - def get_tags( - self, workspace_name=None, workspace_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_tags(self, workspace_id): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -135,13 +135,9 @@ class ClockifyAPI(metaclass=Singleton): tag["name"]: tag["id"] for tag in response.json() } - def get_tasks( - self, - workspace_name=None, project_name=None, - workspace_id=None, project_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + def get_tasks(self, project_id, workspace_id=None): + if workspace_id is None: + workspace_id = self.add_workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) @@ -160,31 +156,29 @@ class ClockifyAPI(metaclass=Singleton): return None return all_workspaces[workspace_name] - def get_project_id( - self, project_name, workspace_name=None, workspace_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) - all_projects = self.get_projects(workspace_id=workspace_id) + def get_project_id(self, project_name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + all_projects = self.get_projects(workspace_id) if project_name not in all_projects: return None return all_projects[project_name] - def get_tag_id(self, tag_name, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) - all_tasks = self.get_tags(workspace_id=workspace_id) + def get_tag_id(self, tag_name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + all_tasks = self.get_tags(workspace_id) if tag_name not in all_tasks: return None return all_tasks[tag_name] def get_task_id( - self, task_name, - project_name=None, workspace_name=None, - project_id=None, workspace_id=None + self, task_name, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id all_tasks = self.get_tasks( - workspace_id=workspace_id, project_id=project_id + project_id, workspace_id ) if task_name not in all_tasks: return None @@ -194,21 +188,16 @@ class ClockifyAPI(metaclass=Singleton): return str(datetime.datetime.utcnow().isoformat())+'Z' def start_time_entry( - self, description, project_name=None, task_name=None, - billable=True, workspace_name=None, - project_id=None, task_id=None, workspace_id=None + self, description, project_id, task_id, + workspace_id=None, billable=True ): # Workspace - workspace_id = self.convert_input(workspace_id, workspace_name) - # Project - project_id = self.convert_input( - project_id, project_name, 'Project' - ) - # Task - task_id = self.convert_input(task_id, task_name, 'Task', project_id) + if workspace_id is None: + workspace_id = self.workspace_id + print(workspace_id) # Check if is currently run another times and has same values - current = self.get_in_progress(workspace_id=workspace_id) + current = self.get_in_progress(workspace_id) if current is not None: if ( current.get("description", None) == description and @@ -217,7 +206,7 @@ class ClockifyAPI(metaclass=Singleton): ): self.bool_timer_run = True return self.bool_timer_run - self.finish_time_entry(workspace_id=workspace_id) + self.finish_time_entry(workspace_id) # Convert billable to strings if billable: @@ -246,8 +235,9 @@ class ClockifyAPI(metaclass=Singleton): success = True return success - def get_in_progress(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_in_progress(self, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/inProgress'.format( workspace_id ) @@ -261,9 +251,10 @@ class ClockifyAPI(metaclass=Singleton): output = None return output - def finish_time_entry(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) - current = self.get_in_progress(workspace_id=workspace_id) + def finish_time_entry(self, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + current = self.get_in_progress(workspace_id) current_id = current["id"] action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, current_id @@ -285,9 +276,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def get_time_entries( - self, quantity=10, workspace_name=None, workspace_id=None + self, workspace_id=None, quantity=10 ): - workspace_id = self.convert_input(workspace_id, workspace_name) + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -295,8 +287,9 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json()[:quantity] - def remove_time_entry(self, tid, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def remove_time_entry(self, tid, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, tid ) @@ -306,8 +299,9 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() - def add_project(self, name, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def add_project(self, name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) body = { "name": name, @@ -338,11 +332,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def add_task( - self, name, project_name=None, project_id=None, - workspace_name=None, workspace_id=None + self, name, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) @@ -358,11 +351,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def delete_project( - self, project_name=None, project_id=None, - workspace_name=None, workspace_id=None + self, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id action_url = '/workspaces/{}/projects/{}'.format( workspace_id, project_id ) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 7fabf1d0b7..d0f8d96c49 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -44,11 +44,10 @@ class StartClockify(BaseAction): desc_items = get_parents(task['parent']) description = '/'.join(desc_items) - + project_id = self.clockapi.get_project_id(project_name) + task_id = self.clockapi.get_task_id(task_name, project_id) self.clockapi.start_time_entry( - description=description, - project_name=project_name, - task_name=task_name, + description, project_id, task_id ) return True diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 84e47005f5..335521c5cb 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -127,13 +127,11 @@ class SyncClocify(BaseAction): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks( - project_id=project_id - ) + clockify_project_tasks = self.clockapi.get_tasks(project_id) for task_type in task_types: if task_type not in clockify_project_tasks: response = self.clockapi.add_task( - task_type, project_id=project_id + task_type, project_id ) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index e1f17f2aa3..c300df3389 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -33,8 +33,8 @@ class ClockifyStart(api.Action): desc_items.append(asset_name) description = '/'.join(desc_items) + project_id = self.clockapi.get_project_id(project_name) + task_id = self.clockapi.get_task_id(task_name, project_id) self.clockapi.start_time_entry( - description=description, - project_name=project_name, - task_name=task_name, + description, project_id, task_id ) diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index 31ae1f3424..2c3f61b681 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -47,12 +47,12 @@ class ClockifySync(api.Action): project_id = clockify_projects[project_name] clockify_project_tasks = self.clockapi.get_tasks( - project_id=project_id + project_id ) for task_type in task_types: if task_type not in clockify_project_tasks: response = self.clockapi.add_task( - task_type, project_id=project_id + task_type, project_id ) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( From 4be701605b05855b39c660d0f2c72d3ec51b05a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:53:57 +0100 Subject: [PATCH 20/53] cosmetic changes in code --- pype/clockify/widget_settings.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index 8b8abb25da..409c7893e4 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -120,13 +120,12 @@ class ClockifySettings(QtWidgets.QWidget): def click_ok(self): api_key = self.input_api_key.text().strip() - if self.optional: - if api_key == '': - self.clockapi.save_api_key(None) - self.clockapi.set_api(api_key) - self.validated = False - self._close_widget() - return + if self.optional is True and api_key == '': + self.clockapi.save_api_key(None) + self.clockapi.set_api(api_key) + self.validated = False + self._close_widget() + return validation = self.clockapi.validate_api_key(api_key) From ded5e0df862a411e6e53a07f18a2d1537ff8ad6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:54:27 +0100 Subject: [PATCH 21/53] setting widget will close in not optional (not used yet) --- pype/clockify/widget_settings.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index 409c7893e4..02fd4350e6 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -142,8 +142,14 @@ class ClockifySettings(QtWidgets.QWidget): ) def closeEvent(self, event): - event.ignore() - self._close_widget() + if self.optional is True: + event.ignore() + self._close_widget() + else: + self.validated = False def _close_widget(self): - self.hide() + if self.optional is True: + self.hide() + else: + self.close() From 9573669a91b8b6fceffea98eaee7881ad98abdb9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:55:29 +0100 Subject: [PATCH 22/53] verify api added so actions can handle with unlogged users --- pype/clockify/clockify_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 06acfa7bc5..c8cae86a66 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -23,6 +23,12 @@ class ClockifyAPI(metaclass=Singleton): file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + def verify_api(self): + for key, value in self.headers.items(): + if value is None or value.strip() == '': + return False + return True + def set_api(self, api_key=None): if api_key is None: api_key = self.get_api_key() From 7dd5c8a8eec4e0a7f83b28e27573b1c398e16f1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:56:24 +0100 Subject: [PATCH 23/53] workspace is set when user is logged in --- pype/clockify/clockify_api.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index c8cae86a66..f82cc0265d 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -22,6 +22,7 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + workspace_id = None def verify_api(self): for key, value in self.headers.items(): @@ -35,6 +36,7 @@ class ClockifyAPI(metaclass=Singleton): if api_key is not None and self.validate_api_key(api_key) is True: self.headers["X-Api-Key"] = api_key + self.set_workspace() return True return False @@ -68,20 +70,23 @@ class ClockifyAPI(metaclass=Singleton): def set_workspace(self, name=None): if name is None: - self.workspace = None - self.workspace_id = None + name = os.environ.get('CLOCKIFY_WORKSPACE', None) + self.workspace = name + self.workspace_id = None + if self.workspace is None: return - result = self.validate_workspace(name) - if result is False: - self.workspace = None - self.workspace_id = None - return False - else: - self.workspace = name + try: + result = self.validate_workspace() + except Exception: + result = False + if result is not False: self.workspace_id = result return True + return False - def validate_workspace(self, name): + def validate_workspace(self, name=None): + if name is None: + name = self.workspace all_workspaces = self.get_workspaces() if name in all_workspaces: return all_workspaces[name] From 695ca47ccde3030e65a3d6313a9cae314f5ceb79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:57:07 +0100 Subject: [PATCH 24/53] clockify api(singleton) can have master object --- pype/clockify/clockify_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index f82cc0265d..ab9d1e6f67 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -22,8 +22,12 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + master_parent = None workspace_id = None + def set_master(self, master_parent): + self.master_parent = master_parent + def verify_api(self): for key, value in self.headers.items(): if value is None or value.strip() == '': From 7797b7732bf9b1c1b253b3f0f33443b69c80bcd3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:58:05 +0100 Subject: [PATCH 25/53] clockify is master of clockify api --- pype/clockify/clockify.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 1a67eeba92..2b5e54b0c5 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -21,16 +21,14 @@ class ClockifyModule: self.bool_timer_run = False def start_up(self): + self.clockapi.set_master(self) self.bool_api_key_set = self.clockapi.set_api() if self.bool_api_key_set is False: self.show_settings() return - workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) - self.bool_workspace_set = self.clockapi.set_workspace(workspace) + self.bool_workspace_set = self.clockapi.workspace_id is not None if self.bool_workspace_set is False: - # TODO show message to user - print("Nope Workspace: clockify.py - line 29") return self.bool_thread_check_running = True From 8e079571eb1c274e23afa7bd498f549365732a9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:02:09 +0100 Subject: [PATCH 26/53] added clockify availability check to ftrack actions --- pype/ftrack/actions/action_clockify_start.py | 2 ++ pype/ftrack/actions/action_clockify_sync.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index d0f8d96c49..49501b1183 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -26,6 +26,8 @@ class StartClockify(BaseAction): return False if entities[0].entity_type.lower() != 'task': return False + if self.clockapi.workspace_id is None: + return False return True def launch(self, session, entities, event): diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 335521c5cb..3b975679ec 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -26,6 +26,9 @@ class SyncClocify(BaseAction): clockapi = ClockifyAPI() def register(self): + if self.clockapi.workspace_id is None: + raise ValueError('Clockify Workspace or API key are not set!') + if self.clockapi.validate_workspace_perm() is False: raise MissingPermision super().register() From 849137e5b1cab4c3bfee71a571e54ff8c170f50d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:02 +0100 Subject: [PATCH 27/53] it is possible to not enter workspace id for get projects --- pype/clockify/clockify_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index ab9d1e6f67..450c99cdea 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -124,7 +124,7 @@ class ClockifyAPI(metaclass=Singleton): workspace["name"]: workspace["id"] for workspace in response.json() } - def get_projects(self, workspace_id): + def get_projects(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) @@ -209,7 +209,6 @@ class ClockifyAPI(metaclass=Singleton): # Workspace if workspace_id is None: workspace_id = self.workspace_id - print(workspace_id) # Check if is currently run another times and has same values current = self.get_in_progress(workspace_id) From 1d5290f4d521262553ee945ef5f410a08514018f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:21 +0100 Subject: [PATCH 28/53] admin permissions are better checked (hope) --- pype/clockify/clockify_api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 450c99cdea..55e3eb4882 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -67,10 +67,18 @@ class ClockifyAPI(metaclass=Singleton): self.endpoint + action_url, headers=self.headers, json=body ) - if response.status_code >= 300: - return False - self.delete_project(test_project) - return True + if response.status_code == 201: + self.delete_project(self.get_project_id(test_project)) + return True + else: + projects = self.get_projects() + if test_project in projects: + try: + self.delete_project(self.get_project_id(test_project)) + return True + except json.decoder.JSONDecodeError: + return False + return False def set_workspace(self, name=None): if name is None: From b5a13468c10702bd98252708f142cbdf663a05fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:46 +0100 Subject: [PATCH 29/53] start timer thread if workspace_id is set --- pype/clockify/clockify_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 55e3eb4882..efbd79dac2 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -93,6 +93,8 @@ class ClockifyAPI(metaclass=Singleton): result = False if result is not False: self.workspace_id = result + if self.master_parent is not None: + self.master_parent.start_timer_check() return True return False From 41c31e01e9df9160561c04ff55e343f220b8fc00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:54:13 +0100 Subject: [PATCH 30/53] missing permission error is more specific now --- pype/ftrack/lib/ftrack_base_handler.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 91fee3d2fc..146259b5e8 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -5,8 +5,10 @@ from pype import api as pype class MissingPermision(Exception): - def __init__(self): - super().__init__('Missing permission') + def __init__(self, message=None): + if message is None: + message = 'Ftrack' + super().__init__(message) class BaseHandler(object): @@ -64,10 +66,10 @@ class BaseHandler(object): self.log.info(( '{} "{}" - Registered successfully ({:.4f}sec)' ).format(self.type, label, run_time)) - except MissingPermision: + except MissingPermision as MPE: self.log.info(( - '!{} "{}" - You\'re missing required permissions' - ).format(self.type, label)) + '!{} "{}" - You\'re missing required {} permissions' + ).format(self.type, label, str(MPE))) except NotImplementedError: self.log.error(( '{} "{}" - Register method is not implemented' From 4db427dcaf5c95e48c2f8d0bda22861eee9ea470 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:54:46 +0100 Subject: [PATCH 31/53] Missing permission in clockify is more specific --- pype/ftrack/actions/action_clockify_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 3b975679ec..de91e68f50 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -30,7 +30,7 @@ class SyncClocify(BaseAction): raise ValueError('Clockify Workspace or API key are not set!') if self.clockapi.validate_workspace_perm() is False: - raise MissingPermision + raise MissingPermision('Clockify') super().register() def discover(self, session, entities, event): From 82a945aac4234f170b6ce06731dee220e545beff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:55:13 +0100 Subject: [PATCH 32/53] clockify module again check for running timer --- pype/clockify/clockify.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 2b5e54b0c5..a22933f700 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,4 +1,3 @@ -import os import threading from app import style from app.vendor.Qt import QtWidgets @@ -12,6 +11,7 @@ class ClockifyModule: self.parent = parent self.clockapi = ClockifyAPI() self.widget_settings = ClockifySettings(main_parent, self) + self.widget_settings_required = None self.thread_timer_check = None # Bools @@ -31,16 +31,12 @@ class ClockifyModule: if self.bool_workspace_set is False: return - self.bool_thread_check_running = True self.start_timer_check() self.set_menu_visibility() - def change_timer_run(self, bool_run): - self.bool_timer_run = bool_run - self.set_menu_visibility() - def start_timer_check(self): + self.bool_thread_check_running = True if self.thread_timer_check is None: self.thread_timer_check = threading.Thread( target=self.check_running @@ -48,6 +44,12 @@ class ClockifyModule: self.thread_timer_check.daemon = True self.thread_timer_check.start() + def stop_timer_check(self): + self.bool_thread_check_running = True + if self.thread_timer_check is not None: + self.thread_timer_check.join() + self.thread_timer_check = None + def check_running(self): import time while self.bool_thread_check_running is True: @@ -60,6 +62,7 @@ class ClockifyModule: def stop_timer(self): self.clockapi.finish_time_entry() + self.bool_timer_run = False # Definition of Tray menu def tray_menu(self, parent): From 5529c531cf09de85b2c5e38b0323d76a66113739 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 18:04:15 +0100 Subject: [PATCH 33/53] clockify sync has white icon in avalon --- pype/plugins/launcher/actions/ClockifySync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index 2c3f61b681..c7459542aa 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -8,7 +8,7 @@ class ClockifySync(api.Action): name = "sync_to_clockify" label = "Sync to Clockify" - icon = "clockify_icon" + icon = "clockify_white_icon" order = 500 clockapi = ClockifyAPI() have_permissions = clockapi.validate_workspace_perm() From 292936c8c1f255df83249130eef66febb4842ad8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 19:33:20 +0100 Subject: [PATCH 34/53] clockify is not using task name but task-type name --- pype/ftrack/actions/action_clockify_start.py | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 49501b1183..d36ab795a9 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -32,7 +32,7 @@ class StartClockify(BaseAction): def launch(self, session, entities, event): task = entities[0] - task_name = task['name'] + task_name = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 7d773136e7..01eca55b5b 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -314,7 +314,7 @@ class AppAction(BaseHandler): # RUN TIMER IN Clockify if clockify_timer is not None: - task_name = task['name'] + task_name = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): From 91e929033d106a3938c1b9bfdf3627cd49408bf0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:26:55 +0100 Subject: [PATCH 35/53] fixed get_tasks bug --- pype/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index efbd79dac2..d6897ec299 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -162,7 +162,7 @@ class ClockifyAPI(metaclass=Singleton): def get_tasks(self, project_id, workspace_id=None): if workspace_id is None: - workspace_id = self.add_workspace_id + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) From bc8efd319d7b12542235a3aebc4b5bea154c349f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:31:38 +0100 Subject: [PATCH 36/53] only actual project is synced --- pype/ftrack/actions/action_clockify_sync.py | 119 ++++++-------------- 1 file changed, 35 insertions(+), 84 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index de91e68f50..406d383b6d 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -37,53 +37,7 @@ class SyncClocify(BaseAction): ''' Validation ''' return True - def interface(self, session, entities, event): - if not event['data'].get('values', {}): - title = 'Select projects to sync' - - projects = session.query('Project').all() - - items = [] - all_projects_label = { - 'type': 'label', - 'value': 'All projects' - } - all_projects_value = { - 'name': '__all__', - 'type': 'boolean', - 'value': False - } - line = { - 'type': 'label', - 'value': '___' - } - items.append(all_projects_label) - items.append(all_projects_value) - items.append(line) - for project in projects: - label = project['full_name'] - item_label = { - 'type': 'label', - 'value': label - } - item_value = { - 'name': project['id'], - 'type': 'boolean', - 'value': False - } - items.append(item_label) - items.append(item_value) - - return { - 'items': items, - 'title': title - } - def launch(self, session, entities, event): - values = event['data'].get('values', {}) - if not values: - return - # JOB SETTINGS userId = event['source']['user']['id'] user = session.query('User where id is ' + userId).one() @@ -97,50 +51,47 @@ class SyncClocify(BaseAction): }) session.commit() try: - if values.get('__all__', False) is True: - projects_to_sync = session.query('Project').all() - else: - projects_to_sync = [] - project_query = 'Project where id is "{}"' - for project_id, sync in values.items(): - if sync is True: - projects_to_sync.append(session.query( - project_query.format(project_id) - ).one()) + entity = entities[0] - projects_info = {} - for project in projects_to_sync: - task_types = [] - for task_type in project['project_schema']['_task_type_schema'][ - 'types' - ]: - task_types.append(task_type['name']) - projects_info[project['full_name']] = task_types + if entity.entity_type.lower() == 'project': + project = entity + else: + project = entity['project'] + project_name = project['full_name'] + + task_types = [] + for task_type in project['project_schema']['_task_type_schema'][ + 'types' + ]: + task_types.append(task_type['name']) clockify_projects = self.clockapi.get_projects() - for project_name, task_types in projects_info.items(): - if project_name not in clockify_projects: - response = self.clockapi.add_project(project_name) + + if project_name not in clockify_projects: + response = self.clockapi.add_project(project_name) + if 'id' not in response: + self.log.error('Project {} can\'t be created'.format( + project_name + )) + return { + 'success': False, + 'message': 'Can\'t create project, unexpected error' + } + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks(project_id) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id + ) if 'id' not in response: - self.log.error('Project {} can\'t be created'.format( - project_name + self.log.error('Task {} can\'t be created'.format( + task_type )) continue - project_id = response['id'] - else: - project_id = clockify_projects[project_name] - - clockify_project_tasks = self.clockapi.get_tasks(project_id) - for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) - if 'id' not in response: - self.log.error('Task {} can\'t be created'.format( - task_type - )) - continue except Exception: job['status'] = 'failed' session.commit() From e0f6ba6eb2accfd874f6514fe409c73fd700a5c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:41:04 +0100 Subject: [PATCH 37/53] task name is added to timer description --- pype/ftrack/actions/action_clockify_start.py | 1 + pype/ftrack/lib/ftrack_app_handler.py | 1 + pype/plugins/launcher/actions/ClockifyStart.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index d36ab795a9..76becb8a1b 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -45,6 +45,7 @@ class StartClockify(BaseAction): return output desc_items = get_parents(task['parent']) + desc_items.append(task['name']) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) task_id = self.clockapi.get_task_id(task_name, project_id) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 01eca55b5b..2553cb2976 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -327,6 +327,7 @@ class AppAction(BaseHandler): return output desc_items = get_parents(task['parent']) + desc_items.append(task['name']) description = '/'.join(desc_items) project_id = clockapi.get_project_id(project_name) diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index c300df3389..ca6d6e1852 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -31,6 +31,7 @@ class ClockifyStart(api.Action): if asset is not None: desc_items = asset.get('data', {}).get('parents', []) desc_items.append(asset_name) + desc_items.append(task_name) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) From bc80648db708d33ccfe11a59ed49f1de3b6b1216 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:51:00 +0100 Subject: [PATCH 38/53] get_tags fixed workspace_id, start timer can have tags, added add_tag method --- pype/clockify/clockify_api.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index d6897ec299..f5ebac0cef 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -147,7 +147,7 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } - def get_tags(self, workspace_id): + def get_tags(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) @@ -213,7 +213,7 @@ class ClockifyAPI(metaclass=Singleton): return str(datetime.datetime.utcnow().isoformat())+'Z' def start_time_entry( - self, description, project_id, task_id, + self, description, project_id, task_id=None, tag_ids=[], workspace_id=None, billable=True ): # Workspace @@ -246,7 +246,7 @@ class ClockifyAPI(metaclass=Singleton): "description": description, "projectId": project_id, "taskId": task_id, - "tagIds": None + "tagIds": tag_ids } response = requests.post( self.endpoint + action_url, @@ -374,6 +374,20 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() + def add_tag(self, name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + action_url = 'workspaces/{}/tags'.format(workspace_id) + body = { + "name": name + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + def delete_project( self, project_id, workspace_id=None ): From ef91b6e53deb8e59c9e48ad768ebac5d11a73195 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:51:36 +0100 Subject: [PATCH 39/53] task types are stored as tags instead of tasks during sync to clockify --- pype/ftrack/actions/action_clockify_sync.py | 8 +++----- pype/plugins/launcher/actions/ClockifySync.py | 10 +++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 406d383b6d..202bb7b912 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -81,12 +81,10 @@ class SyncClocify(BaseAction): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks(project_id) + clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) + if task_type not in clockify_workspace_tags: + response = self.clockapi.add_tag(task_type) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( task_type diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index c7459542aa..d8c69bc768 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -46,14 +46,10 @@ class ClockifySync(api.Action): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks( - project_id - ) + clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) + if task_type not in clockify_workspace_tags: + response = self.clockapi.add_tag(task_type) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( task_type From 7d3a0fe82b0a34ee0751ec05d5af0228b598e924 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:52:07 +0100 Subject: [PATCH 40/53] start timer set tag_ids instead of task_id --- pype/ftrack/actions/action_clockify_start.py | 5 +++-- pype/ftrack/lib/ftrack_app_handler.py | 9 +++++---- pype/plugins/launcher/actions/ClockifyStart.py | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 76becb8a1b..b1c60a2525 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -48,9 +48,10 @@ class StartClockify(BaseAction): desc_items.append(task['name']) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) - task_id = self.clockapi.get_task_id(task_name, project_id) + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_name)) self.clockapi.start_time_entry( - description, project_id, task_id + description, project_id, tag_ids=tag_ids ) return True diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 2553cb2976..48531cf014 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -314,7 +314,7 @@ class AppAction(BaseHandler): # RUN TIMER IN Clockify if clockify_timer is not None: - task_name = task['type']['name'] + task_type = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): @@ -331,9 +331,10 @@ class AppAction(BaseHandler): description = '/'.join(desc_items) project_id = clockapi.get_project_id(project_name) - task_id = clockapi.get_task_id(task_name, project_id) - clockapi.start_time_entry( - description, project_id, task_id, + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_type)) + self.clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids ) # Change status of task to In progress diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index ca6d6e1852..d0d1bb48f3 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -35,7 +35,8 @@ class ClockifyStart(api.Action): description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) - task_id = self.clockapi.get_task_id(task_name, project_id) + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_name)) self.clockapi.start_time_entry( - description, project_id, task_id + description, project_id, tag_ids=tag_ids ) From b10ac0b279f0c4c5afcc6e7ed57a2e1961ad6dc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 16 Mar 2019 09:50:54 +0100 Subject: [PATCH 41/53] fixed bug in start clockify app launch --- pype/ftrack/lib/ftrack_app_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 48531cf014..bd216ff6bf 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -332,8 +332,8 @@ class AppAction(BaseHandler): project_id = clockapi.get_project_id(project_name) tag_ids = [] - tag_ids.append(self.clockapi.get_tag_id(task_type)) - self.clockapi.start_time_entry( + tag_ids.append(clockapi.get_tag_id(task_type)) + clockapi.start_time_entry( description, project_id, tag_ids=tag_ids ) From 9554b76ed4772bdd8c5ef41c6b85fbb2b3194b26 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 15:06:48 +0100 Subject: [PATCH 42/53] hotfix/zero out pivot on loaded models --- pype/plugins/maya/load/load_alembic.py | 3 +++ pype/plugins/maya/load/load_model.py | 11 +++++++++-- pype/plugins/maya/load/load_rig.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 9e08702521..11ed0c4f64 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -16,6 +16,7 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, namespace=namespace, @@ -25,6 +26,8 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index f29af65b72..9e46e16e92 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -20,12 +20,16 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): from avalon import maya with maya.maintained_selection(): + + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, reference=True, returnNewNodes=True, groupReference=True, - groupName="{}:{}".format(namespace, name)) + groupName=groupName) + + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) self[:] = nodes @@ -141,15 +145,18 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, groupReference=True, - groupName="{}:{}".format(namespace, name), + groupName=groupName, reference=True, returnNewNodes=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index aa40ca3cc2..5a90783548 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -21,12 +21,15 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, data): + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, reference=True, returnNewNodes=True, groupReference=True, - groupName="{}:{}".format(namespace, name)) + groupName=groupName) + + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) # Store for post-process self[:] = nodes From 1bf91ce6b2b743df735d27239053f1ef4e749dba Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 15:46:55 +0100 Subject: [PATCH 43/53] hotfix/collect arnold attributes in may --- pype/plugins/maya/publish/collect_look.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index 8befbfeee0..dfefa15fe5 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -47,6 +47,8 @@ def get_look_attrs(node): for attr in attrs: if attr in SHAPE_ATTRS: result.append(attr) + elif attr.startswith('ai'): + result.append(attr) return result @@ -387,6 +389,8 @@ class CollectLook(pyblish.api.InstancePlugin): # Collect changes to "custom" attributes node_attrs = get_look_attrs(node) + self.log.info(node_attrs) + # Only include if there are any properties we care about if not node_attrs: continue From 4f45e9e2a7070fa936e8fe5a7178768c7486f62a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 16:29:08 +0100 Subject: [PATCH 44/53] hotfix/check if transfers data exists in frames integrator before assigning it. --- pype/plugins/global/publish/integrate_rendered_frames.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index ae11d33348..1f6dc63d35 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -168,6 +168,9 @@ class IntegrateFrames(pyblish.api.InstancePlugin): representations = [] destination_list = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + for files in instance.data["files"]: # Collection # _______ From b6e22e7c477a37c2febcf495f9a51df27186b567 Mon Sep 17 00:00:00 2001 From: antirotor Date: Sat, 23 Mar 2019 23:42:49 +0100 Subject: [PATCH 45/53] feat(maya): colorize outliner based on families --- pype/plugins/maya/load/load_alembic.py | 19 ++++++- pype/plugins/maya/load/load_ass.py | 32 +++++++++++- pype/plugins/maya/load/load_camera.py | 17 +++++++ pype/plugins/maya/load/load_fbx.py | 17 +++++++ pype/plugins/maya/load/load_mayaascii.py | 16 ++++++ pype/plugins/maya/load/load_model.py | 50 ++++++++++++++++++- pype/plugins/maya/load/load_rig.py | 19 ++++++- .../plugins/maya/load/load_vdb_to_redshift.py | 15 ++++++ pype/plugins/maya/load/load_vdb_to_vray.py | 15 ++++++ pype/plugins/maya/load/load_yeti_cache.py | 13 +++++ pype/plugins/maya/load/load_yeti_rig.py | 16 ++++++ 11 files changed, 223 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 11ed0c4f64..0869ff7f2f 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class AbcLoader(pype.maya.plugin.ReferenceLoader): @@ -26,7 +28,22 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('pointcache') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index 13ad85473c..e96d404379 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -2,6 +2,7 @@ from avalon import api import pype.maya.plugin import os import pymel.core as pm +import json class AssProxyLoader(pype.maya.plugin.ReferenceLoader): @@ -34,7 +35,8 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) # Set attributes proxyShape = pm.ls(nodes, type="mesh")[0] @@ -43,6 +45,19 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): proxyShape.dso.set(path) proxyShape.aiOverrideShaders.set(0) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('ass') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes @@ -132,7 +147,6 @@ class AssStandinLoader(api.Loader): import mtoa.ui.arnoldmenu import pymel.core as pm - asset = context['asset']['name'] namespace = namespace or lib.unique_namespace( asset + "_", @@ -146,6 +160,20 @@ class AssStandinLoader(api.Loader): label = "{}:{}".format(namespace, name) root = pm.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('ass') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) + # Create transform with shape transform_name = label + "_ASS" # transform = pm.createNode("transform", name=transform_name, diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index eb75c3a63d..0483d3ac76 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class CameraLoader(pype.maya.plugin.ReferenceLoader): @@ -17,6 +19,7 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): # Get family type from the context cmds.loadPlugin("AbcImport.mll", quiet=True) + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, @@ -27,6 +30,20 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): cameras = cmds.ls(nodes, type="camera") + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('camera') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) + # Check the Maya version, lockTransform has been introduced since # Maya 2016.5 Ext 2 version = int(cmds.about(version=True)) diff --git a/pype/plugins/maya/load/load_fbx.py b/pype/plugins/maya/load/load_fbx.py index 2ee3e5fdbd..f86b9a8dfa 100644 --- a/pype/plugins/maya/load/load_fbx.py +++ b/pype/plugins/maya/load/load_fbx.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class FBXLoader(pype.maya.plugin.ReferenceLoader): @@ -28,6 +30,21 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName="{}:{}".format(namespace, name)) + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('fbx') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 6f4c6a63a0..7521040c15 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import json +import os class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): @@ -28,6 +30,20 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): groupName="{}:{}".format(namespace, name)) self[:] = nodes + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('mayaAscii') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) return nodes diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 9e46e16e92..3eaed71e3f 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -1,5 +1,7 @@ from avalon import api import pype.maya.plugin +import json +import os class ModelLoader(pype.maya.plugin.ReferenceLoader): @@ -19,6 +21,14 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + with maya.maintained_selection(): groupName = "{}:{}".format(namespace, name) @@ -29,7 +39,14 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + c = colors.get('model') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes @@ -68,6 +85,19 @@ class GpuCacheLoader(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('model') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create transform with shape transform_name = label + "_GPU" @@ -129,6 +159,7 @@ class GpuCacheLoader(api.Loader): except RuntimeError: pass + class AbcModelLoader(pype.maya.plugin.ReferenceLoader): """Specific loader of Alembic for the studio.animation family""" @@ -155,7 +186,22 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('model') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index 5a90783548..d66a8f9007 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -2,6 +2,8 @@ from maya import cmds import pype.maya.plugin from avalon import api, maya +import os +import json class RigLoader(pype.maya.plugin.ReferenceLoader): @@ -29,7 +31,22 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('rig') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) # Store for post-process self[:] = nodes diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py index 8ff8bc0326..c4023e2618 100644 --- a/pype/plugins/maya/load/load_vdb_to_redshift.py +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -1,4 +1,6 @@ from avalon import api +import os +import json class LoadVDBtoRedShift(api.Loader): @@ -48,6 +50,19 @@ class LoadVDBtoRedShift(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('vdbcache') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create VR volume_node = cmds.createNode("RedshiftVolumeShape", diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py index ac20b0eb43..0abf1bd952 100644 --- a/pype/plugins/maya/load/load_vdb_to_vray.py +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -1,4 +1,6 @@ from avalon import api +import json +import os class LoadVDBtoVRay(api.Loader): @@ -40,6 +42,19 @@ class LoadVDBtoVRay(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('vdbcache') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create VR grid_node = cmds.createNode("VRayVolumeGrid", diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 2160924047..908687a5c5 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -49,6 +49,19 @@ class YetiCacheLoader(api.Loader): group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('yeticache') + if c is not None: + cmds.setAttr(group_name + ".useOutlinerColor", 1) + cmds.setAttr(group_name + ".outlinerColor", + c[0], c[1], c[2]) nodes.append(group_node) diff --git a/pype/plugins/maya/load/load_yeti_rig.py b/pype/plugins/maya/load/load_yeti_rig.py index 096b936b41..c821c6ca02 100644 --- a/pype/plugins/maya/load/load_yeti_rig.py +++ b/pype/plugins/maya/load/load_yeti_rig.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class YetiRigLoader(pype.maya.plugin.ReferenceLoader): @@ -24,6 +26,20 @@ class YetiRigLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName="{}:{}".format(namespace, name)) + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('yetiRig') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes self.log.info("Yeti Rig Connection Manager will be available soon") From 3077785c7e295a9ef52f7b32719122743f039b74 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 28 Mar 2019 11:48:17 +0100 Subject: [PATCH 46/53] hotfix. assumed destination not working for unknown reason :) --- .../global/publish/collect_assumed_destination.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 96f7e4b585..7de358b422 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -8,14 +8,10 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): """Generate the assumed destination path where the file will be stored""" label = "Collect Assumed Destination" - order = pyblish.api.CollectorOrder + 0.499 + order = pyblish.api.CollectorOrder + 0.498 exclude_families = ["clip"] def process(self, instance): - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: - return - """Create a destination filepath based on the current data available Example template: @@ -27,6 +23,9 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): Returns: file path (str) """ + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return # get all the stuff from the database subset_name = instance.data["subset"] From b19f703189571fcf3346c03f90fd6eacb3bf4491 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 28 Mar 2019 14:03:55 +0100 Subject: [PATCH 47/53] change ;thumbnail update logic on ftrack event --- pype/ftrack/events/event_thumbnail_updates.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index a825088e60..50089e26b8 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -23,8 +23,12 @@ class ThumbnailEvents(BaseEvent): parent['name'], task['name'])) # Update task thumbnail from published version - if (entity['entityType'] == 'assetversion' and - entity['action'] == 'encoded'): + # if (entity['entityType'] == 'assetversion' and + # entity['action'] == 'encoded'): + if ( + entity['entityType'] == 'assetversion' + and 'thumbid' in entity['keys'] + ): version = session.get('AssetVersion', entity['entityId']) thumbnail = version.get('thumbnail') @@ -40,6 +44,7 @@ class ThumbnailEvents(BaseEvent): pass + def register(session, **kw): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): From a8d4409ce888ab3bc8145f580cb58336dac5a167 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:27:31 +0200 Subject: [PATCH 48/53] fix(pype): converting temlates.py into module so self. could be holding singleton data --- pype/templates.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pype/templates.py b/pype/templates.py index c5578a983c..92a0e2c3c7 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -1,5 +1,6 @@ import os import re +import sys from avalon import io from avalon import api as avalon from . import lib @@ -7,12 +8,14 @@ from app.api import (Templates, Logger, format) log = Logger.getLogger(__name__, os.getenv("AVALON_APP", "pype-config")) -SESSION = None + +self = sys.modules[__name__] +self.SESSION = None def set_session(): lib.set_io_database() - SESSION = avalon.session + self.SESSION = avalon.session def load_data_from_templates(): @@ -104,9 +107,9 @@ def set_project_code(code): os.environ[KEY]: project code avalon.sesion[KEY]: project code """ - if SESSION is None: + if self.SESSION is None: set_session() - SESSION["AVALON_PROJECTCODE"] = code + self.SESSION["AVALON_PROJECTCODE"] = code os.environ["AVALON_PROJECTCODE"] = code @@ -118,9 +121,9 @@ def get_project_name(): string: project name """ - if SESSION is None: + if self.SESSION is None: set_session() - project_name = SESSION.get("AVALON_PROJECT", None) \ + project_name = self.SESSION.get("AVALON_PROJECT", None) \ or os.getenv("AVALON_PROJECT", None) assert project_name, log.error("missing `AVALON_PROJECT`" "in avalon session " @@ -138,9 +141,9 @@ def get_asset(): Raises: log: error """ - if SESSION is None: + if self.SESSION is None: set_session() - asset = SESSION.get("AVALON_ASSET", None) \ + asset = self.SESSION.get("AVALON_ASSET", None) \ or os.getenv("AVALON_ASSET", None) log.info("asset: {}".format(asset)) assert asset, log.error("missing `AVALON_ASSET`" @@ -159,9 +162,9 @@ def get_task(): Raises: log: error """ - if SESSION is None: + if self.SESSION is None: set_session() - task = SESSION.get("AVALON_TASK", None) \ + task = self.SESSION.get("AVALON_TASK", None) \ or os.getenv("AVALON_TASK", None) assert task, log.error("missing `AVALON_TASK`" "in avalon session " @@ -196,9 +199,9 @@ def set_hierarchy(hierarchy): Args: hierarchy (string): hierarchy path ("silo/folder/seq") """ - if SESSION is None: + if self.SESSION is None: set_session() - SESSION["AVALON_HIERARCHY"] = hierarchy + self.SESSION["AVALON_HIERARCHY"] = hierarchy os.environ["AVALON_HIERARCHY"] = hierarchy @@ -248,10 +251,10 @@ def set_avalon_workdir(project=None, avalon.session[AVALON_WORKDIR]: workdir path """ - if SESSION is None: + if self.SESSION is None: set_session() - awd = SESSION.get("AVALON_WORKDIR", None) \ - or os.getenv("AVALON_WORKDIR", None) + + awd = self.SESSION.get("AVALON_WORKDIR", None) or os.getenv("AVALON_WORKDIR", None) data = get_context_data(project, hierarchy, asset, task) if (not awd) or ("{" not in awd): @@ -259,7 +262,7 @@ def set_avalon_workdir(project=None, awd_filled = os.path.normpath(format(awd, data)) - SESSION["AVALON_WORKDIR"] = awd_filled + self.SESSION["AVALON_WORKDIR"] = awd_filled os.environ["AVALON_WORKDIR"] = awd_filled log.info("`AVALON_WORKDIR` fixed to: {}".format(awd_filled)) From 4e99de691c6faa0a2cef4fde6250d02ad85a0926 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:28:59 +0200 Subject: [PATCH 49/53] fix(nuke): fixing path formating to replace "\" in path to "/" --- pype/plugins/nuke/load/load_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index a4a591e657..b80d1a0ca1 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -101,7 +101,7 @@ class LoadSequence(api.Loader): if namespace is None: namespace = context['asset']['name'] - file = self.fname + file = self.fname.replace("\\", "/") log.info("file: {}\n".format(self.fname)) read_name = "Read_" + context["representation"]["context"]["subset"] @@ -112,7 +112,7 @@ class LoadSequence(api.Loader): r = nuke.createNode( "Read", "name {}".format(read_name)) - r["file"].setValue(self.fname) + r["file"].setValue(file) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace", None) From 8cd1bfa55fa3f12c1c3a500f3bee8a9ecf22a7fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:29:52 +0200 Subject: [PATCH 50/53] fix(pype): reading padding info for image sequence path from anatomy instead hardcoding it to ##### --- pype/plugins/global/publish/integrate_rendered_frames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 1f6dc63d35..8e7e2a59c4 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -243,7 +243,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) - template_data["frame"] = "#####" + template_data["frame"] = "#" * anatomy.render.padding anatomy_filled = anatomy.format(template_data) path_to_save = anatomy_filled.render.path template = anatomy.render.fullpath From 0c9186dc6551a8011385365f00a0ef48d578e3fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:31:24 +0200 Subject: [PATCH 51/53] fix(pype): fixing `host` attribute to `hosts`, it was not filtering plugins out of context --- pype/plugins/global/publish/extract_jpeg.py | 3 ++- pype/plugins/global/publish/extract_quicktime.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index a99e6bc787..7720c9d56d 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -16,9 +16,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): """ label = "Extract Jpeg EXR" + hosts = ["shell"] order = pyblish.api.ExtractorOrder families = ["imagesequence", "render", "write", "source"] - host = ["shell"] + def process(self, instance): start = instance.data.get("startFrame") diff --git a/pype/plugins/global/publish/extract_quicktime.py b/pype/plugins/global/publish/extract_quicktime.py index a226bf7e2a..621078e3c0 100644 --- a/pype/plugins/global/publish/extract_quicktime.py +++ b/pype/plugins/global/publish/extract_quicktime.py @@ -18,7 +18,7 @@ class ExtractQuicktimeEXR(pyblish.api.InstancePlugin): label = "Extract Quicktime EXR" order = pyblish.api.ExtractorOrder families = ["imagesequence", "render", "write", "source"] - host = ["shell"] + hosts = ["shell"] def process(self, instance): fps = instance.data.get("fps") From 90b3a1f78365f3db6876d9ed1b099fa50104d57f Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 3 Apr 2019 12:50:58 +0200 Subject: [PATCH 52/53] fix(maya): outliner colorize now respects families in context --- pype/plugins/maya/load/load_alembic.py | 7 +++++- pype/plugins/maya/load/load_ass.py | 7 +++++- pype/plugins/maya/load/load_camera.py | 7 +++++- pype/plugins/maya/load/load_fbx.py | 7 +++++- pype/plugins/maya/load/load_mayaascii.py | 7 +++++- pype/plugins/maya/load/load_model.py | 6 ++++- pype/plugins/maya/load/load_rig.py | 7 +++++- .../plugins/maya/load/load_vdb_to_redshift.py | 7 +++++- pype/plugins/maya/load/load_vdb_to_vray.py | 7 +++++- pype/plugins/maya/load/load_vrayproxy.py | 24 +++++++++++++++++-- pype/plugins/maya/load/load_yeti_cache.py | 7 +++++- 11 files changed, 81 insertions(+), 12 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 0869ff7f2f..9fd4aa2108 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -18,6 +18,11 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "animation" + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, @@ -39,7 +44,7 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('pointcache') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index e96d404379..c268ce70c5 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -22,6 +22,11 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): from avalon import maya import pymel.core as pm + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "ass" + with maya.maintained_selection(): groupName = "{}:{}".format(namespace, name) @@ -53,7 +58,7 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('ass') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index 0483d3ac76..989e80e979 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -18,6 +18,11 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds # Get family type from the context + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "camera" + cmds.loadPlugin("AbcImport.mll", quiet=True) groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, @@ -38,7 +43,7 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('camera') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_fbx.py b/pype/plugins/maya/load/load_fbx.py index f86b9a8dfa..b580257334 100644 --- a/pype/plugins/maya/load/load_fbx.py +++ b/pype/plugins/maya/load/load_fbx.py @@ -19,6 +19,11 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "fbx" + # Ensure FBX plug-in is loaded cmds.loadPlugin("fbxmaya", quiet=True) @@ -39,7 +44,7 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('fbx') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 7521040c15..549d1dff4c 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -21,6 +21,11 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" + with maya.maintained_selection(): nodes = cmds.file(self.fname, namespace=namespace, @@ -39,7 +44,7 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('mayaAscii') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 3eaed71e3f..2e76ce8e28 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -21,6 +21,10 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" preset_file = os.path.join( os.environ.get('PYPE_STUDIO_TEMPLATES'), 'presets', 'tools', @@ -42,7 +46,7 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - c = colors.get('model') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index d66a8f9007..1dcff45bb9 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -23,6 +23,11 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, data): + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "rig" + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, @@ -42,7 +47,7 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('rig') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py index c4023e2618..169c3bf34a 100644 --- a/pype/plugins/maya/load/load_vdb_to_redshift.py +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -19,6 +19,11 @@ class LoadVDBtoRedShift(api.Loader): import avalon.maya.lib as lib from avalon.maya.pipeline import containerise + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vdbcache" + # Check if the plugin for redshift is available on the pc try: cmds.loadPlugin("redshift4maya", quiet=True) @@ -58,7 +63,7 @@ class LoadVDBtoRedShift(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('vdbcache') + c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py index 0abf1bd952..58d6d1b56e 100644 --- a/pype/plugins/maya/load/load_vdb_to_vray.py +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -18,6 +18,11 @@ class LoadVDBtoVRay(api.Loader): import avalon.maya.lib as lib from avalon.maya.pipeline import containerise + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vdbcache" + # Check if viewport drawing engine is Open GL Core (compat) render_engine = None compatible = "OpenGLCoreProfileCompat" @@ -50,7 +55,7 @@ class LoadVDBtoVRay(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('vdbcache') + c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vrayproxy.py b/pype/plugins/maya/load/load_vrayproxy.py index 9396e124ce..a3a114440a 100644 --- a/pype/plugins/maya/load/load_vrayproxy.py +++ b/pype/plugins/maya/load/load_vrayproxy.py @@ -1,6 +1,7 @@ from avalon.maya import lib from avalon import api - +import json +import os import maya.cmds as cmds @@ -20,6 +21,19 @@ class VRayProxyLoader(api.Loader): from avalon.maya.pipeline import containerise from pype.maya.lib import namespaced + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vrayproxy" + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + asset_name = context['asset']["name"] namespace = namespace or lib.unique_namespace( asset_name + "_", @@ -40,6 +54,12 @@ class VRayProxyLoader(api.Loader): if not nodes: return + c = colors.get(family) + if c is not None: + cmds.setAttr("{0}_{1}.useOutlinerColor".format(name, "GRP"), 1) + cmds.setAttr("{0}_{1}.outlinerColor".format(name, "GRP"), + c[0], c[1], c[2]) + return containerise( name=name, namespace=namespace, @@ -101,7 +121,7 @@ class VRayProxyLoader(api.Loader): # Create nodes vray_mesh = cmds.createNode('VRayMesh', name="{}_VRMS".format(name)) mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) - vray_mat = cmds.shadingNode("VRayMeshMaterial", asShader=True, + vray_mat = cmds.shadingNode("VRayMeshMaterial", asShader=True, name="{}_VRMM".format(name)) vray_mat_sg = cmds.sets(name="{}_VRSG".format(name), empty=True, diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 908687a5c5..b19bed1393 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -23,6 +23,11 @@ class YetiCacheLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "yeticache" + # Build namespace asset = context["asset"] if namespace is None: @@ -57,7 +62,7 @@ class YetiCacheLoader(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('yeticache') + c = colors.get(family) if c is not None: cmds.setAttr(group_name + ".useOutlinerColor", 1) cmds.setAttr(group_name + ".outlinerColor", From 366aec21a2964e2fd05426a876144e1d27b5856b Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Apr 2019 14:20:08 +0200 Subject: [PATCH 53/53] add double check on the created namespace. this fixes problem with asseblies not loading becasue of the wron namespace --- pype/plugins/maya/load/load_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 2e76ce8e28..16f3556de7 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -190,6 +190,9 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) + namespace = cmds.referenceQuery(nodes[0], namespace=True) + groupName = "{}:{}".format(namespace, name) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True)