Clockify: refresh and fix the integration (#4607)

* WIP clockify fix

* WIP disable wp validation, make sync work

* fix launcher start timer action

* fix finish time entry

* fix start and stop timers, cleanup, add TODO

* show task name and type in description, add TODO

* change rate limiter constants

* black formatting

* remove task type from data

* cleanup debug prints

* fix hound comments

* remove unused import

* move ids to property, fix user validation

* remove f-strings, rollback description parsing

* attempt to fix ftrack actions

* check if sync action got some projects

* get api data on process

* remove unused variable

* remove ratelimiter dependency

* add response validation

* a bit cleanup

* Update openpype/modules/clockify/clockify_module.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Update openpype/modules/clockify/clockify_api.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Update openpype/modules/clockify/clockify_api.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Update openpype/modules/clockify/clockify_api.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Update openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* replace dunders with underscores

* remove excessive variables

* update set_user_id

* continue check_running if no timer found

* bring back come py2 compatibility

* cleanup

* get values directly from clockapi

* hound

* get task type to fill the tag field correctly

* add logger, catch some json errors

* remove check running timer, add project_id verification module

* add current task_id check

* remove package entries

* make method private, fix typo

* get task_type for the idle-restarted timer

* remove trailing whitespace

* show correct idle countdown values

* finx indentation

* Update openpype/modules/clockify/clockify_api.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Update openpype/modules/clockify/clockify_module.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* revert lock file

* remove unused constants and redundant code

* import clockify_api inside the method

* do not query asset docs double time, add comments

* add permissions check fail Exception

* rename clockapi to clockify_api

* formatting

* removed unused variables

---------

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <jakub.trllo@gmail.com>
This commit is contained in:
Alexey Bogomolov 2023-03-17 13:41:32 +03:00 committed by GitHub
parent 289efe25f8
commit 249bda0c80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 361 additions and 351 deletions

View file

@ -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))

View file

@ -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 <b>\"{}\"</b> is not"
" in Clockify Workspace <b>\"{}\"</b>."
'Project <b>"{}"</b> is not'
' in Clockify Workspace <b>"{}"</b>.'
"<br><br>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,
)

View file

@ -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/"

View file

@ -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()

View file

@ -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()

View file

@ -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,
)

View file

@ -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

View file

@ -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:

View file

@ -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