Удаление баз, поиск, дублирование в ZIP-профиль + фикс скролла
- Панель «Подключённые базы»: клик по пользователю показывает его базы, удаление отмеченных с подтверждением - Поиск по пользователям и базам (галочки сохраняются при фильтрации) - Опциональное дублирование add/delete в ZIP-профиль (автопоиск папки <user>.*) - Устойчивое чтение ibases.v8i (UTF-8 BOM/без/cp1251) - Интерфейс переведён на русский, окно резиновое - Фикс: сброс прокрутки списка наверх при поиске - Новая иконка download.ico, spec теперь версионируется Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.spec
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['Main_v3.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='1C_Base_Adder',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['download.ico'],
|
||||||
|
)
|
||||||
+401
-175
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox, filedialog
|
||||||
import pyodbc
|
import pyodbc
|
||||||
import uuid
|
import uuid
|
||||||
import sys
|
import sys
|
||||||
@@ -71,97 +72,154 @@ if getattr(sys, "stdout", None):
|
|||||||
class RemoteFolderSelector(tk.Tk):
|
class RemoteFolderSelector(tk.Tk):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title("Remote Folder and SQL Database Selector")
|
self.title("Добавление баз 1С пользователям")
|
||||||
self.geometry("620x560")
|
self.geometry("760x780")
|
||||||
|
self.minsize(700, 640)
|
||||||
|
|
||||||
|
# --- Состояние ---
|
||||||
|
self.ip_address = None # None/"" => локальный ПК, иначе IP удалённого ПК
|
||||||
|
self.active_user = None # пользователь, чьи базы показаны в панели "Подключённые базы"
|
||||||
|
self.user_vars = {} # имя пользователя -> BooleanVar (галочка)
|
||||||
|
self.db_vars = {} # имя базы из SQL -> BooleanVar (галочка)
|
||||||
|
self.conn_vars = {} # имя подключённой базы активного пользователя -> BooleanVar
|
||||||
|
|
||||||
|
# Компоновка окна: две колонки сверху, широкая кнопка "Добавить" снизу
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
self.grid_columnconfigure(1, weight=1)
|
||||||
|
self.grid_rowconfigure(0, weight=1)
|
||||||
|
|
||||||
# Создадим фреймы для лучшей компоновки элементов
|
|
||||||
self.frame_left = tk.Frame(self)
|
self.frame_left = tk.Frame(self)
|
||||||
self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
|
self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
self.frame_right = tk.Frame(self)
|
self.frame_right = tk.Frame(self)
|
||||||
self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
|
self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
|
||||||
|
|
||||||
# Сделаем окна масштабируемыми
|
self._build_left_panel()
|
||||||
self.grid_columnconfigure(0, weight=1)
|
self._build_right_panel()
|
||||||
self.grid_columnconfigure(1, weight=1)
|
|
||||||
self.grid_rowconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Текущий режим подключения (локально либо по IP)
|
# Большая кнопка добавления снизу, на всю ширину окна
|
||||||
self.ip_address = None
|
self.add_button = tk.Button(
|
||||||
|
self, text="Добавить отмеченные базы отмеченным пользователям",
|
||||||
|
font=("Segoe UI", 11, "bold"), command=self.add_database_to_ibases
|
||||||
|
)
|
||||||
|
self.add_button.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 12), sticky="ew")
|
||||||
|
|
||||||
# Поле ввода IP-адреса
|
# По умолчанию грузим пользователей локального ПК
|
||||||
self.ip_label = tk.Label(self.frame_left, text="Enter IP (optional):")
|
|
||||||
self.ip_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.ip_entry = tk.Entry(self.frame_left, width=30)
|
|
||||||
self.ip_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.connect_button = tk.Button(self.frame_left, text="Load Users", command=self.load_users)
|
|
||||||
self.connect_button.grid(row=2, column=0, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Панель "отметить все / снять все" для пользователей
|
|
||||||
self.user_toolbar = tk.Frame(self.frame_left)
|
|
||||||
self.user_toolbar.grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w")
|
|
||||||
tk.Button(self.user_toolbar, text="Отметить все",
|
|
||||||
command=lambda: self._set_all(self.user_vars, True)).pack(side="left", padx=(0, 4))
|
|
||||||
tk.Button(self.user_toolbar, text="Снять все",
|
|
||||||
command=lambda: self._set_all(self.user_vars, False)).pack(side="left")
|
|
||||||
|
|
||||||
# Список пользователей с чекбоксами
|
|
||||||
self.user_vars = {}
|
|
||||||
self.user_container, self.user_inner = self._create_scrollable_checklist(self.frame_left)
|
|
||||||
self.user_container.grid(row=4, column=0, padx=5, pady=5, sticky="nsew")
|
|
||||||
self.frame_left.grid_rowconfigure(4, weight=1)
|
|
||||||
self.frame_left.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Поля для ввода данных SQL
|
|
||||||
self.server_label = tk.Label(self.frame_right, text="SQL Server:")
|
|
||||||
self.server_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.server_entry = tk.Entry(self.frame_right, width=30)
|
|
||||||
self.server_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
self.server_entry.insert(0, "SERVERSQL")
|
|
||||||
|
|
||||||
self.login_label = tk.Label(self.frame_right, text="SQL Login:")
|
|
||||||
self.login_label.grid(row=2, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.login_entry = tk.Entry(self.frame_right, width=30)
|
|
||||||
self.login_entry.grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
self.login_entry.insert(0, "sa")
|
|
||||||
|
|
||||||
self.password_label = tk.Label(self.frame_right, text="SQL Password:")
|
|
||||||
self.password_label.grid(row=4, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.password_entry = tk.Entry(self.frame_right, show="*", width=30)
|
|
||||||
self.password_entry.grid(row=5, column=0, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
self.sql_button = tk.Button(self.frame_right, text="Load Databases", command=self.load_databases)
|
|
||||||
self.sql_button.grid(row=6, column=0, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Панель "отметить все / снять все" для баз
|
|
||||||
self.db_toolbar = tk.Frame(self.frame_right)
|
|
||||||
self.db_toolbar.grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w")
|
|
||||||
tk.Button(self.db_toolbar, text="Отметить все",
|
|
||||||
command=lambda: self._set_all(self.db_vars, True)).pack(side="left", padx=(0, 4))
|
|
||||||
tk.Button(self.db_toolbar, text="Снять все",
|
|
||||||
command=lambda: self._set_all(self.db_vars, False)).pack(side="left")
|
|
||||||
|
|
||||||
# Список баз данных с чекбоксами
|
|
||||||
self.db_vars = {}
|
|
||||||
self.db_container, self.db_inner = self._create_scrollable_checklist(self.frame_right)
|
|
||||||
self.db_container.grid(row=8, column=0, padx=5, pady=5, sticky="nsew")
|
|
||||||
self.frame_right.grid_rowconfigure(8, weight=1)
|
|
||||||
self.frame_right.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Кнопка добавления базы
|
|
||||||
self.add_button = tk.Button(self.frame_left, text="Добавить отмеченные базы", command=self.add_database_to_ibases)
|
|
||||||
self.add_button.grid(row=5, column=0, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Загрузка пользователей с локального компьютера по умолчанию
|
|
||||||
self.load_users()
|
self.load_users()
|
||||||
|
|
||||||
def _create_scrollable_checklist(self, parent, width=200, height=180):
|
# ------------------------------------------------------------------ UI
|
||||||
"""Создаём прокручиваемую область для чекбоксов. Возвращаем (container, inner)."""
|
|
||||||
|
def _build_left_panel(self):
|
||||||
|
f = self.frame_left
|
||||||
|
f.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
tk.Label(f, text="IP-адрес (необязательно):").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.ip_entry = tk.Entry(f)
|
||||||
|
self.ip_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
|
||||||
|
tk.Button(f, text="Загрузить пользователей", command=self.load_users)\
|
||||||
|
.grid(row=2, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Поиск по пользователям
|
||||||
|
tk.Label(f, text="Поиск пользователей:").grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.user_search_var = tk.StringVar()
|
||||||
|
self.user_search_var.trace_add("write", lambda *a: self._render_checklist(
|
||||||
|
self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click))
|
||||||
|
tk.Entry(f, textvariable=self.user_search_var)\
|
||||||
|
.grid(row=4, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
|
||||||
|
# Отметить все / снять все (с учётом фильтра)
|
||||||
|
bar = tk.Frame(f)
|
||||||
|
bar.grid(row=5, column=0, padx=5, pady=(0, 3), sticky="w")
|
||||||
|
tk.Button(bar, text="Отметить все",
|
||||||
|
command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), True))\
|
||||||
|
.pack(side="left", padx=(0, 4))
|
||||||
|
tk.Button(bar, text="Снять все",
|
||||||
|
command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), False))\
|
||||||
|
.pack(side="left")
|
||||||
|
|
||||||
|
# Список пользователей
|
||||||
|
self.user_container, self.user_inner = self._create_scrollable_checklist(f, height=150)
|
||||||
|
self.user_container.grid(row=6, column=0, padx=5, pady=5, sticky="nsew")
|
||||||
|
f.grid_rowconfigure(6, weight=1)
|
||||||
|
|
||||||
|
# Панель подключённых баз активного пользователя
|
||||||
|
self.conn_label = tk.Label(f, text="Подключённые базы: (выберите пользователя)")
|
||||||
|
self.conn_label.grid(row=7, column=0, padx=5, pady=(8, 0), sticky="w")
|
||||||
|
|
||||||
|
bar2 = tk.Frame(f)
|
||||||
|
bar2.grid(row=8, column=0, padx=5, pady=(0, 3), sticky="w")
|
||||||
|
tk.Button(bar2, text="Отметить все",
|
||||||
|
command=lambda: self._set_all(self.conn_vars, "", True)).pack(side="left", padx=(0, 4))
|
||||||
|
tk.Button(bar2, text="Снять все",
|
||||||
|
command=lambda: self._set_all(self.conn_vars, "", False)).pack(side="left")
|
||||||
|
|
||||||
|
self.conn_container, self.conn_inner = self._create_scrollable_checklist(f, height=150)
|
||||||
|
self.conn_container.grid(row=9, column=0, padx=5, pady=5, sticky="nsew")
|
||||||
|
f.grid_rowconfigure(9, weight=1)
|
||||||
|
|
||||||
|
tk.Button(f, text="Удалить отмеченные базы у пользователя", command=self.delete_connected_bases)\
|
||||||
|
.grid(row=10, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Опция дублирования в ZIP (roaming) профиль
|
||||||
|
self.zip_enabled = tk.BooleanVar(value=False)
|
||||||
|
tk.Checkbutton(f, text="Дублировать изменения в ZIP-профиль пользователя",
|
||||||
|
variable=self.zip_enabled, command=self._toggle_zip)\
|
||||||
|
.grid(row=11, column=0, padx=5, pady=(8, 0), sticky="w")
|
||||||
|
|
||||||
|
zf = tk.Frame(f)
|
||||||
|
zf.grid(row=12, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
zf.grid_columnconfigure(0, weight=1)
|
||||||
|
self.zip_path_var = tk.StringVar(value=r"Z:\ZIP_User_Profiles")
|
||||||
|
self.zip_path_entry = tk.Entry(zf, textvariable=self.zip_path_var, state="disabled")
|
||||||
|
self.zip_path_entry.grid(row=0, column=0, sticky="ew", padx=(0, 4))
|
||||||
|
self.zip_browse_btn = tk.Button(zf, text="Обзор...", state="disabled", command=self._browse_zip)
|
||||||
|
self.zip_browse_btn.grid(row=0, column=1)
|
||||||
|
|
||||||
|
def _build_right_panel(self):
|
||||||
|
f = self.frame_right
|
||||||
|
f.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
tk.Label(f, text="SQL-сервер:").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.server_entry = tk.Entry(f)
|
||||||
|
self.server_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
self.server_entry.insert(0, "SERVERSQL")
|
||||||
|
|
||||||
|
tk.Label(f, text="Логин SQL:").grid(row=2, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.login_entry = tk.Entry(f)
|
||||||
|
self.login_entry.grid(row=3, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
self.login_entry.insert(0, "sa")
|
||||||
|
|
||||||
|
tk.Label(f, text="Пароль SQL:").grid(row=4, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.password_entry = tk.Entry(f, show="*")
|
||||||
|
self.password_entry.grid(row=5, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
|
||||||
|
tk.Button(f, text="Загрузить базы", command=self.load_databases)\
|
||||||
|
.grid(row=6, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# Поиск по базам
|
||||||
|
tk.Label(f, text="Поиск баз:").grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w")
|
||||||
|
self.db_search_var = tk.StringVar()
|
||||||
|
self.db_search_var.trace_add("write", lambda *a: self._render_checklist(
|
||||||
|
self.db_inner, self.db_vars, self.db_search_var.get()))
|
||||||
|
tk.Entry(f, textvariable=self.db_search_var)\
|
||||||
|
.grid(row=8, column=0, padx=5, pady=(0, 5), sticky="ew")
|
||||||
|
|
||||||
|
bar = tk.Frame(f)
|
||||||
|
bar.grid(row=9, column=0, padx=5, pady=(0, 3), sticky="w")
|
||||||
|
tk.Button(bar, text="Отметить все",
|
||||||
|
command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), True))\
|
||||||
|
.pack(side="left", padx=(0, 4))
|
||||||
|
tk.Button(bar, text="Снять все",
|
||||||
|
command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), False))\
|
||||||
|
.pack(side="left")
|
||||||
|
|
||||||
|
self.db_container, self.db_inner = self._create_scrollable_checklist(f, height=380)
|
||||||
|
self.db_container.grid(row=10, column=0, padx=5, pady=5, sticky="nsew")
|
||||||
|
f.grid_rowconfigure(10, weight=1)
|
||||||
|
|
||||||
|
def _create_scrollable_checklist(self, parent, width=220, height=180):
|
||||||
|
"""Прокручиваемая область для чекбоксов. Возвращает (container, inner)."""
|
||||||
container = tk.Frame(parent, borderwidth=1, relief="sunken")
|
container = tk.Frame(parent, borderwidth=1, relief="sunken")
|
||||||
canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0)
|
canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0)
|
||||||
scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview)
|
scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview)
|
||||||
@@ -172,7 +230,6 @@ class RemoteFolderSelector(tk.Tk):
|
|||||||
canvas.pack(side="left", fill="both", expand=True)
|
canvas.pack(side="left", fill="both", expand=True)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
# Прокрутка колесом мыши, когда курсор над областью
|
|
||||||
def _on_mousewheel(event):
|
def _on_mousewheel(event):
|
||||||
canvas.yview_scroll(int(-event.delta / 120), "units")
|
canvas.yview_scroll(int(-event.delta / 120), "units")
|
||||||
|
|
||||||
@@ -180,88 +237,189 @@ class RemoteFolderSelector(tk.Tk):
|
|||||||
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
|
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
|
||||||
return container, inner
|
return container, inner
|
||||||
|
|
||||||
def _populate_checklist(self, inner, vars_dict, items):
|
def _load_items(self, vars_dict, items):
|
||||||
"""Пересоздаём чекбоксы в области inner по списку items."""
|
"""Создаём свежий набор BooleanVar по списку items (галочки сбрасываются)."""
|
||||||
for child in inner.winfo_children():
|
|
||||||
child.destroy()
|
|
||||||
vars_dict.clear()
|
vars_dict.clear()
|
||||||
for item in items:
|
for item in items:
|
||||||
var = tk.BooleanVar(value=False)
|
vars_dict[item] = tk.BooleanVar(value=False)
|
||||||
vars_dict[item] = var
|
|
||||||
tk.Checkbutton(inner, text=item, variable=var, anchor="w").pack(fill="x", anchor="w")
|
|
||||||
|
|
||||||
def _set_all(self, vars_dict, value):
|
def _render_checklist(self, inner, vars_dict, filter_text="", on_click=None):
|
||||||
"""Отмечаем (True) либо снимаем (False) все чекбоксы."""
|
"""Рисуем чекбоксы, показывая только элементы, подходящие под фильтр.
|
||||||
for var in vars_dict.values():
|
Переменные (галочки) сохраняются между перерисовками — состояние не теряется."""
|
||||||
|
for child in inner.winfo_children():
|
||||||
|
child.destroy()
|
||||||
|
ft = (filter_text or "").lower()
|
||||||
|
for name, var in vars_dict.items():
|
||||||
|
if ft and ft not in name.lower():
|
||||||
|
continue
|
||||||
|
cmd = (lambda n=name: on_click(n)) if on_click else None
|
||||||
|
tk.Checkbutton(inner, text=name, variable=var, anchor="w", command=cmd)\
|
||||||
|
.pack(fill="x", anchor="w")
|
||||||
|
# После перерисовки (например, при вводе в поиск) возвращаем прокрутку наверх,
|
||||||
|
# иначе список остаётся проскроллен вниз и результат поиска не виден.
|
||||||
|
inner.master.yview_moveto(0)
|
||||||
|
|
||||||
|
def _set_all(self, vars_dict, filter_text, value):
|
||||||
|
"""Отмечаем/снимаем все чекбоксы, попадающие под текущий фильтр."""
|
||||||
|
ft = (filter_text or "").lower()
|
||||||
|
for name, var in vars_dict.items():
|
||||||
|
if ft and ft not in name.lower():
|
||||||
|
continue
|
||||||
var.set(value)
|
var.set(value)
|
||||||
|
|
||||||
def _get_checked(self, vars_dict):
|
def _get_checked(self, vars_dict):
|
||||||
"""Возвращаем список отмеченных элементов."""
|
|
||||||
return [name for name, var in vars_dict.items() if var.get()]
|
return [name for name, var in vars_dict.items() if var.get()]
|
||||||
|
|
||||||
|
def _toggle_zip(self):
|
||||||
|
state = "normal" if self.zip_enabled.get() else "disabled"
|
||||||
|
self.zip_path_entry.config(state=state)
|
||||||
|
self.zip_browse_btn.config(state=state)
|
||||||
|
|
||||||
|
def _browse_zip(self):
|
||||||
|
path = filedialog.askdirectory(title="Каталог с ZIP-профилями пользователей")
|
||||||
|
if path:
|
||||||
|
self.zip_path_var.set(os.path.normpath(path))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- Загрузка
|
||||||
|
|
||||||
def load_users(self):
|
def load_users(self):
|
||||||
"""Загружаем пользователей с указанного IP-адреса или локального ПК."""
|
"""Загружаем список пользователей с локального ПК или с удалённого по IP."""
|
||||||
self.ip_address = self.ip_entry.get()
|
self.ip_address = self.ip_entry.get().strip()
|
||||||
|
if not self.ip_address:
|
||||||
if not self.ip_address: # Если IP не указан, используем локальный ПК
|
base = r"C:\Users"
|
||||||
local_path = r"C:\Users"
|
|
||||||
self.load_local_users(local_path)
|
|
||||||
else:
|
else:
|
||||||
|
base = f"\\\\{self.ip_address}\\C$\\Users"
|
||||||
try:
|
try:
|
||||||
# Используем указанный IP для подключения к удалённому ПК
|
folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))]
|
||||||
remote_path = f"\\\\{self.ip_address}\\C$\\Users"
|
|
||||||
self.load_remote_users(remote_path)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", f"Could not load users: {str(e)}")
|
messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
def load_local_users(self, path):
|
self._load_items(self.user_vars, folders)
|
||||||
"""Загружаем список пользователей с локального ПК."""
|
self._render_checklist(self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click)
|
||||||
try:
|
|
||||||
folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
|
|
||||||
self._populate_checklist(self.user_inner, self.user_vars, folders)
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Error", f"Could not load local users: {str(e)}")
|
|
||||||
|
|
||||||
def load_remote_users(self, remote_path):
|
# Сбрасываем панель подключённых баз — она относилась к прежнему источнику
|
||||||
"""Загружаем список пользователей с удалённого ПК."""
|
self.active_user = None
|
||||||
try:
|
self.conn_vars.clear()
|
||||||
folders = [f for f in os.listdir(remote_path) if os.path.isdir(os.path.join(remote_path, f))]
|
self._render_checklist(self.conn_inner, self.conn_vars, "")
|
||||||
self._populate_checklist(self.user_inner, self.user_vars, folders)
|
self.conn_label.config(text="Подключённые базы: (выберите пользователя)")
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Error", f"Could not load remote users: {str(e)}")
|
|
||||||
|
|
||||||
def load_databases(self):
|
def load_databases(self):
|
||||||
"""Подключаемся к SQL серверу и загружаем список баз данных."""
|
"""Подключаемся к SQL-серверу и загружаем список баз."""
|
||||||
server = self.server_entry.get()
|
server = self.server_entry.get().strip()
|
||||||
login = self.login_entry.get()
|
login = self.login_entry.get()
|
||||||
password = self.password_entry.get()
|
password = self.password_entry.get()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};"
|
connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};"
|
||||||
connection = pyodbc.connect(connection_string)
|
connection = pyodbc.connect(connection_string)
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cursor.execute("SELECT name FROM sys.databases")
|
cursor.execute("SELECT name FROM sys.databases")
|
||||||
databases = cursor.fetchall()
|
databases = cursor.fetchall()
|
||||||
|
|
||||||
# Скрываем служебные базы SQL Server, они не являются базами 1С
|
|
||||||
system_dbs = {"master", "tempdb", "model", "msdb"}
|
|
||||||
names = [db[0] for db in databases if db[0].lower() not in system_dbs]
|
|
||||||
self._populate_checklist(self.db_inner, self.db_vars, names)
|
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
connection.close()
|
connection.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", f"Could not connect to SQL server: {str(e)}")
|
messagebox.showerror("Ошибка", f"Не удалось подключиться к SQL-серверу:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Служебные базы SQL Server скрываем — это не базы 1С
|
||||||
|
system_dbs = {"master", "tempdb", "model", "msdb"}
|
||||||
|
names = [db[0] for db in databases if db[0].lower() not in system_dbs]
|
||||||
|
self._load_items(self.db_vars, names)
|
||||||
|
self._render_checklist(self.db_inner, self.db_vars, self.db_search_var.get())
|
||||||
|
|
||||||
|
def on_user_click(self, user):
|
||||||
|
"""Клик по пользователю — делаем его активным и показываем его подключённые базы."""
|
||||||
|
self.load_connected_bases(user)
|
||||||
|
|
||||||
|
def load_connected_bases(self, user):
|
||||||
|
"""Читаем ibases.v8i пользователя и показываем список его баз в панели."""
|
||||||
|
self.active_user = user
|
||||||
|
self.ip_address = self.ip_entry.get().strip()
|
||||||
|
path = self.get_ibases_path(user)
|
||||||
|
text = self._read_ibases(path)
|
||||||
|
_, sections = self._parse_ibases(text)
|
||||||
|
names = [name for name, _ in sections]
|
||||||
|
self._load_items(self.conn_vars, names)
|
||||||
|
self._render_checklist(self.conn_inner, self.conn_vars, "")
|
||||||
|
suffix = "" if names else " (баз нет)"
|
||||||
|
self.conn_label.config(text=f"Подключённые базы: {user}{suffix}")
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- Пути
|
||||||
|
|
||||||
def get_ibases_path(self, user):
|
def get_ibases_path(self, user):
|
||||||
"""Возвращаем путь к ibases.v8i выбранного пользователя (локально либо по IP)."""
|
"""Путь к ibases.v8i пользователя (локально либо по IP)."""
|
||||||
if not self.ip_address:
|
if not self.ip_address:
|
||||||
base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
|
base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
|
||||||
else:
|
else:
|
||||||
base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
|
base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart"
|
||||||
return os.path.join(base_path, "ibases.v8i")
|
return os.path.join(base_path, "ibases.v8i")
|
||||||
|
|
||||||
|
def get_zip_ibases_path(self, user):
|
||||||
|
"""Путь к ibases.v8i внутри ZIP-профиля пользователя.
|
||||||
|
Папку профиля ищем автопоиском: <user> либо <user>.* (например <user>.V6).
|
||||||
|
Возвращаем None, если корень недоступен или профиль не найден."""
|
||||||
|
root = self.zip_path_var.get().strip()
|
||||||
|
if not root or not os.path.isdir(root):
|
||||||
|
return None
|
||||||
|
u = user.lower()
|
||||||
|
candidates = []
|
||||||
|
try:
|
||||||
|
for name in os.listdir(root):
|
||||||
|
if not os.path.isdir(os.path.join(root, name)):
|
||||||
|
continue
|
||||||
|
low = name.lower()
|
||||||
|
if low == u or low.startswith(u + "."):
|
||||||
|
candidates.append(name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Приоритет: точное <user>.V6, затем любой суффикс .*, затем совпадение без суффикса
|
||||||
|
def score(n):
|
||||||
|
low = n.lower()
|
||||||
|
if low == u + ".v6":
|
||||||
|
return 0
|
||||||
|
if "." in low:
|
||||||
|
return 1
|
||||||
|
return 2
|
||||||
|
candidates.sort(key=score)
|
||||||
|
chosen = candidates[0]
|
||||||
|
return os.path.join(root, chosen, "AppData", "Roaming", "1C", "1CEStart", "ibases.v8i")
|
||||||
|
|
||||||
|
# ------------------------------------------------------- Чтение/запись v8i
|
||||||
|
|
||||||
|
def _read_ibases(self, path):
|
||||||
|
"""Читаем ibases.v8i, устойчиво к кодировке (UTF-8 с BOM / без / cp1251)."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return ""
|
||||||
|
for enc in ("utf-8-sig", "cp1251"):
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding=enc) as f:
|
||||||
|
return f.read()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def _parse_ibases(self, text):
|
||||||
|
"""Разбираем содержимое на (преамбула, [(имя_базы, блок_текста), ...]).
|
||||||
|
Секция начинается со строки вида [ИмяБазы]."""
|
||||||
|
header_re = re.compile(r"^\s*\[(.+?)\]\s*$")
|
||||||
|
preamble = []
|
||||||
|
sections = [] # список [имя, [строки]]
|
||||||
|
current = None
|
||||||
|
for line in text.splitlines(keepends=True):
|
||||||
|
m = header_re.match(line)
|
||||||
|
if m:
|
||||||
|
current = [m.group(1), [line]]
|
||||||
|
sections.append(current)
|
||||||
|
elif current is None:
|
||||||
|
preamble.append(line)
|
||||||
|
else:
|
||||||
|
current[1].append(line)
|
||||||
|
return "".join(preamble), [(name, "".join(ls)) for name, ls in sections]
|
||||||
|
|
||||||
def build_base_entry(self, base, server):
|
def build_base_entry(self, base, server):
|
||||||
"""Формируем запись одной базы для ibases.v8i."""
|
"""Формируем запись одной базы для ibases.v8i."""
|
||||||
return (
|
return (
|
||||||
@@ -280,74 +438,142 @@ class RemoteFolderSelector(tk.Tk):
|
|||||||
"DisableLocalSpeechToText=0\n"
|
"DisableLocalSpeechToText=0\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_database_to_ibases(self):
|
def _add_entries_to_file(self, ibases_path, bases, server):
|
||||||
"""Добавляем все выбранные базы всем выбранным пользователям."""
|
"""Добавляем базы в один файл ibases.v8i (с фильтром дублей).
|
||||||
# Актуализируем режим (локально / по IP) на момент добавления
|
Возвращаем (добавлено, пропущено)."""
|
||||||
self.ip_address = self.ip_entry.get().strip()
|
base_dir = os.path.dirname(ibases_path)
|
||||||
|
if not os.path.exists(base_dir):
|
||||||
|
os.makedirs(base_dir)
|
||||||
|
|
||||||
|
existing = self._read_ibases(ibases_path)
|
||||||
|
entries = []
|
||||||
|
added = skipped = 0
|
||||||
|
for base in bases:
|
||||||
|
connect_line = f'Connect=Srvr="{server}";Ref="{base}";'
|
||||||
|
if connect_line in existing or any(connect_line in e for e in entries):
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
entries.append(self.build_base_entry(base, server))
|
||||||
|
added += 1
|
||||||
|
if entries:
|
||||||
|
with open(ibases_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write("".join(entries))
|
||||||
|
return added, skipped
|
||||||
|
|
||||||
|
def _delete_entries_from_file(self, ibases_path, base_names):
|
||||||
|
"""Удаляем указанные базы из одного файла ibases.v8i. Возвращаем число удалённых."""
|
||||||
|
if not os.path.exists(ibases_path):
|
||||||
|
return 0
|
||||||
|
text = self._read_ibases(ibases_path)
|
||||||
|
preamble, sections = self._parse_ibases(text)
|
||||||
|
target = {b.lower() for b in base_names}
|
||||||
|
kept = [blk for (name, blk) in sections if name.lower() not in target]
|
||||||
|
removed = len(sections) - len(kept)
|
||||||
|
if removed:
|
||||||
|
with open(ibases_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(preamble + "".join(kept))
|
||||||
|
return removed
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- Действия
|
||||||
|
|
||||||
|
def add_database_to_ibases(self):
|
||||||
|
"""Добавляем отмеченные базы всем отмеченным пользователям (+ ZIP по галочке)."""
|
||||||
|
self.ip_address = self.ip_entry.get().strip()
|
||||||
users = self._get_checked(self.user_vars)
|
users = self._get_checked(self.user_vars)
|
||||||
bases = self._get_checked(self.db_vars)
|
bases = self._get_checked(self.db_vars)
|
||||||
server = self.server_entry.get().strip()
|
server = self.server_entry.get().strip()
|
||||||
|
|
||||||
if not users:
|
if not users:
|
||||||
messagebox.showerror("Error", "Не отмечен ни один пользователь!")
|
messagebox.showerror("Ошибка", "Не отмечен ни один пользователь!")
|
||||||
return
|
return
|
||||||
if not bases:
|
if not bases:
|
||||||
messagebox.showerror("Error", "Не отмечена ни одна база!")
|
messagebox.showerror("Ошибка", "Не отмечена ни одна база!")
|
||||||
return
|
return
|
||||||
if not server:
|
if not server:
|
||||||
messagebox.showerror("Error", "Не указан SQL-сервер!")
|
messagebox.showerror("Ошибка", "Не указан SQL-сервер!")
|
||||||
return
|
return
|
||||||
|
|
||||||
added = 0
|
use_zip = self.zip_enabled.get()
|
||||||
skipped = 0
|
added = skipped = zadded = zskipped = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
try:
|
try:
|
||||||
ibases_path = self.get_ibases_path(user)
|
a, s = self._add_entries_to_file(self.get_ibases_path(user), bases, server)
|
||||||
base_dir = os.path.dirname(ibases_path)
|
added += a
|
||||||
|
skipped += s
|
||||||
# Создаём каталог, если его нет
|
|
||||||
if not os.path.exists(base_dir):
|
|
||||||
os.makedirs(base_dir)
|
|
||||||
|
|
||||||
# Читаем текущее содержимое файла для проверки дублей
|
|
||||||
existing = ""
|
|
||||||
if os.path.exists(ibases_path):
|
|
||||||
with open(ibases_path, "r", encoding="utf-8") as f:
|
|
||||||
existing = f.read()
|
|
||||||
|
|
||||||
entries_to_add = []
|
|
||||||
for base in bases:
|
|
||||||
# Дубль определяем по строке подключения (сервер + имя базы)
|
|
||||||
connect_line = f'Connect=Srvr="{server}";Ref="{base}";'
|
|
||||||
already_present = connect_line in existing or any(
|
|
||||||
connect_line in e for e in entries_to_add
|
|
||||||
)
|
|
||||||
if already_present:
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
entries_to_add.append(self.build_base_entry(base, server))
|
|
||||||
added += 1
|
|
||||||
|
|
||||||
# Дописываем новые записи одним заходом
|
|
||||||
if entries_to_add:
|
|
||||||
with open(ibases_path, "a", encoding="utf-8") as f:
|
|
||||||
for entry in entries_to_add:
|
|
||||||
f.write(entry)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append(f"{user}: {e}")
|
errors.append(f"{user}: {e}")
|
||||||
|
if use_zip:
|
||||||
|
zp = self.get_zip_ibases_path(user)
|
||||||
|
if zp is None:
|
||||||
|
errors.append(f"{user}: ZIP-профиль не найден")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
a, s = self._add_entries_to_file(zp, bases, server)
|
||||||
|
zadded += a
|
||||||
|
zskipped += s
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{user} (ZIP): {e}")
|
||||||
|
|
||||||
|
# Обновим панель подключённых баз, если активный пользователь среди затронутых
|
||||||
|
if self.active_user in users:
|
||||||
|
self.load_connected_bases(self.active_user)
|
||||||
|
|
||||||
# Итоговый отчёт
|
|
||||||
summary = (
|
summary = (
|
||||||
f"Пользователей: {len(users)}, баз: {len(bases)}\n"
|
f"Пользователей: {len(users)}, баз отмечено: {len(bases)}\n"
|
||||||
f"Добавлено записей: {added}\n"
|
f"Локально — добавлено: {added}, пропущено (уже есть): {skipped}"
|
||||||
f"Пропущено (уже есть): {skipped}"
|
|
||||||
)
|
)
|
||||||
|
if use_zip:
|
||||||
|
summary += f"\nZIP-профили — добавлено: {zadded}, пропущено: {zskipped}"
|
||||||
|
self._report(summary, errors)
|
||||||
|
|
||||||
|
def delete_connected_bases(self):
|
||||||
|
"""Удаляем отмеченные подключённые базы у активного пользователя (+ ZIP по галочке)."""
|
||||||
|
user = self.active_user
|
||||||
|
if not user:
|
||||||
|
messagebox.showerror("Ошибка", "Сначала выберите пользователя слева.")
|
||||||
|
return
|
||||||
|
bases = self._get_checked(self.conn_vars)
|
||||||
|
if not bases:
|
||||||
|
messagebox.showerror("Ошибка", "Не отмечена ни одна база для удаления!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"Подтверждение",
|
||||||
|
f"Удалить {len(bases)} баз(ы) у пользователя «{user}»?\n\n" + "\n".join(bases)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ip_address = self.ip_entry.get().strip()
|
||||||
|
use_zip = self.zip_enabled.get()
|
||||||
|
removed_local = removed_zip = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
removed_local = self._delete_entries_from_file(self.get_ibases_path(user), bases)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{user}: {e}")
|
||||||
|
if use_zip:
|
||||||
|
zp = self.get_zip_ibases_path(user)
|
||||||
|
if zp is None:
|
||||||
|
errors.append(f"{user}: ZIP-профиль не найден")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
removed_zip = self._delete_entries_from_file(zp, bases)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"{user} (ZIP): {e}")
|
||||||
|
|
||||||
|
self.load_connected_bases(user)
|
||||||
|
|
||||||
|
summary = f"Пользователь: {user}\nУдалено локально: {removed_local}"
|
||||||
|
if use_zip:
|
||||||
|
summary += f"\nУдалено в ZIP-профиле: {removed_zip}"
|
||||||
|
self._report(summary, errors)
|
||||||
|
|
||||||
|
def _report(self, summary, errors):
|
||||||
if errors:
|
if errors:
|
||||||
summary += "\n\nОшибки:\n" + "\n".join(errors)
|
messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors))
|
||||||
messagebox.showwarning("Готово с ошибками", summary)
|
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo("Готово", summary)
|
messagebox.showinfo("Готово", summary)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
Reference in New Issue
Block a user