Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ This update focuses on personalization and user experience.
### ⚡ Also in this update

- Minor UI improvements and bug fixes for a more stable experience.
- Improved session stability after idle time: document lists and search results no longer disappear if a reload or search request fails.
- Main window startup loading was moved off the UI thread to reduce freezes during the initial data fetch.
- Authentication flows now fail safely if the system keyring or session storage is unavailable.
- API client now correctly handles empty successful responses such as `204 No Content`.
- Logging out now explicitly disables auto-login for the current profile.

---

Expand Down
5 changes: 5 additions & 0 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
### ⚡ Также в этом обновлении

- Улучшения интерфейса и исправления ошибок для более стабильной работы.
- Улучшена стабильность сессии после простоя: список документов и результаты поиска больше не исчезают, если запрос обновления или поиска завершился ошибкой.
- Начальная загрузка главного окна перенесена из UI-потока в фоновый, чтобы уменьшить подвисания при первом получении данных.
- Сценарии авторизации теперь корректно завершаются ошибкой, если системное хранилище ключей или сессии недоступно.
- API-клиент теперь корректно обрабатывает успешные пустые ответы, например `204 No Content`.
- При выходе из аккаунта теперь явно отключается auto-login для текущего профиля.

---

Expand Down
4 changes: 3 additions & 1 deletion api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,4 +537,6 @@ def _request(self, method: str, url: str, timeout: int = 10, **kwargs) -> dict:
"""Generic method for making API requests."""
request = self.session.request(method, url, timeout=timeout, **kwargs)
request.raise_for_status()
return request.json()
if request.status_code == 204 or not request.content:
return {}
return request.json()
61 changes: 32 additions & 29 deletions modules/auth/mvc/auth_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ def login_user(self, data: dict) -> None:
Args:
data (dict): The user data received from the API upon login.
"""
# Save user data
auto_login = self.view.login_page.get_auto_login_state()
user_id = self.model.save_user(user_data=data, auto_login=auto_login)
try:
auto_login = self.view.login_page.get_auto_login_state()
user_id = self.model.save_user(user_data=data, auto_login=auto_login)

# Clear lineedits
self.view.login_page.clear_lineedits()

# Switch to main window
self.login_successful.emit("auth", user_id)
self.view.login_page.clear_lineedits()
self.login_successful.emit("auth", user_id)
except Exception as e:
msg = get_friendly_error_message(e)
NotificationService().show_toast("error", "Ошибка входа", msg)



Expand All @@ -117,20 +117,21 @@ def signup_user(self, user_data: dict) -> None:
Args:
user_data (dict): The user data received from the API upon signup.
"""
# Save user data
auto_login = self.view.signup_page.get_auto_login_state()
user_id = self.model.save_user(user_data=user_data, auto_login=auto_login)

# Clear lineedits
self.view.signup_page.clear_lineedits()

NotificationService().show_toast(
notification_type="success",
title="Регистрация успешна",
message="Аккаунт создан. Добро пожаловать!"
)
# Switch to main window
self.login_successful.emit("auth", user_id)
try:
auto_login = self.view.signup_page.get_auto_login_state()
user_id = self.model.save_user(user_data=user_data, auto_login=auto_login)

self.view.signup_page.clear_lineedits()

NotificationService().show_toast(
notification_type="success",
title="Регистрация успешна",
message="Аккаунт создан. Добро пожаловать!"
)
self.login_successful.emit("auth", user_id)
except Exception as e:
msg = get_friendly_error_message(e)
NotificationService().show_toast("error", "Ошибка регистрации", msg)


def signup(self, data: dict, email: str, password: str) -> None:
Expand Down Expand Up @@ -203,13 +204,15 @@ def switch_to_reset_password_page(self, data: dict) -> None:
Args:
data (dict): Data containing the reset token from the API.
"""
# Save token
self.model.save_token(token_name="reset_token", token="reset_token", data=data)
try:
self.model.save_token(token_name="reset_token", token="reset_token", data=data)

# Switch to change password page
page = self.view.pages.get("change_password_change_page", None)
if page:
self.view.switch_page(page=page)
page = self.view.pages.get("change_password_change_page", None)
if page:
self.view.switch_page(page=page)
except Exception as e:
msg = get_friendly_error_message(e)
NotificationService().show_toast("error", "Ошибка восстановления", msg)


def open_email_confirm_modal_window(self, data, email: str) -> None:
Expand Down Expand Up @@ -653,4 +656,4 @@ def _worker_error(self, exception: Exception) -> None:
notification_type="error",
title="Ошибка",
message=message
)
)
18 changes: 16 additions & 2 deletions modules/auth/mvc/auth_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ def delete_file(file_path: Path) -> None:
if file_path.exists():
file_path.unlink()

def disable_auto_login(user_id: int) -> None:
profile_path = self.APP_DIR / "Profiles" / f"user_data_{user_id}.json"
profile_data = read_json(profile_path)
if not isinstance(profile_data, dict):
return

profile_data["auto_login"] = False
with open(profile_path, "w", encoding="utf-8") as f:
json.dump(profile_data, f, indent=4, ensure_ascii=False)

# Get last user id
last_logged_data = read_json(self.LOCAL_DIR_LAST_LOGGED)
if not last_logged_data:
Expand All @@ -276,6 +286,8 @@ def delete_file(file_path: Path) -> None:
except keyring_errors.PasswordDeleteError:
logging.info(f"Tokens for user_id {user_id} not found in keyring, skipping deletion.")

disable_auto_login(user_id)

# Delete the last logged user file to disable auto-login on next start
delete_file(self.LOCAL_DIR_LAST_LOGGED)
logging.info("Last logged user file deleted to disable auto-login. User is fully logged out.")
Expand Down Expand Up @@ -376,7 +388,9 @@ def save_token(self, token_name: str, token: str, data: dict) -> None:
password=data.get(token, None)
)

except keyring_errors.PasswordSetError:
except keyring_errors.PasswordSetError as e:
logging.error(msg="PasswordSetError", exc_info=True)
raise RuntimeError("Не удалось сохранить данные сессии в системное хранилище.") from e
except Exception as e:
logging.error(msg=e, exc_info=True)
logging.error(msg=e, exc_info=True)
raise RuntimeError("Не удалось сохранить данные сессии в системное хранилище.") from e
128 changes: 86 additions & 42 deletions modules/main/mvc/main_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(
self.current_documents = []
self.current_search_worker = None
self.current_load_worker = None
self.initial_load_worker = None
self.active_workers = set()
self.is_updating_data = False

Expand Down Expand Up @@ -788,6 +789,7 @@ def _on_search_error(self, error: Exception) -> None:
"""Handles search errors."""
if self.sender() != self.current_search_worker:
return
self.current_search_worker = None
self._handle_error(error, "Ошибка поиска")

def _cleanup_worker(self) -> None:
Expand All @@ -804,53 +806,86 @@ def _cleanup_worker(self) -> None:

def _init_ui(self) -> None:
"""Initializes the UI with default data."""
self._load_sidebar_data()
self.view.set_profile_mode(self.mode)

# Load and apply settings
if self.settings_manager:
search_filters = self.settings_manager.get_setting("search_filters")
if search_filters:
self.view.set_search_filters(search_filters)
self._load_initial_data()

# Set user data
if self.mode == "auth":
user_data = self.model.get_user_data()

if user_data:
username = user_data.get("username")
department_name = user_data.get("department")
self.view.set_username(name=username if username else user_data.get("email"))
self.view.set_user_department(dept=department_name if department_name else "Отдел не выбран")

# Find the user's department ID from the name
user_dept_id = None
if department_name:
for dept in self.model.departments:
if dept.get("name") == department_name:
user_dept_id = dept.get("id")
break

# If a department is found, set it as current by triggering selection
if user_dept_id:
# Use a timer to ensure the UI is ready for selection.
# This will trigger the _on_department_selected slot,
# which will then update the model and load the data.
QTimer.singleShot(50, lambda: self.view.select_department(user_dept_id))
# If user has no department, select the first one in the list
elif self.model.departments:
first_dept_id = self.model.departments[0].get("id")
if first_dept_id:
QTimer.singleShot(50, lambda: self.view.select_department(first_dept_id))
def _load_initial_data(self) -> None:
"""Loads the initial sidebar and profile data without blocking the UI thread."""
worker = APIWorker(self.model.load_initial_data)
self.initial_load_worker = worker
self.active_workers.add(worker)
worker.finished.connect(self._on_initial_data_loaded)
worker.error.connect(self._on_initial_data_error)
worker.finished.connect(self._cleanup_worker)
worker.error.connect(self._cleanup_worker)
worker.start()

def _on_initial_data_loaded(self, data: dict) -> None:
if self.sender() != self.initial_load_worker:
return

self.initial_load_worker = None
self.model.departments = data.get("departments", [])
self.model.categories = data.get("categories", [])
self.model.current_department_id = self.model.departments[0]["id"] if self.model.departments else None
self._load_sidebar_data()

if self.mode == "auth":
self._apply_user_data(data.get("user_data"))
else:
self.view.set_username("Гость")
self.view.set_user_department("Войдите в аккаунт")
if self.model.departments:
first_dept_id = self.model.departments[0].get("id")
if first_dept_id:
QTimer.singleShot(50, lambda: self.view.select_department(first_dept_id))

# Set documents data
self._update_create_category_state()
self._update_create_document_state()

def _on_initial_data_error(self, error: Exception) -> None:
if self.sender() != self.initial_load_worker:
return

self.initial_load_worker = None
if self.mode == "auth":
self.view.set_username("Пользователь")
self.view.set_user_department("Не удалось загрузить данные")
else:
self.view.set_username("Гость")
self.view.set_user_department("Войдите в аккаунт")
self._handle_error(error, "Ошибка загрузки данных")

def _apply_user_data(self, user_data: dict | None) -> None:
"""Applies the loaded user data to the UI and restores sidebar selection."""
if not user_data:
return

username = user_data.get("username")
department_name = user_data.get("department")
self.view.set_username(name=username if username else user_data.get("email"))
self.view.set_user_department(dept=department_name if department_name else "Отдел не выбран")

user_dept_id = None
if department_name:
for dept in self.model.departments:
if dept.get("name") == department_name:
user_dept_id = dept.get("id")
break

if user_dept_id:
QTimer.singleShot(50, lambda: self.view.select_department(user_dept_id))
elif self.model.departments:
first_dept_id = self.model.departments[0].get("id")
if first_dept_id:
QTimer.singleShot(50, lambda: self.view.select_department(first_dept_id))


def _setup_connections(self) -> None:
"""Sets up signal-slot connections."""
Expand Down Expand Up @@ -954,20 +989,26 @@ def _update_categories_list(self) -> None:
def _update_documents_list(self) -> None:
"""Updates the documents list based on the current category."""
self.model.current_document_id = None

# Reset pagination

if self.model.current_category_id is None:
self.offset = 0
self.has_more = False
self.current_documents = []
self.search_results_all = []
self.pending_documents_to_add = []
self.view.clear_documents_table()
self.view.set_finded_counter(0)
self.view.set_active_search_tags([])
self.view.set_export_print_enabled(False)
return

self.offset = 0
self.has_more = True
self.current_documents = []
self.view.clear_documents_table()
self.view.set_finded_counter(0)
self.view.set_active_search_tags([])
self.view.set_export_print_enabled(False)

# Reset loading state to ensure we can load new data

self.is_loading = False
self.current_load_worker = None

self.pending_documents_to_add = []

self._load_more_documents()


Expand Down Expand Up @@ -1013,6 +1054,7 @@ def _on_load_more_finished(self, docs: list) -> None:

self.is_loading = False
self.current_load_worker = None
self.search_results_all = []

# Prevent updating UI if search is active (stale request)
if self.view.get_search_text():
Expand All @@ -1029,6 +1071,7 @@ def _on_load_more_finished(self, docs: list) -> None:
# Instead of one large, blocking update, we add the data in chunks
# to keep the UI responsive.
self.view.clear_documents_table()
self.view.set_active_search_tags([])
self.pending_documents_to_add = list(self.current_documents)
# Start the chunked update. Use a single shot timer to yield to the event loop.
QTimer.singleShot(0, self._add_document_chunk)
Expand Down Expand Up @@ -1059,6 +1102,7 @@ def _on_load_more_error(self, error: Exception) -> None:
return

self.is_loading = False
self.current_load_worker = None
self._handle_error(error, "Ошибка загрузки документов")


Expand Down Expand Up @@ -1194,4 +1238,4 @@ def _handle_error(self, e: Exception, title: str = "Ошибка") -> None:
notification_type="error",
title=title,
message=msg
)
)
Loading
Loading