diff --git a/openpype/modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py
index 6af911fffc..80979c83ab 100644
--- a/openpype/modules/clockify/clockify_api.py
+++ b/openpype/modules/clockify/clockify_api.py
@@ -6,34 +6,22 @@ import datetime
import requests
from .constants import (
CLOCKIFY_ENDPOINT,
- ADMIN_PERMISSION_NAMES
+ ADMIN_PERMISSION_NAMES,
)
from openpype.lib.local_settings import OpenPypeSecureRegistry
-
-
-def time_check(obj):
- if obj.request_counter < 10:
- obj.request_counter += 1
- return
-
- wait_time = 1 - (time.time() - obj.request_time)
- if wait_time > 0:
- time.sleep(wait_time)
-
- obj.request_time = time.time()
- obj.request_counter = 0
+from openpype.lib import Logger
class ClockifyAPI:
+ log = Logger.get_logger(__name__)
+
def __init__(self, api_key=None, master_parent=None):
self.workspace_name = None
- self.workspace_id = None
self.master_parent = master_parent
self.api_key = api_key
- self.request_counter = 0
- self.request_time = time.time()
-
+ self._workspace_id = None
+ self._user_id = None
self._secure_registry = None
@property
@@ -44,11 +32,19 @@ class ClockifyAPI:
@property
def headers(self):
- return {"X-Api-Key": self.api_key}
+ return {"x-api-key": self.api_key}
+
+ @property
+ def workspace_id(self):
+ return self._workspace_id
+
+ @property
+ def user_id(self):
+ return self._user_id
def verify_api(self):
for key, value in self.headers.items():
- if value is None or value.strip() == '':
+ if value is None or value.strip() == "":
return False
return True
@@ -59,65 +55,55 @@ class ClockifyAPI:
if api_key is not None and self.validate_api_key(api_key) is True:
self.api_key = api_key
self.set_workspace()
+ self.set_user_id()
if self.master_parent:
self.master_parent.signed_in()
return True
return False
def validate_api_key(self, api_key):
- test_headers = {'X-Api-Key': api_key}
- action_url = 'workspaces/'
- time_check(self)
+ test_headers = {"x-api-key": api_key}
+ action_url = "user"
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=test_headers
+ CLOCKIFY_ENDPOINT + action_url, headers=test_headers
)
if response.status_code != 200:
return False
return True
- def validate_workspace_perm(self, workspace_id=None):
- user_id = self.get_user_id()
+ def validate_workspace_permissions(self, workspace_id=None, user_id=None):
if user_id is None:
+ self.log.info("No user_id found during validation")
return False
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = "/workspaces/{}/users/{}/permissions".format(
- workspace_id, user_id
- )
- time_check(self)
+ action_url = f"workspaces/{workspace_id}/users?includeRoles=1"
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
- user_permissions = response.json()
- for perm in user_permissions:
- if perm['name'] in ADMIN_PERMISSION_NAMES:
+ data = response.json()
+ for user in data:
+ if user.get("id") == user_id:
+ roles_data = user.get("roles")
+ for entities in roles_data:
+ if entities.get("role") in ADMIN_PERMISSION_NAMES:
return True
return False
def get_user_id(self):
- action_url = 'v1/user/'
- time_check(self)
+ action_url = "user"
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
- # this regex is neccessary: UNICODE strings are crashing
- # during json serialization
- id_regex = '\"{1}id\"{1}\:{1}\"{1}\w+\"{1}'
- result = re.findall(id_regex, str(response.content))
- if len(result) != 1:
- # replace with log and better message?
- print('User ID was not found (this is a BUG!!!)')
- return None
- return json.loads('{'+result[0]+'}')['id']
+ result = response.json()
+ user_id = result.get("id", None)
+
+ return user_id
def set_workspace(self, name=None):
if name is None:
- name = os.environ.get('CLOCKIFY_WORKSPACE', None)
+ name = os.environ.get("CLOCKIFY_WORKSPACE", None)
self.workspace_name = name
- self.workspace_id = None
if self.workspace_name is None:
return
try:
@@ -125,7 +111,7 @@ class ClockifyAPI:
except Exception:
result = False
if result is not False:
- self.workspace_id = result
+ self._workspace_id = result
if self.master_parent is not None:
self.master_parent.start_timer_check()
return True
@@ -139,6 +125,14 @@ class ClockifyAPI:
return all_workspaces[name]
return False
+ def set_user_id(self):
+ try:
+ user_id = self.get_user_id()
+ except Exception:
+ user_id = None
+ if user_id is not None:
+ self._user_id = user_id
+
def get_api_key(self):
return self.secure_registry.get_item("api_key", None)
@@ -146,11 +140,9 @@ class ClockifyAPI:
self.secure_registry.set_item("api_key", api_key)
def get_workspaces(self):
- action_url = 'workspaces/'
- time_check(self)
+ action_url = "workspaces/"
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return {
workspace["name"]: workspace["id"] for workspace in response.json()
@@ -159,27 +151,22 @@ class ClockifyAPI:
def get_projects(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/projects/'.format(workspace_id)
- time_check(self)
+ action_url = f"workspaces/{workspace_id}/projects"
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
-
- return {
- project["name"]: project["id"] for project in response.json()
- }
+ if response.status_code != 403:
+ result = response.json()
+ return {project["name"]: project["id"] for project in result}
def get_project_by_id(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/projects/{}/'.format(
+ action_url = "workspaces/{}/projects/{}".format(
workspace_id, project_id
)
- time_check(self)
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()
@@ -187,32 +174,24 @@ class ClockifyAPI:
def get_tags(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/tags/'.format(workspace_id)
- time_check(self)
+ action_url = "workspaces/{}/tags".format(workspace_id)
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
- return {
- tag["name"]: tag["id"] for tag in response.json()
- }
+ return {tag["name"]: tag["id"] for tag in response.json()}
def get_tasks(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/projects/{}/tasks/'.format(
+ action_url = "workspaces/{}/projects/{}/tasks".format(
workspace_id, project_id
)
- time_check(self)
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
- return {
- task["name"]: task["id"] for task in response.json()
- }
+ return {task["name"]: task["id"] for task in response.json()}
def get_workspace_id(self, workspace_name):
all_workspaces = self.get_workspaces()
@@ -236,48 +215,64 @@ class ClockifyAPI:
return None
return all_tasks[tag_name]
- def get_task_id(
- self, task_name, project_id, workspace_id=None
- ):
+ def get_task_id(self, task_name, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- all_tasks = self.get_tasks(
- project_id, workspace_id
- )
+ all_tasks = self.get_tasks(project_id, workspace_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'
+ return str(datetime.datetime.utcnow().isoformat()) + "Z"
def start_time_entry(
- self, description, project_id, task_id=None, tag_ids=[],
- workspace_id=None, billable=True
+ self,
+ description,
+ project_id,
+ task_id=None,
+ tag_ids=None,
+ workspace_id=None,
+ user_id=None,
+ billable=True,
):
# Workspace
if workspace_id is None:
workspace_id = self.workspace_id
+ # User ID
+ if user_id is None:
+ user_id = self._user_id
+
+ # get running timer to check if we need to start it
+ current_timer = self.get_in_progress()
# Check if is currently run another times and has same values
- current = self.get_in_progress(workspace_id)
- if current is not None:
+ # DO not restart the timer, if it is already running for curent task
+ if current_timer:
+ current_timer_hierarchy = current_timer.get("description")
+ current_project_id = current_timer.get("projectId")
+ current_task_id = current_timer.get("taskId")
if (
- current.get("description", None) == description and
- current.get("projectId", None) == project_id and
- current.get("taskId", None) == task_id
+ description == current_timer_hierarchy
+ and project_id == current_project_id
+ and task_id == current_task_id
):
+ self.log.info(
+ "Timer for the current project is already running"
+ )
self.bool_timer_run = True
return self.bool_timer_run
- self.finish_time_entry(workspace_id)
+ self.finish_time_entry()
# Convert billable to strings
if billable:
- billable = 'true'
+ billable = "true"
else:
- billable = 'false'
+ billable = "false"
# Rest API Action
- action_url = 'workspaces/{}/timeEntries/'.format(workspace_id)
+ action_url = "workspaces/{}/user/{}/time-entries".format(
+ workspace_id, user_id
+ )
start = self.get_current_time()
body = {
"start": start,
@@ -285,169 +280,135 @@ class ClockifyAPI:
"description": description,
"projectId": project_id,
"taskId": task_id,
- "tagIds": tag_ids
+ "tagIds": tag_ids,
}
- time_check(self)
response = requests.post(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
-
- success = False
if response.status_code < 300:
- success = True
- return success
+ return True
+ return False
- 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
- )
- time_check(self)
- response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
- )
+ def _get_current_timer_values(self, response):
+ if response is None:
+ return
try:
output = response.json()
except json.decoder.JSONDecodeError:
- output = None
- return output
+ return None
+ if output and isinstance(output, list):
+ return output[0]
+ return None
- def finish_time_entry(self, workspace_id=None):
+ def get_in_progress(self, user_id=None, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- current = self.get_in_progress(workspace_id)
- if current is None:
- return
+ if user_id is None:
+ user_id = self.user_id
- current_id = current["id"]
- action_url = 'workspaces/{}/timeEntries/{}'.format(
- workspace_id, current_id
+ action_url = (
+ f"workspaces/{workspace_id}/user/"
+ f"{user_id}/time-entries?in-progress=1"
)
- 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()
- }
- time_check(self)
- response = requests.put(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ response = requests.get(
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
+ )
+ return self._get_current_timer_values(response)
+
+ def finish_time_entry(self, workspace_id=None, user_id=None):
+ if workspace_id is None:
+ workspace_id = self.workspace_id
+ if user_id is None:
+ user_id = self.user_id
+ current_timer = self.get_in_progress()
+ if not current_timer:
+ return
+ action_url = "workspaces/{}/user/{}/time-entries".format(
+ workspace_id, user_id
+ )
+ body = {"end": self.get_current_time()}
+ response = requests.patch(
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
- def get_time_entries(
- self, workspace_id=None, quantity=10
- ):
+ def get_time_entries(self, workspace_id=None, user_id=None, quantity=10):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/timeEntries/'.format(workspace_id)
- time_check(self)
+ if user_id is None:
+ user_id = self.user_id
+ action_url = "workspaces/{}/user/{}/time-entries".format(
+ workspace_id, user_id
+ )
response = requests.get(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()[:quantity]
- def remove_time_entry(self, tid, workspace_id=None):
+ def remove_time_entry(self, tid, workspace_id=None, user_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/timeEntries/{}'.format(
- workspace_id, tid
+ action_url = "workspaces/{}/user/{}/time-entries/{}".format(
+ workspace_id, user_id, tid
)
- time_check(self)
response = requests.delete(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()
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)
+ action_url = "workspaces/{}/projects".format(workspace_id)
body = {
"name": name,
"clientId": "",
"isPublic": "false",
- "estimate": {
- "estimate": 0,
- "type": "AUTO"
- },
+ "estimate": {"estimate": 0, "type": "AUTO"},
"color": "#f44336",
- "billable": "true"
+ "billable": "true",
}
- time_check(self)
response = requests.post(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def add_workspace(self, name):
- action_url = 'workspaces/'
+ action_url = "workspaces/"
body = {"name": name}
- time_check(self)
response = requests.post(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
- def add_task(
- self, name, project_id, workspace_id=None
- ):
+ def add_task(self, name, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = 'workspaces/{}/projects/{}/tasks/'.format(
+ action_url = "workspaces/{}/projects/{}/tasks".format(
workspace_id, project_id
)
- body = {
- "name": name,
- "projectId": project_id
- }
- time_check(self)
+ body = {"name": name, "projectId": project_id}
response = requests.post(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
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
- }
- time_check(self)
+ action_url = "workspaces/{}/tags".format(workspace_id)
+ body = {"name": name}
response = requests.post(
- CLOCKIFY_ENDPOINT + action_url,
- headers=self.headers,
- json=body
+ CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
- def delete_project(
- self, project_id, workspace_id=None
- ):
+ def delete_project(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
- action_url = '/workspaces/{}/projects/{}'.format(
+ action_url = "/workspaces/{}/projects/{}".format(
workspace_id, project_id
)
- time_check(self)
response = requests.delete(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
@@ -455,12 +416,12 @@ class ClockifyAPI:
return response.json()
def convert_input(
- self, entity_id, entity_name, mode='Workspace', project_id=None
+ 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 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
@@ -471,14 +432,14 @@ class ClockifyAPI:
else:
if entity_id is None and entity_name is None:
error = True
- elif mode.lower() == 'project':
+ elif mode.lower() == "project":
entity_id = self.get_project_id(entity_name)
- elif mode.lower() == 'task':
+ elif mode.lower() == "task":
entity_id = self.get_task_id(
task_name=entity_name, project_id=project_id
)
else:
- raise TypeError('Unknown type')
+ raise TypeError("Unknown type")
# Raise error
if error:
raise ValueError(error_msg.format(mode))
diff --git a/openpype/modules/clockify/clockify_module.py b/openpype/modules/clockify/clockify_module.py
index 300d5576e2..200a268ad7 100644
--- a/openpype/modules/clockify/clockify_module.py
+++ b/openpype/modules/clockify/clockify_module.py
@@ -2,24 +2,13 @@ import os
import threading
import time
-from openpype.modules import (
- OpenPypeModule,
- ITrayModule,
- IPluginPaths
-)
+from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths
+from openpype.client import get_asset_by_name
-from .clockify_api import ClockifyAPI
-from .constants import (
- CLOCKIFY_FTRACK_USER_PATH,
- CLOCKIFY_FTRACK_SERVER_PATH
-)
+from .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH
-class ClockifyModule(
- OpenPypeModule,
- ITrayModule,
- IPluginPaths
-):
+class ClockifyModule(OpenPypeModule, ITrayModule, IPluginPaths):
name = "clockify"
def initialize(self, modules_settings):
@@ -33,18 +22,23 @@ class ClockifyModule(
self.timer_manager = None
self.MessageWidgetClass = None
self.message_widget = None
-
- self.clockapi = ClockifyAPI(master_parent=self)
+ self._clockify_api = None
# TimersManager attributes
# - set `timers_manager_connector` only in `tray_init`
self.timers_manager_connector = None
self._timers_manager_module = None
+ @property
+ def clockify_api(self):
+ if self._clockify_api is None:
+ from .clockify_api import ClockifyAPI
+
+ self._clockify_api = ClockifyAPI(master_parent=self)
+ return self._clockify_api
+
def get_global_environments(self):
- return {
- "CLOCKIFY_WORKSPACE": self.workspace_name
- }
+ return {"CLOCKIFY_WORKSPACE": self.workspace_name}
def tray_init(self):
from .widgets import ClockifySettings, MessageWidget
@@ -52,7 +46,7 @@ class ClockifyModule(
self.MessageWidgetClass = MessageWidget
self.message_widget = None
- self.widget_settings = ClockifySettings(self.clockapi)
+ self.widget_settings = ClockifySettings(self.clockify_api)
self.widget_settings_required = None
self.thread_timer_check = None
@@ -61,7 +55,7 @@ class ClockifyModule(
self.bool_api_key_set = False
self.bool_workspace_set = False
self.bool_timer_run = False
- self.bool_api_key_set = self.clockapi.set_api()
+ self.bool_api_key_set = self.clockify_api.set_api()
# Define itself as TimersManager connector
self.timers_manager_connector = self
@@ -71,12 +65,11 @@ class ClockifyModule(
self.show_settings()
return
- self.bool_workspace_set = self.clockapi.workspace_id is not None
+ self.bool_workspace_set = self.clockify_api.workspace_id is not None
if self.bool_workspace_set is False:
return
self.start_timer_check()
-
self.set_menu_visibility()
def tray_exit(self, *_a, **_kw):
@@ -85,23 +78,19 @@ class ClockifyModule(
def get_plugin_paths(self):
"""Implementaton of IPluginPaths to get plugin paths."""
actions_path = os.path.join(
- os.path.dirname(os.path.abspath(__file__)),
- "launcher_actions"
+ os.path.dirname(os.path.abspath(__file__)), "launcher_actions"
)
- return {
- "actions": [actions_path]
- }
+ return {"actions": [actions_path]}
def get_ftrack_event_handler_paths(self):
"""Function for Ftrack module to add ftrack event handler paths."""
return {
"user": [CLOCKIFY_FTRACK_USER_PATH],
- "server": [CLOCKIFY_FTRACK_SERVER_PATH]
+ "server": [CLOCKIFY_FTRACK_SERVER_PATH],
}
def clockify_timer_stopped(self):
self.bool_timer_run = False
- # Call `ITimersManager` method
self.timer_stopped()
def start_timer_check(self):
@@ -122,45 +111,44 @@ class ClockifyModule(
def check_running(self):
while self.bool_thread_check_running is True:
bool_timer_run = False
- if self.clockapi.get_in_progress() is not None:
+ if self.clockify_api.get_in_progress() is not None:
bool_timer_run = True
if self.bool_timer_run != bool_timer_run:
if self.bool_timer_run is True:
self.clockify_timer_stopped()
elif self.bool_timer_run is False:
- actual_timer = self.clockapi.get_in_progress()
- if not actual_timer:
+ current_timer = self.clockify_api.get_in_progress()
+ if current_timer is None:
+ continue
+ current_proj_id = current_timer.get("projectId")
+ if not current_proj_id:
continue
- actual_proj_id = actual_timer["projectId"]
- if not actual_proj_id:
- continue
-
- project = self.clockapi.get_project_by_id(actual_proj_id)
+ project = self.clockify_api.get_project_by_id(
+ current_proj_id
+ )
if project and project.get("code") == 501:
continue
- project_name = project["name"]
+ project_name = project.get("name")
- actual_timer_hierarchy = actual_timer["description"]
- hierarchy_items = actual_timer_hierarchy.split("/")
+ current_timer_hierarchy = current_timer.get("description")
+ if not current_timer_hierarchy:
+ continue
+ hierarchy_items = current_timer_hierarchy.split("/")
# Each pype timer must have at least 2 items!
if len(hierarchy_items) < 2:
continue
+
task_name = hierarchy_items[-1]
hierarchy = hierarchy_items[:-1]
- task_type = None
- if len(actual_timer.get("tags", [])) > 0:
- task_type = actual_timer["tags"][0].get("name")
data = {
"task_name": task_name,
"hierarchy": hierarchy,
"project_name": project_name,
- "task_type": task_type
}
- # Call `ITimersManager` method
self.timer_started(data)
self.bool_timer_run = bool_timer_run
@@ -184,6 +172,7 @@ class ClockifyModule(
def tray_menu(self, parent_menu):
# Menu for Tray App
from qtpy import QtWidgets
+
menu = QtWidgets.QMenu("Clockify", parent_menu)
menu.setProperty("submenu", "on")
@@ -204,7 +193,9 @@ class ClockifyModule(
parent_menu.addMenu(menu)
def show_settings(self):
- self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
+ self.widget_settings.input_api_key.setText(
+ self.clockify_api.get_api_key()
+ )
self.widget_settings.show()
def set_menu_visibility(self):
@@ -218,72 +209,82 @@ class ClockifyModule(
def timer_started(self, data):
"""Tell TimersManager that timer started."""
if self._timers_manager_module is not None:
- self._timers_manager_module.timer_started(self._module.id, data)
+ self._timers_manager_module.timer_started(self.id, data)
def timer_stopped(self):
"""Tell TimersManager that timer stopped."""
if self._timers_manager_module is not None:
- self._timers_manager_module.timer_stopped(self._module.id)
+ self._timers_manager_module.timer_stopped(self.id)
def stop_timer(self):
"""Called from TimersManager to stop timer."""
- self.clockapi.finish_time_entry()
+ self.clockify_api.finish_time_entry()
- def start_timer(self, input_data):
- """Called from TimersManager to start timer."""
- # If not api key is not entered then skip
- if not self.clockapi.get_api_key():
- return
-
- actual_timer = self.clockapi.get_in_progress()
- actual_timer_hierarchy = None
- actual_project_id = None
- if actual_timer is not None:
- actual_timer_hierarchy = actual_timer.get("description")
- actual_project_id = actual_timer.get("projectId")
-
- # Concatenate hierarchy and task to get description
- desc_items = [val for val in input_data.get("hierarchy", [])]
- desc_items.append(input_data["task_name"])
- description = "/".join(desc_items)
-
- # Check project existence
- project_name = input_data["project_name"]
- project_id = self.clockapi.get_project_id(project_name)
+ def _verify_project_exists(self, project_name):
+ project_id = self.clockify_api.get_project_id(project_name)
if not project_id:
- self.log.warning((
- "Project \"{}\" was not found in Clockify. Timer won't start."
- ).format(project_name))
+ self.log.warning(
+ 'Project "{}" was not found in Clockify. Timer won\'t start.'
+ ).format(project_name)
if not self.MessageWidgetClass:
return
msg = (
- "Project \"{}\" is not"
- " in Clockify Workspace \"{}\"."
+ 'Project "{}" is not'
+ ' in Clockify Workspace "{}".'
"
Please inform your Project Manager."
- ).format(project_name, str(self.clockapi.workspace_name))
+ ).format(project_name, str(self.clockify_api.workspace_name))
self.message_widget = self.MessageWidgetClass(
msg, "Clockify - Info Message"
)
self.message_widget.closed.connect(self.on_message_widget_close)
self.message_widget.show()
+ return False
+ return project_id
+ def start_timer(self, input_data):
+ """Called from TimersManager to start timer."""
+ # If not api key is not entered then skip
+ if not self.clockify_api.get_api_key():
return
- if (
- actual_timer is not None and
- description == actual_timer_hierarchy and
- project_id == actual_project_id
- ):
+ task_name = input_data.get("task_name")
+
+ # Concatenate hierarchy and task to get description
+ description_items = list(input_data.get("hierarchy", []))
+ description_items.append(task_name)
+ description = "/".join(description_items)
+
+ # Check project existence
+ project_name = input_data.get("project_name")
+ project_id = self._verify_project_exists(project_name)
+ if not project_id:
return
+ # Setup timer tags
tag_ids = []
- task_tag_id = self.clockapi.get_tag_id(input_data["task_type"])
+ tag_name = input_data.get("task_type")
+ if not tag_name:
+ # no task_type found in the input data
+ # if the timer is restarted by idle time (bug?)
+ asset_name = input_data["hierarchy"][-1]
+ asset_doc = get_asset_by_name(project_name, asset_name)
+ task_info = asset_doc["data"]["tasks"][task_name]
+ tag_name = task_info.get("type", "")
+ if not tag_name:
+ self.log.info("No tag information found for the timer")
+
+ task_tag_id = self.clockify_api.get_tag_id(tag_name)
if task_tag_id is not None:
tag_ids.append(task_tag_id)
- self.clockapi.start_time_entry(
- description, project_id, tag_ids=tag_ids
+ # Start timer
+ self.clockify_api.start_time_entry(
+ description,
+ project_id,
+ tag_ids=tag_ids,
+ workspace_id=self.clockify_api.workspace_id,
+ user_id=self.clockify_api.user_id,
)
diff --git a/openpype/modules/clockify/constants.py b/openpype/modules/clockify/constants.py
index 66f6cb899a..4574f91be1 100644
--- a/openpype/modules/clockify/constants.py
+++ b/openpype/modules/clockify/constants.py
@@ -9,4 +9,4 @@ CLOCKIFY_FTRACK_USER_PATH = os.path.join(
)
ADMIN_PERMISSION_NAMES = ["WORKSPACE_OWN", "WORKSPACE_ADMIN"]
-CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/"
+CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/v1/"
diff --git a/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py
index c6b55947da..985cf49b97 100644
--- a/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py
+++ b/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py
@@ -4,7 +4,7 @@ from openpype_modules.ftrack.lib import ServerAction
from openpype_modules.clockify.clockify_api import ClockifyAPI
-class SyncClocifyServer(ServerAction):
+class SyncClockifyServer(ServerAction):
'''Synchronise project names and task types.'''
identifier = "clockify.sync.server"
@@ -14,12 +14,12 @@ class SyncClocifyServer(ServerAction):
role_list = ["Pypeclub", "Administrator", "project Manager"]
def __init__(self, *args, **kwargs):
- super(SyncClocifyServer, self).__init__(*args, **kwargs)
+ super(SyncClockifyServer, self).__init__(*args, **kwargs)
workspace_name = os.environ.get("CLOCKIFY_WORKSPACE")
api_key = os.environ.get("CLOCKIFY_API_KEY")
- self.clockapi = ClockifyAPI(api_key)
- self.clockapi.set_workspace(workspace_name)
+ self.clockify_api = ClockifyAPI(api_key)
+ self.clockify_api.set_workspace(workspace_name)
if api_key is None:
modified_key = "None"
else:
@@ -48,13 +48,16 @@ class SyncClocifyServer(ServerAction):
return True
def launch(self, session, entities, event):
- if self.clockapi.workspace_id is None:
+ self.clockify_api.set_api()
+ if self.clockify_api.workspace_id is None:
return {
"success": False,
"message": "Clockify Workspace or API key are not set!"
}
- if self.clockapi.validate_workspace_perm() is False:
+ if not self.clockify_api.validate_workspace_permissions(
+ self.clockify_api.workspace_id, self.clockify_api.user_id
+ ):
return {
"success": False,
"message": "Missing permissions for this action!"
@@ -88,9 +91,9 @@ class SyncClocifyServer(ServerAction):
task_type["name"] for task_type in task_types
]
try:
- clockify_projects = self.clockapi.get_projects()
+ clockify_projects = self.clockify_api.get_projects()
if project_name not in clockify_projects:
- response = self.clockapi.add_project(project_name)
+ response = self.clockify_api.add_project(project_name)
if "id" not in response:
self.log.warning(
"Project \"{}\" can't be created. Response: {}".format(
@@ -105,7 +108,7 @@ class SyncClocifyServer(ServerAction):
).format(project_name)
}
- clockify_workspace_tags = self.clockapi.get_tags()
+ clockify_workspace_tags = self.clockify_api.get_tags()
for task_type_name in task_type_names:
if task_type_name in clockify_workspace_tags:
self.log.debug(
@@ -113,7 +116,7 @@ class SyncClocifyServer(ServerAction):
)
continue
- response = self.clockapi.add_tag(task_type_name)
+ response = self.clockify_api.add_tag(task_type_name)
if "id" not in response:
self.log.warning(
"Task \"{}\" can't be created. Response: {}".format(
@@ -138,4 +141,4 @@ class SyncClocifyServer(ServerAction):
def register(session, **kw):
- SyncClocifyServer(session).register()
+ SyncClockifyServer(session).register()
diff --git a/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py b/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py
index a430791906..0e8cf6bd37 100644
--- a/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py
+++ b/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py
@@ -3,7 +3,7 @@ from openpype_modules.ftrack.lib import BaseAction, statics_icon
from openpype_modules.clockify.clockify_api import ClockifyAPI
-class SyncClocifyLocal(BaseAction):
+class SyncClockifyLocal(BaseAction):
'''Synchronise project names and task types.'''
#: Action identifier.
@@ -18,9 +18,9 @@ class SyncClocifyLocal(BaseAction):
icon = statics_icon("app_icons", "clockify-white.png")
def __init__(self, *args, **kwargs):
- super(SyncClocifyLocal, self).__init__(*args, **kwargs)
+ super(SyncClockifyLocal, self).__init__(*args, **kwargs)
#: CLockifyApi
- self.clockapi = ClockifyAPI()
+ self.clockify_api = ClockifyAPI()
def discover(self, session, entities, event):
if (
@@ -31,14 +31,18 @@ class SyncClocifyLocal(BaseAction):
return False
def launch(self, session, entities, event):
- self.clockapi.set_api()
- if self.clockapi.workspace_id is None:
+ self.clockify_api.set_api()
+ if self.clockify_api.workspace_id is None:
return {
"success": False,
"message": "Clockify Workspace or API key are not set!"
}
- if self.clockapi.validate_workspace_perm() is False:
+ if (
+ self.clockify_api.validate_workspace_permissions(
+ self.clockify_api.workspace_id, self.clockify_api.user_id)
+ is False
+ ):
return {
"success": False,
"message": "Missing permissions for this action!"
@@ -74,9 +78,9 @@ class SyncClocifyLocal(BaseAction):
task_type["name"] for task_type in task_types
]
try:
- clockify_projects = self.clockapi.get_projects()
+ clockify_projects = self.clockify_api.get_projects()
if project_name not in clockify_projects:
- response = self.clockapi.add_project(project_name)
+ response = self.clockify_api.add_project(project_name)
if "id" not in response:
self.log.warning(
"Project \"{}\" can't be created. Response: {}".format(
@@ -91,7 +95,7 @@ class SyncClocifyLocal(BaseAction):
).format(project_name)
}
- clockify_workspace_tags = self.clockapi.get_tags()
+ clockify_workspace_tags = self.clockify_api.get_tags()
for task_type_name in task_type_names:
if task_type_name in clockify_workspace_tags:
self.log.debug(
@@ -99,7 +103,7 @@ class SyncClocifyLocal(BaseAction):
)
continue
- response = self.clockapi.add_tag(task_type_name)
+ response = self.clockify_api.add_tag(task_type_name)
if "id" not in response:
self.log.warning(
"Task \"{}\" can't be created. Response: {}".format(
@@ -121,4 +125,4 @@ class SyncClocifyLocal(BaseAction):
def register(session, **kw):
- SyncClocifyLocal(session).register()
+ SyncClockifyLocal(session).register()
diff --git a/openpype/modules/clockify/launcher_actions/ClockifyStart.py b/openpype/modules/clockify/launcher_actions/ClockifyStart.py
index 7663aecc31..4a653c1b8d 100644
--- a/openpype/modules/clockify/launcher_actions/ClockifyStart.py
+++ b/openpype/modules/clockify/launcher_actions/ClockifyStart.py
@@ -6,9 +6,9 @@ from openpype_modules.clockify.clockify_api import ClockifyAPI
class ClockifyStart(LauncherAction):
name = "clockify_start_timer"
label = "Clockify - Start Timer"
- icon = "clockify_icon"
+ icon = "app_icons/clockify.png"
order = 500
- clockapi = ClockifyAPI()
+ clockify_api = ClockifyAPI()
def is_compatible(self, session):
"""Return whether the action is compatible with the session"""
@@ -17,23 +17,39 @@ class ClockifyStart(LauncherAction):
return False
def process(self, session, **kwargs):
+ self.clockify_api.set_api()
+ user_id = self.clockify_api.user_id
+ workspace_id = self.clockify_api.workspace_id
project_name = session["AVALON_PROJECT"]
asset_name = session["AVALON_ASSET"]
task_name = session["AVALON_TASK"]
-
description = asset_name
- asset_doc = get_asset_by_name(
- project_name, asset_name, fields=["data.parents"]
- )
- if asset_doc is not None:
- desc_items = asset_doc.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)
- tag_ids = []
- tag_ids.append(self.clockapi.get_tag_id(task_name))
- self.clockapi.start_time_entry(
- description, project_id, tag_ids=tag_ids
+ # fetch asset docs
+ asset_doc = get_asset_by_name(project_name, asset_name)
+
+ # get task type to fill the timer tag
+ task_info = asset_doc["data"]["tasks"][task_name]
+ task_type = task_info["type"]
+
+ # check if the task has hierarchy and fill the
+ parents_data = asset_doc["data"]
+ if parents_data is not None:
+ description_items = parents_data.get("parents", [])
+ description_items.append(asset_name)
+ description_items.append(task_name)
+ description = "/".join(description_items)
+
+ project_id = self.clockify_api.get_project_id(
+ project_name, workspace_id
+ )
+ tag_ids = []
+ tag_name = task_type
+ tag_ids.append(self.clockify_api.get_tag_id(tag_name, workspace_id))
+ self.clockify_api.start_time_entry(
+ description,
+ project_id,
+ tag_ids=tag_ids,
+ workspace_id=workspace_id,
+ user_id=user_id,
)
diff --git a/openpype/modules/clockify/launcher_actions/ClockifySync.py b/openpype/modules/clockify/launcher_actions/ClockifySync.py
index c346a1b4f6..cbd2519a04 100644
--- a/openpype/modules/clockify/launcher_actions/ClockifySync.py
+++ b/openpype/modules/clockify/launcher_actions/ClockifySync.py
@@ -3,20 +3,39 @@ from openpype_modules.clockify.clockify_api import ClockifyAPI
from openpype.pipeline import LauncherAction
-class ClockifySync(LauncherAction):
+class ClockifyPermissionsCheckFailed(Exception):
+ """Timer start failed due to user permissions check.
+ Message should be self explanatory as traceback won't be shown.
+ """
+ pass
+
+
+class ClockifySync(LauncherAction):
name = "sync_to_clockify"
label = "Sync to Clockify"
- icon = "clockify_white_icon"
+ icon = "app_icons/clockify-white.png"
order = 500
- clockapi = ClockifyAPI()
- have_permissions = clockapi.validate_workspace_perm()
+ clockify_api = ClockifyAPI()
def is_compatible(self, session):
- """Return whether the action is compatible with the session"""
- return self.have_permissions
+ """Check if there's some projects to sync"""
+ try:
+ next(get_projects())
+ return True
+ except StopIteration:
+ return False
def process(self, session, **kwargs):
+ self.clockify_api.set_api()
+ workspace_id = self.clockify_api.workspace_id
+ user_id = self.clockify_api.user_id
+ if not self.clockify_api.validate_workspace_permissions(
+ workspace_id, user_id
+ ):
+ raise ClockifyPermissionsCheckFailed(
+ "Current CLockify user is missing permissions for this action!"
+ )
project_name = session.get("AVALON_PROJECT") or ""
projects_to_sync = []
@@ -30,24 +49,28 @@ class ClockifySync(LauncherAction):
task_types = project["config"]["tasks"].keys()
projects_info[project["name"]] = task_types
- clockify_projects = self.clockapi.get_projects()
+ clockify_projects = self.clockify_api.get_projects(workspace_id)
for project_name, task_types in projects_info.items():
if project_name in clockify_projects:
continue
- response = self.clockapi.add_project(project_name)
+ response = self.clockify_api.add_project(
+ project_name, workspace_id
+ )
if "id" not in response:
- self.log.error("Project {} can't be created".format(
- project_name
- ))
+ self.log.error(
+ "Project {} can't be created".format(project_name)
+ )
continue
- clockify_workspace_tags = self.clockapi.get_tags()
+ clockify_workspace_tags = self.clockify_api.get_tags(workspace_id)
for task_type in task_types:
if task_type not in clockify_workspace_tags:
- response = self.clockapi.add_tag(task_type)
+ response = self.clockify_api.add_tag(
+ task_type, workspace_id
+ )
if "id" not in response:
- self.log.error('Task {} can\'t be created'.format(
- task_type
- ))
+ self.log.error(
+ "Task {} can't be created".format(task_type)
+ )
continue
diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py
index 122b6212c0..8c28f38b6e 100644
--- a/openpype/modules/clockify/widgets.py
+++ b/openpype/modules/clockify/widgets.py
@@ -77,15 +77,15 @@ class MessageWidget(QtWidgets.QWidget):
class ClockifySettings(QtWidgets.QWidget):
- SIZE_W = 300
+ SIZE_W = 500
SIZE_H = 130
loginSignal = QtCore.Signal(object, object, object)
- def __init__(self, clockapi, optional=True):
+ def __init__(self, clockify_api, optional=True):
super(ClockifySettings, self).__init__()
- self.clockapi = clockapi
+ self.clockify_api = clockify_api
self.optional = optional
self.validated = False
@@ -162,17 +162,17 @@ class ClockifySettings(QtWidgets.QWidget):
def click_ok(self):
api_key = self.input_api_key.text().strip()
if self.optional is True and api_key == '':
- self.clockapi.save_api_key(None)
- self.clockapi.set_api(api_key)
+ self.clockify_api.save_api_key(None)
+ self.clockify_api.set_api(api_key)
self.validated = False
self._close_widget()
return
- validation = self.clockapi.validate_api_key(api_key)
+ validation = self.clockify_api.validate_api_key(api_key)
if validation:
- self.clockapi.save_api_key(api_key)
- self.clockapi.set_api(api_key)
+ self.clockify_api.save_api_key(api_key)
+ self.clockify_api.set_api(api_key)
self.validated = True
self._close_widget()
else:
diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py
index 0ba68285a4..43286f7da4 100644
--- a/openpype/modules/timers_manager/timers_manager.py
+++ b/openpype/modules/timers_manager/timers_manager.py
@@ -141,7 +141,9 @@ class TimersManager(
signal_handler = SignalHandler(self)
idle_manager = IdleManager()
widget_user_idle = WidgetUserIdle(self)
- widget_user_idle.set_countdown_start(self.time_show_message)
+ widget_user_idle.set_countdown_start(
+ self.time_stop_timer - self.time_show_message
+ )
idle_manager.signal_reset_timer.connect(
widget_user_idle.reset_countdown