Skip to content

Commit aa3aba4

Browse files
authored
Merge pull request #25 from pntech-dev/hotfix
Enhance stability and error handling across various components
2 parents ba47ddb + 9cd4d59 commit aa3aba4

26 files changed

Lines changed: 746 additions & 93 deletions

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,11 @@ Allows authorized users to create, edit, and manage document pages. Support for
3535

3636
---
3737

38-
## 💡 What's New (v0.2.1) - Stability & Profile Improvements
38+
## 🛠 What's New (v0.2.16)
3939

40-
This patch release improves reliability and profile editing behavior.
41-
42-
### 🛠 Improved
43-
44-
- Fixed a crash when opening the profile window for accounts without a filled display name.
45-
- Improved reliability of profile data updates after backend deployment updates.
46-
- Improved handling of empty values in document and page names.
47-
- Improved stability of search and document loading in edge cases.
40+
- Improved toolbar behavior in document edit/create windows.
41+
- `Duplicate` and `Delete` now only mark a document as changed when pages were actually modified.
42+
- Reduced accidental “unsaved changes” state when pressing toolbar actions with no selected pages.
4843

4944
---
5045

README_RU.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,11 @@
3535

3636
---
3737

38-
## 💡 Что нового (v0.2.1) - Стабильность и профиль
38+
## 🛠 Что нового (v0.2.16)
3939

40-
Это патч-обновление повышает надежность и улучшает поведение при работе с профилем.
41-
42-
### 🛠 Улучшено
43-
44-
- Исправлен сбой при открытии профиля для аккаунтов без заполненного отображаемого имени.
45-
- Повышена надежность обновления данных профиля после обновлений backend-сервиса.
46-
- Улучшена обработка пустых значений в названиях документов и страниц.
47-
- Повышена стабильность поиска и загрузки документов в пограничных сценариях.
40+
- Улучшено поведение панели инструментов в окнах создания и редактирования документа.
41+
- `Дублировать` и `Удалить` теперь помечают документ как изменённый только при реальном изменении страниц.
42+
- Снижен риск ложного состояния “есть несохранённые изменения” при нажатии действий без выбранных страниц.
4843

4944
---
5045

api/api_client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,10 @@ def _request(self, method: str, url: str, timeout: int = 10, **kwargs) -> dict:
539539
request.raise_for_status()
540540
if request.status_code == 204 or not request.content:
541541
return {}
542-
return request.json()
542+
try:
543+
return request.json()
544+
except ValueError:
545+
# Some backends may return 200 with an empty/whitespace body.
546+
if not (request.text or "").strip():
547+
return {}
548+
raise

app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
os.environ.setdefault("QT_SCALE_FACTOR_ROUNDING_POLICY", "PassThrough")
2828

2929

30-
APP_VERSION = "0.2.1"
30+
APP_VERSION = "0.2.16"
3131

3232
class Application:
3333
"""

core/updater.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class UpdateDownloader(QThread):
7878
"""Thread for downloading the update file."""
7979
progress = pyqtSignal(int, int) # downloaded_bytes, total_bytes
8080
finished = pyqtSignal(str) # путь к скачанному файлу
81+
canceled = pyqtSignal()
8182
error = pyqtSignal(str)
8283

8384
def __init__(self, url: str, expected_size: int = 0):
@@ -91,6 +92,7 @@ def stop(self):
9192
self._is_running = False
9293

9394
def run(self):
95+
path = None
9496
try:
9597
# Use requests context manager to ensure connection closure
9698
with requests.get(self.url, stream=True, timeout=30, allow_redirects=True) as response:
@@ -121,6 +123,7 @@ def run(self):
121123
os.remove(path)
122124
except OSError:
123125
pass
126+
self.canceled.emit()
124127
return
125128

126129
if chunk:
@@ -138,10 +141,24 @@ def run(self):
138141
self.progress.emit(downloaded_size, total_size)
139142
last_emitted_size = downloaded_size
140143

141-
if self._is_running:
142-
self.finished.emit(path)
144+
if not self._is_running:
145+
try:
146+
os.remove(path)
147+
except OSError:
148+
pass
149+
self.canceled.emit()
150+
return
151+
152+
self.finished.emit(path)
143153

144154
except Exception as e:
155+
if path and not self._is_running:
156+
try:
157+
os.remove(path)
158+
except OSError:
159+
pass
160+
self.canceled.emit()
161+
return
145162
logger.error(f"Download failed: {e}")
146163
self.error.emit(str(e))
147164

@@ -193,6 +210,7 @@ def _start_download(self, url, size):
193210
self._downloader = UpdateDownloader(url, size)
194211
self._downloader.progress.connect(self.progress_dialog.set_progress)
195212
self._downloader.finished.connect(self._on_download_finished)
213+
self._downloader.canceled.connect(self._on_download_canceled)
196214
self._downloader.error.connect(self._on_download_error)
197215

198216
self.progress_dialog.canceled.connect(self._downloader.stop)
@@ -201,13 +219,20 @@ def _start_download(self, url, size):
201219

202220
def _on_download_error(self, error_msg):
203221
self.progress_dialog.close()
222+
self._downloader = None
204223
# If main window is not created yet (check on startup), NotificationService won't work
205224
if NotificationService().main_window:
206225
NotificationService().show_toast("error", "Ошибка", f"Ошибка скачивания: {error_msg}")
207226
else:
208227
# Use standard QMessageBox as a fallback
209228
QMessageBox.critical(self.parent_widget, "Ошибка", f"Ошибка скачивания:\n{error_msg}")
210229

230+
def _on_download_canceled(self):
231+
self.progress_dialog.close()
232+
self._downloader = None
233+
if NotificationService().main_window:
234+
NotificationService().show_toast("info", "Обновление", "Скачивание обновления отменено.")
235+
211236
def _on_download_finished(self, file_path):
212237
# Force set 100% so user sees completion
213238
self.progress_dialog.set_progress(100, 100)
@@ -216,6 +241,7 @@ def _on_download_finished(self, file_path):
216241

217242
def _show_install_confirmation(self, file_path):
218243
self.progress_dialog.close()
244+
self._downloader = None
219245
dialog = InstallConfirmDialog(self.parent_widget)
220246
if dialog.exec_() == QDialog.Accepted:
221247
self._install_update(file_path)

modules/auth/mvc/auth_controller.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ def login_user(self, data: dict) -> None:
9696
try:
9797
auto_login = self.view.login_page.get_auto_login_state()
9898
user_id = self.model.save_user(user_data=data, auto_login=auto_login)
99+
if not isinstance(user_id, int):
100+
NotificationService().show_toast(
101+
"error",
102+
"Ошибка входа",
103+
"Не удалось сохранить сессию. Попробуйте войти снова."
104+
)
105+
return
99106

100107
self.view.login_page.clear_lineedits()
101108
self.login_successful.emit("auth", user_id)
@@ -120,6 +127,13 @@ def signup_user(self, user_data: dict) -> None:
120127
try:
121128
auto_login = self.view.signup_page.get_auto_login_state()
122129
user_id = self.model.save_user(user_data=user_data, auto_login=auto_login)
130+
if not isinstance(user_id, int):
131+
NotificationService().show_toast(
132+
"error",
133+
"Ошибка регистрации",
134+
"Не удалось сохранить сессию. Попробуйте войти снова."
135+
)
136+
return
123137

124138
self.view.signup_page.clear_lineedits()
125139

@@ -154,6 +168,13 @@ def signup(self, data: dict, email: str, password: str) -> None:
154168
message=f"Код подтверждения отправлен на {email}"
155169
)
156170
code = EmailConfirmDialog.get_code(parent=self.auth_window)
171+
if not code:
172+
NotificationService().show_toast(
173+
notification_type="info",
174+
title="Подтверждение отменено",
175+
message="Регистрация не завершена: код подтверждения не введён."
176+
)
177+
return
157178

158179
# Create success callback
159180
success_cb = lambda data: self.signup_user(user_data=data)
@@ -228,6 +249,13 @@ def open_email_confirm_modal_window(self, data, email: str) -> None:
228249
"""
229250
logging.info(f"Reset password data received: {data}")
230251
code = EmailConfirmDialog.get_code(parent=self.auth_window)
252+
if not code:
253+
NotificationService().show_toast(
254+
notification_type="info",
255+
title="Подтверждение отменено",
256+
message="Сброс пароля не завершён: код подтверждения не введён."
257+
)
258+
return
231259

232260
# Create success callback
233261
success_cb = lambda data: self.switch_to_reset_password_page(data=data)
@@ -328,10 +356,12 @@ def on_login_page_lineedits_changed(self) -> None:
328356

329357
# Validate email
330358
if not self.field_validator.validate_email(email=email):
359+
self.view.login_page.update_submit_button_state(state=False)
331360
return
332361

333362
# Validate password
334363
if not self.field_validator.validate_password(password=password):
364+
self.view.login_page.update_submit_button_state(state=False)
335365
return
336366

337367
# Defining login button state (True if both lineedits has text, else False)
@@ -400,10 +430,12 @@ def on_signup_page_lineedits_changed(self) -> None:
400430

401431
# Validate email
402432
if not self.field_validator.validate_email(email=email):
433+
self.view.signup_page.update_submit_button_state(state=False)
403434
return
404435

405436
# Validate password
406437
if not self.field_validator.validate_password(password=password):
438+
self.view.signup_page.update_submit_button_state(state=False)
407439
return
408440

409441
# Check password matching
@@ -491,6 +523,7 @@ def on_reset_password_page_lineedits_changed(self) -> None:
491523

492524
# Validate password
493525
if not self.field_validator.validate_password(password=password):
526+
self.view.forgot_page_reset_password.update_submit_button_state(state=False)
494527
return
495528

496529
# Check password matching

modules/auth/mvc/auth_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ def save_user(self, user_data: dict, auto_login: bool) -> int | None:
138138
return None
139139

140140
user_id = user.get("id", None)
141+
if not isinstance(user_id, int):
142+
logging.error("Cannot save user session: invalid or missing user id.")
143+
return None
141144

142145
# Save access and refresh tokens
143146
self.save_token(token_name=f"access_token_{user_id}", token="access_token", data=user_data)

modules/document_editor/mvc/document_editor_controller.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ def _on_add_page_button_clicked(self) -> None:
129129

130130
def _on_duplicate_page_button_clicked(self) -> None:
131131
"""Handles the duplicate page button click event."""
132-
self.view.duplicate_selected_pages()
133-
self._on_document_data_changed()
132+
changed = self.view.duplicate_selected_pages()
133+
if changed:
134+
self._on_document_data_changed()
135+
self._on_table_selection_changed()
134136

135137

136138
def _on_print_button_clicked(self) -> None:
@@ -250,8 +252,9 @@ def _on_export_button_clicked(self) -> None:
250252

251253
def _on_delete_page_button_clicked(self) -> None:
252254
"""Handles the delete page button click event."""
253-
self.view.delete_selected_pages()
254-
self._on_document_data_changed()
255+
changed = self.view.delete_selected_pages()
256+
if changed:
257+
self._on_document_data_changed()
255258
self._on_table_selection_changed()
256259

257260

modules/document_editor/mvc/document_editor_model.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,12 @@ def save_document(self, data: dict) -> None:
190190

191191
def _get_user_token(self) -> str | None:
192192
last_logged = read_json(self.LOCAL_DIR_LAST_LOGGED)
193-
194-
if not last_logged:
193+
if not isinstance(last_logged, dict):
195194
return None
196195

197-
user_id = last_logged["user_id"]
196+
user_id = last_logged.get("user_id")
197+
if user_id in (None, ""):
198+
return None
198199

199200
access_token = keyring.get_password(
200201
service_name="Documents Exp",
@@ -222,10 +223,12 @@ def _refresh_tokens(self) -> bool:
222223
"""Refreshes tokens using the stored refresh token."""
223224
try:
224225
last_logged = read_json(self.LOCAL_DIR_LAST_LOGGED)
225-
if not last_logged:
226+
if not isinstance(last_logged, dict):
226227
return False
227228

228229
user_id = last_logged.get("user_id")
230+
if user_id in (None, ""):
231+
return False
229232
refresh_token = keyring.get_password("Documents Exp", f"refresh_token_{user_id}")
230233

231234
if not refresh_token:
@@ -237,4 +240,4 @@ def _refresh_tokens(self) -> bool:
237240
return True
238241
except Exception as e:
239242
logging.error(f"Failed to refresh tokens: {e}")
240-
return False
243+
return False

modules/document_editor/mvc/document_editor_view.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,12 @@ def add_new_page(self) -> None:
292292
self.table_view.edit(index)
293293

294294

295-
def delete_selected_pages(self) -> None:
296-
"""Deletes selected or checked pages from the table."""
295+
def delete_selected_pages(self) -> bool:
296+
"""Deletes selected or checked pages from the table.
297+
298+
Returns:
299+
bool: True if any page was deleted, otherwise False.
300+
"""
297301
model = self.table_view.model()
298302
rows_to_delete = set()
299303

@@ -313,10 +317,15 @@ def delete_selected_pages(self) -> None:
313317
model.removeRow(row)
314318

315319
self.table_view.clearSelection()
320+
return bool(rows_to_delete)
316321

317322

318-
def duplicate_selected_pages(self) -> None:
319-
"""Duplicates selected or checked pages."""
323+
def duplicate_selected_pages(self) -> bool:
324+
"""Duplicates selected or checked pages.
325+
326+
Returns:
327+
bool: True if any page was duplicated, otherwise False.
328+
"""
320329
model = self.table_view.model()
321330
rows_to_duplicate = set()
322331

@@ -375,6 +384,7 @@ def duplicate_selected_pages(self) -> None:
375384
model.index(insert_row, 0),
376385
QItemSelectionModel.Select | QItemSelectionModel.Rows
377386
)
387+
return bool(rows_to_duplicate)
378388

379389

380390
def has_selection_or_checks(self) -> bool:
@@ -561,14 +571,22 @@ def has_selected_pages(self) -> bool:
561571
return self.pages_table.has_selection_or_checks()
562572

563573

564-
def delete_selected_pages(self) -> None:
565-
"""Deletes selected or checked pages."""
566-
self.pages_table.delete_selected_pages()
574+
def delete_selected_pages(self) -> bool:
575+
"""Deletes selected or checked pages.
576+
577+
Returns:
578+
bool: True if any page was deleted, otherwise False.
579+
"""
580+
return self.pages_table.delete_selected_pages()
567581

568582

569-
def duplicate_selected_pages(self) -> None:
570-
"""Duplicates selected or checked pages."""
571-
self.pages_table.duplicate_selected_pages()
583+
def duplicate_selected_pages(self) -> bool:
584+
"""Duplicates selected or checked pages.
585+
586+
Returns:
587+
bool: True if any page was duplicated, otherwise False.
588+
"""
589+
return self.pages_table.duplicate_selected_pages()
572590

573591

574592
def get_document_data(self) -> dict:
@@ -811,4 +829,4 @@ def switch_to_files_tab(self) -> None:
811829
self.ui.tabs_tabWidget.setCurrentIndex(1)
812830

813831
def set_file_drop_active(self, active: bool) -> None:
814-
self.files_tab.file_drop_widget.setDragActive(active)
832+
self.files_tab.file_drop_widget.setDragActive(active)

0 commit comments

Comments
 (0)