Initial commit
This commit is contained in:
408
lib/projects.py
Normal file
408
lib/projects.py
Normal file
@@ -0,0 +1,408 @@
|
||||
from typing import Any, Dict, Optional, List
|
||||
from lib.kanboard_api import KanboardAPI
|
||||
from lib.base_migrator import BaseMigrator
|
||||
|
||||
|
||||
class ProjectsMigrator(BaseMigrator):
|
||||
"""Миграция и удаление проектов в Kanboard."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_api: KanboardAPI,
|
||||
target_api: KanboardAPI,
|
||||
project_mapping_file: str = "project_mapping.json",
|
||||
user_mapping_file: str = "user_mapping.json",
|
||||
column_mapping_file: str = "column_mapping.json"
|
||||
):
|
||||
self.source_api = source_api
|
||||
self.target_api = target_api
|
||||
self.project_mapping_file = project_mapping_file
|
||||
self.user_mapping_file = user_mapping_file
|
||||
self.column_mapping_file = column_mapping_file
|
||||
self.project_mapping = self._load_mapping(self.project_mapping_file)
|
||||
self.user_mapping = self._load_mapping(self.user_mapping_file)
|
||||
self.column_mapping = self._load_nested_mapping(self.column_mapping_file)
|
||||
|
||||
# === Helpers ===
|
||||
def _create_or_find_project(self, src_proj: Dict[str, Any]) -> Optional[int]:
|
||||
name = src_proj.get("name") or ""
|
||||
identifier = src_proj.get("identifier") or ""
|
||||
description = src_proj.get("description") or ""
|
||||
owner_id = src_proj.get("owner_id")
|
||||
email = src_proj.get("email")
|
||||
|
||||
target_proj = None
|
||||
found = False
|
||||
if identifier:
|
||||
try:
|
||||
target_proj = self.target_api.call("getProjectByIdentifier", {"identifier": identifier})
|
||||
except Exception:
|
||||
target_proj = None
|
||||
if not target_proj and name:
|
||||
try:
|
||||
target_proj = self.target_api.call("getProjectByName", {"name": name})
|
||||
except Exception:
|
||||
target_proj = None
|
||||
if target_proj:
|
||||
try:
|
||||
found = True
|
||||
return int(target_proj["id"]), found
|
||||
except Exception:
|
||||
return None, found
|
||||
|
||||
create_params: Dict[str, Any] = {"name": name}
|
||||
if description is not None:
|
||||
create_params["description"] = description
|
||||
if identifier:
|
||||
create_params["identifier"] = identifier
|
||||
if email:
|
||||
create_params["email"] = email
|
||||
|
||||
if owner_id:
|
||||
try:
|
||||
mapped_owner = self.user_mapping.get(int(owner_id))
|
||||
if mapped_owner:
|
||||
create_params["owner_id"] = mapped_owner
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
new_id = self.target_api.call("createProject", create_params)
|
||||
if new_id:
|
||||
return int(new_id), False
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при createProject '{name}': {e}")
|
||||
return None, found
|
||||
|
||||
|
||||
def _migrate_columns(self, src_project_id: int, target_project_id: int):
|
||||
"""Миграция колонок - создаем такие же как в source и сохраняем маппинг."""
|
||||
try:
|
||||
src_columns = self.source_api.call("getColumns", [src_project_id]) or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить колонки {src_project_id}: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
target_columns = self.target_api.call("getColumns", [target_project_id]) or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить колонки target-проекта {target_project_id}: {e}")
|
||||
target_columns = []
|
||||
|
||||
# Инициализируем маппинг колонок для этого проекта
|
||||
if src_project_id not in self.column_mapping:
|
||||
self.column_mapping[src_project_id] = {}
|
||||
|
||||
project_column_mapping = self.column_mapping[src_project_id]
|
||||
created_columns: List[int] = []
|
||||
existing_titles = {col.get("title", "").lower(): col for col in target_columns}
|
||||
|
||||
for src_col in sorted(src_columns, key=lambda c: int(c.get("position", 0))):
|
||||
src_col_id = int(src_col.get("id"))
|
||||
title = src_col.get("title", "")
|
||||
task_limit = None
|
||||
try:
|
||||
tl = int(src_col.get("task_limit", 0))
|
||||
if tl > 0:
|
||||
task_limit = tl
|
||||
except Exception:
|
||||
pass
|
||||
description = src_col.get("description")
|
||||
|
||||
# Проверяем, есть ли колонка с таким названием
|
||||
if title.lower() in existing_titles:
|
||||
# Колонка уже существует, находим ее ID и сохраняем маппинг
|
||||
target_col = existing_titles[title.lower()]
|
||||
target_col_id = int(target_col.get("id"))
|
||||
project_column_mapping[src_col_id] = target_col_id
|
||||
created_columns.append(target_col_id)
|
||||
print(f"Колонка '{title}' уже существует ({src_col_id} -> {target_col_id})")
|
||||
continue
|
||||
|
||||
# Создаем новую колонку
|
||||
params = {"project_id": target_project_id, "title": title}
|
||||
if task_limit is not None:
|
||||
params["task_limit"] = task_limit
|
||||
if description:
|
||||
params["description"] = description
|
||||
|
||||
try:
|
||||
new_col_id = self.target_api.call("addColumn", params)
|
||||
if new_col_id:
|
||||
new_col_id_int = int(new_col_id)
|
||||
created_columns.append(new_col_id_int)
|
||||
# Сохраняем маппинг новой колонки
|
||||
project_column_mapping[src_col_id] = new_col_id_int
|
||||
print(f"Добавлена колонка '{title}' ({src_col_id} -> {new_col_id_int})")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при addColumn '{title}': {e}")
|
||||
|
||||
# Обновляем позиции колонок
|
||||
for position, col_id in enumerate(created_columns, start=1):
|
||||
try:
|
||||
ok = self.target_api.call("changeColumnPosition", [target_project_id, col_id, position])
|
||||
if not ok:
|
||||
print(f"Не удалось установить позицию {position} для колонки {col_id}")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при changeColumnPosition {col_id}: {e}")
|
||||
|
||||
# Сохраняем маппинг колонок после обработки всех колонок проекта
|
||||
self._save_nested_mapping(self.column_mapping_file, self.column_mapping)
|
||||
|
||||
def _migrate_columns(self, src_project_id: int, target_project_id: int):
|
||||
"""Миграция колонок - создаем такие же как в source и сохраняем маппинг."""
|
||||
try:
|
||||
src_columns = self.source_api.call("getColumns", [src_project_id]) or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить колонки {src_project_id}: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
target_columns = self.target_api.call("getColumns", [target_project_id]) or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить колонки target-проекта {target_project_id}: {e}")
|
||||
target_columns = []
|
||||
|
||||
# Инициализируем маппинг колонок для этого проекта
|
||||
if str(src_project_id) not in self.column_mapping:
|
||||
self.column_mapping[str(src_project_id)] = {}
|
||||
|
||||
project_column_mapping = self.column_mapping[str(src_project_id)]
|
||||
created_columns: List[int] = []
|
||||
existing_titles = {col.get("title", "").lower(): col for col in target_columns}
|
||||
|
||||
for src_col in sorted(src_columns, key=lambda c: int(c.get("position", 0))):
|
||||
src_col_id = src_col.get("id")
|
||||
title = src_col.get("title", "")
|
||||
task_limit = None
|
||||
try:
|
||||
tl = int(src_col.get("task_limit", 0))
|
||||
if tl > 0:
|
||||
task_limit = tl
|
||||
except Exception:
|
||||
pass
|
||||
description = src_col.get("description")
|
||||
|
||||
# Проверяем, есть ли колонка с таким названием
|
||||
if title.lower() in existing_titles:
|
||||
# Колонка уже существует, находим ее ID и сохраняем маппинг
|
||||
target_col = existing_titles[title.lower()]
|
||||
target_col_id = target_col.get("id")
|
||||
project_column_mapping[str(src_col_id)] = target_col_id
|
||||
created_columns.append(int(target_col_id))
|
||||
print(f"Колонка '{title}' уже существует ({src_col_id} -> {target_col_id})")
|
||||
continue
|
||||
|
||||
# Создаем новую колонку
|
||||
params = {"project_id": target_project_id, "title": title}
|
||||
if task_limit is not None:
|
||||
params["task_limit"] = task_limit
|
||||
if description:
|
||||
params["description"] = description
|
||||
|
||||
try:
|
||||
new_col_id = self.target_api.call("addColumn", params)
|
||||
if new_col_id:
|
||||
new_col_id_int = int(new_col_id)
|
||||
created_columns.append(new_col_id_int)
|
||||
# Сохраняем маппинг новой колонки
|
||||
project_column_mapping[str(src_col_id)] = new_col_id_int
|
||||
print(f"Добавлена колонка '{title}' ({src_col_id} -> {new_col_id_int})")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при addColumn '{title}': {e}")
|
||||
|
||||
# Обновляем позиции колонок
|
||||
for position, col_id in enumerate(created_columns, start=1):
|
||||
try:
|
||||
ok = self.target_api.call("changeColumnPosition", [target_project_id, col_id, position])
|
||||
if not ok:
|
||||
print(f"Не удалось установить позицию {position} для колонки {col_id}")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при changeColumnPosition {col_id}: {e}")
|
||||
|
||||
# Сохраняем маппинг колонок после обработки всех колонок проекта
|
||||
self._save_mapping(self.column_mapping_file, self.column_mapping)
|
||||
def _migrate_project_users(self, src_project_id: int, target_project_id: int):
|
||||
try:
|
||||
src_members = self.source_api.call("getProjectUsers", [src_project_id]) or {}
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить пользователей проекта {src_project_id}: {e}")
|
||||
src_members = {}
|
||||
|
||||
for src_uid_str, username in list(src_members.items()):
|
||||
try:
|
||||
src_uid = int(src_uid_str)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
role = self.source_api.call("getProjectUserRole", [src_project_id, src_uid])
|
||||
except Exception:
|
||||
role = None
|
||||
|
||||
target_uid = self.user_mapping.get(src_uid)
|
||||
if not target_uid:
|
||||
print(f" Пропускаем пользователя '{username}' ({src_uid}) — нет в user_mapping")
|
||||
continue
|
||||
|
||||
try:
|
||||
ok = self.target_api.call(
|
||||
"addProjectUser",
|
||||
[target_project_id, target_uid, role] if role else [target_project_id, target_uid]
|
||||
)
|
||||
if ok:
|
||||
print(f" Добавлен пользователь '{username}' -> target_id {target_uid} роль='{role}'")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при addProjectUser '{username}': {e}")
|
||||
|
||||
def _migrate_project_files(self, src_project_id: int, target_project_id: int):
|
||||
"""Миграция файлов проекта."""
|
||||
try:
|
||||
files = self.source_api.call("getAllProjectFiles", {"project_id": src_project_id}) or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить файлы проекта {src_project_id}: {e}")
|
||||
return
|
||||
|
||||
for f in files:
|
||||
file_id = int(f.get("id"))
|
||||
filename = f.get("name")
|
||||
try:
|
||||
data_b64 = self.source_api.call("downloadProjectFile", [src_project_id, file_id])
|
||||
if not data_b64:
|
||||
print(f" Пропущен файл '{filename}' — пустой контент")
|
||||
continue
|
||||
new_id = self.target_api.call("createProjectFile", [target_project_id, filename, data_b64])
|
||||
if new_id:
|
||||
print(f" Файл '{filename}' перенесён ({file_id} -> {new_id})")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при переносе файла '{filename}': {e}")
|
||||
|
||||
def _apply_project_settings(self, src_project_id: int, target_project_id: int):
|
||||
try:
|
||||
src_info = self.source_api.call("getProjectById", {"project_id": src_project_id}) or {}
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить info проекта {src_project_id}: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
if str(src_info.get("is_active", "1")) == "1":
|
||||
self.target_api.call("enableProject", [target_project_id])
|
||||
else:
|
||||
self.target_api.call("disableProject", [target_project_id])
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при установке is_active {target_project_id}: {e}")
|
||||
|
||||
try:
|
||||
if str(src_info.get("is_public", "0")) == "1":
|
||||
self.target_api.call("enableProjectPublicAccess", [target_project_id])
|
||||
else:
|
||||
self.target_api.call("disableProjectPublicAccess", [target_project_id])
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при установке public access {target_project_id}: {e}")
|
||||
|
||||
updatable = {}
|
||||
for fld in (
|
||||
"start_date", "end_date", "priority_default",
|
||||
"priority_start", "priority_end", "email",
|
||||
"identifier", "description", "name"
|
||||
):
|
||||
if fld in src_info and src_info.get(fld) is not None:
|
||||
updatable[fld] = src_info.get(fld)
|
||||
|
||||
if updatable:
|
||||
updatable["project_id"] = target_project_id
|
||||
try:
|
||||
ok = self.target_api.call("updateProject", updatable)
|
||||
if not ok:
|
||||
print(f" Не удалось применить дополнительные настройки для проекта {target_project_id}")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при updateProject {target_project_id}: {e}")
|
||||
|
||||
def _delete_project_files(self, project_id: int):
|
||||
"""Удаление всех файлов проекта перед удалением."""
|
||||
try:
|
||||
files = self.target_api.call("getAllProjectFiles", {"project_id": project_id}) or []
|
||||
if files:
|
||||
self.target_api.call("removeAllProjectFiles", {"project_id": project_id})
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при удалении файлов проекта {project_id}: {e}")
|
||||
|
||||
# === Public API ===
|
||||
def migrate(self):
|
||||
print("Начало миграции проектов...")
|
||||
try:
|
||||
source_projects = self.source_api.call("getAllProjects") or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить список проектов: {e}")
|
||||
source_projects = []
|
||||
|
||||
for proj in source_projects:
|
||||
try:
|
||||
src_id = int(proj.get("id"))
|
||||
except Exception:
|
||||
continue
|
||||
target_id, found = self._create_or_find_project(proj)
|
||||
if not target_id:
|
||||
print(f"Не удалось создать или найти проект '{proj.get('name')}' ({src_id})")
|
||||
continue
|
||||
|
||||
self.project_mapping[src_id] = target_id
|
||||
if found:
|
||||
print(f"Проект '{proj.get('name')}' уже существует ({src_id} -> {target_id})")
|
||||
else:
|
||||
print(f"Проект '{proj.get('name')}' создан ({src_id} -> {target_id})")
|
||||
|
||||
self._migrate_columns(src_id, target_id)
|
||||
self._migrate_project_users(src_id, target_id)
|
||||
self._migrate_project_files(src_id, target_id)
|
||||
self._apply_project_settings(src_id, target_id)
|
||||
|
||||
self._save_mapping(self.project_mapping_file, self.project_mapping)
|
||||
self._save_mapping(self.column_mapping_file, self.column_mapping)
|
||||
print(f"Миграция проектов завершена.")
|
||||
print(f"Маппинг проектов сохранён в {self.project_mapping_file}")
|
||||
print(f"Маппинг колонок сохранён в {self.column_mapping_file}")
|
||||
|
||||
def delete_all(self, exclude_project_ids: Optional[List[int]] = None, **kwargs):
|
||||
"""
|
||||
Удаление всех проектов.
|
||||
|
||||
Args:
|
||||
exclude_project_ids: Список ID проектов, которые нужно исключить из удаления
|
||||
"""
|
||||
exclude_project_ids = set(exclude_project_ids or [])
|
||||
print("Начало удаления проектов...")
|
||||
try:
|
||||
all_projects = self.target_api.call("getAllProjects") or []
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Не удалось получить проекты target: {e}")
|
||||
return
|
||||
|
||||
for p in all_projects:
|
||||
try:
|
||||
pid = int(p.get("id"))
|
||||
except Exception:
|
||||
continue
|
||||
if pid in exclude_project_ids:
|
||||
continue
|
||||
try:
|
||||
self._delete_project_files(pid)
|
||||
ok = self.target_api.call("removeProject", {"project_id": pid})
|
||||
if ok:
|
||||
print(f"Проект '{p.get('name')}' (ID {pid}) удалён")
|
||||
else:
|
||||
print(f"Проект '{p.get('name')}' (ID {pid}) не удалён")
|
||||
except Exception as e:
|
||||
print(f"[{type(e).__name__}] Ошибка при удалении проекта {pid}: {e}")
|
||||
|
||||
if hasattr(self, 'project_mapping_file'):
|
||||
self._delete_mapping_file(self.project_mapping_file)
|
||||
if hasattr(self, 'column_mapping_file'):
|
||||
self._delete_mapping_file(self.column_mapping_file)
|
||||
|
||||
if hasattr(self, 'project_mapping_file'):
|
||||
self._delete_mapping_file(self.project_mapping_file)
|
||||
if hasattr(self, 'project_mapping'):
|
||||
self.project_mapping.clear()
|
||||
|
||||
print("Удаление проектов завершено.")
|
||||
|
||||
Reference in New Issue
Block a user