135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""Сервис переноса трудочасов из Clockify в Redmine."""
|
||
|
||
import logging
|
||
|
||
from colorama import Fore
|
||
|
||
from classes import Activity, AggregatedActivity
|
||
from utils import format_date, format_hours
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def aggregate_activities(activities: list[Activity]) -> list[AggregatedActivity]:
|
||
"""Объединяет одинаковые активности и суммирует их трудозатраты."""
|
||
grouped_activities = {}
|
||
for activity in activities:
|
||
key = (
|
||
activity.author_id,
|
||
activity.task_id,
|
||
activity.tag_id,
|
||
activity.project_id,
|
||
activity.vetro_project_id,
|
||
activity.date_start.date(),
|
||
)
|
||
grouped_activities.setdefault(key, []).append(activity)
|
||
|
||
result = []
|
||
for grouped in grouped_activities.values():
|
||
descriptions = list(
|
||
dict.fromkeys(
|
||
description.rstrip(".")
|
||
for activity in grouped
|
||
if (description := activity.description.strip())
|
||
)
|
||
)
|
||
result.append(
|
||
AggregatedActivity(
|
||
**{
|
||
**grouped[0].__dict__,
|
||
"description": ". ".join(descriptions),
|
||
"time_spent": sum(activity.time_spent for activity in grouped),
|
||
},
|
||
source_activities=tuple(grouped),
|
||
)
|
||
)
|
||
return result
|
||
|
||
|
||
class TrackingService:
|
||
"""Координирует загрузку, сохранение и перенос активностей в Redmine."""
|
||
|
||
def __init__(self, clock, red, db, is_debug: bool, is_dry_run: bool = False):
|
||
self.clock = clock
|
||
self.red = red
|
||
self.db = db
|
||
self.is_debug = is_debug
|
||
self.is_dry_run = is_dry_run
|
||
|
||
def track_period(self, start, end, page_size: int | None):
|
||
"""Переносит активности за период и возвращает список найденных активностей."""
|
||
self._save_current_user()
|
||
activities = sorted(
|
||
self.clock.get_activities(
|
||
**{
|
||
"start": format_date(start),
|
||
"end": format_date(end),
|
||
"page-size": page_size,
|
||
},
|
||
),
|
||
key=lambda x: x.date_start,
|
||
)
|
||
if not activities:
|
||
logger.info(Fore.YELLOW + "Нет задач для занесения трудочасов" + Fore.RESET)
|
||
return []
|
||
activities_to_track = aggregate_activities(
|
||
[
|
||
activity
|
||
for activity in activities
|
||
if not activity.is_tracked and activity.date_end
|
||
]
|
||
)
|
||
if self.is_dry_run:
|
||
self._log_preview(activities, activities_to_track)
|
||
return activities
|
||
|
||
self.db.insert_activities(activities)
|
||
self.red.track_activities(
|
||
activities_to_track,
|
||
on_success=None if self.is_debug else self._save_tracked_activity,
|
||
)
|
||
return activities
|
||
|
||
def _save_current_user(self):
|
||
"""Сохраняет связку текущих пользователей Clockify и Redmine."""
|
||
user = self.clock.get_user()
|
||
clock_user = {
|
||
"clockify_user_id": user["id"],
|
||
"clockify_email": user["email"],
|
||
"clockify_username": user["username"],
|
||
}
|
||
user = self.red.get_user()
|
||
red_user = {
|
||
"redmine_user_id": user["id"],
|
||
"redmine_email": user["email"],
|
||
"first_name": user["first_name"],
|
||
"last_name": user["last_name"],
|
||
"redmine_username": user["username"],
|
||
}
|
||
self.db.insert_user(**clock_user, **red_user)
|
||
|
||
def _save_tracked_activity(self, activity, redmine_time_entry_id: int):
|
||
"""Сохраняет активность как успешно перенесенную в Redmine."""
|
||
source_activities = list(activity.source_activities or (activity,))
|
||
for source_activity in source_activities:
|
||
source_activity.is_tracked = True
|
||
self.db.insert_activities(source_activities)
|
||
self.db.insert_time_entry(
|
||
activity,
|
||
source_activities,
|
||
redmine_time_entry_id,
|
||
)
|
||
|
||
def _log_preview(self, activities, activities_to_track):
|
||
"""Выводит план переноса активностей без сохранения и отправки."""
|
||
total_hours = sum(activity.time_spent for activity in activities_to_track)
|
||
logger.info(f"{Fore.YELLOW}Предпросмотр переноса трудочасов{Fore.RESET}")
|
||
logger.info(f"Найдено активностей: {len(activities)}")
|
||
logger.info(f"Готово к переносу: {len(activities_to_track)}")
|
||
logger.info(f"Всего часов: {format_hours(total_hours)}")
|
||
for activity in activities_to_track:
|
||
logger.info(
|
||
f"{activity.date_start.date()} | #{activity.task_id} | "
|
||
f"{format_hours(activity.time_spent)} | {activity.description}"
|
||
)
|