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
24 changes: 23 additions & 1 deletion api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ def verify(self, token: str) -> dict:
dict: The JSON response from the API containing user data.
"""
headers = {"Authorization": f"Bearer {token}"}
return self._request("GET",
res = self._request("GET",
url=self.base_url + "/auth/user",
headers=headers
)
print(res)
return res


def get_user_data(self, token: str) -> dict:
Expand All @@ -44,6 +46,26 @@ def get_user_data(self, token: str) -> dict:
dict: The JSON response containing user data.
"""
return self.verify(token)


def update_user_data(self, token: str, user_id: int, data: dict) -> dict:
"""Updates user data.

Args:
token (str): The access token.
user_id (int): The ID of the user to update.
data (dict): The user data to update.

Returns:
dict: The JSON response from the API containing user data.
"""
print(data)
headers = {"Authorization": f"Bearer {token}"}
return self._request("PATCH",
url=self.base_url + f"/auth/user/{user_id}",
headers=headers,
json=data
)


# === Login ===
Expand Down
79 changes: 77 additions & 2 deletions modules/main/mvc/main_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from modules.categories_editings import (
CreateCategory, EditCategory
)
from modules.profile.profile_dialog import ProfileDialog
from utils import NotificationService
from utils.error_messages import get_friendly_error_message

Expand Down Expand Up @@ -378,6 +379,59 @@ def _on_logout_clicked(self) -> None:
self.logout_requested.emit()


def _show_profile_dialog(self) -> None:
"""Handles showing the user profile dialog."""
if self.mode != "auth":
return

try:
user_data = self.model.get_full_user_data()
if not user_data:
logger.warning("Could not retrieve user data for profile.")
return

departments = self.model.departments

updated_data = ProfileDialog.show_dialog(
parent=self.window,
user_data=user_data,
departments=departments
)

if updated_data:
worker = APIWorker(
self.model.update_user_profile,
user_id=user_data['id'],
data=updated_data
)
self.active_workers.add(worker)
worker.finished.connect(self._on_profile_update_finished)
worker.error.connect(lambda e: self._handle_error(e, "Ошибка обновления профиля"))
worker.finished.connect(self._cleanup_worker)
worker.error.connect(self._cleanup_worker)
worker.start()

except Exception as e:
self._handle_error(e, "Ошибка профиля")


def _on_profile_update_finished(self, new_data: dict):
"""Handles successful profile update."""
logger.info(f"User profile updated successfully for user ID: {new_data.get('id')}")

# Refresh all data to ensure consistency
self.model.refresh_data()

# Update the UI with new user data
self._init_ui()

NotificationService().show_toast(
notification_type="success",
title="Профиль обновлен",
message="Ваши данные были успешно сохранены."
)


def _on_department_selected(self, selected, deselected) -> None:
"""Handles department selection change."""
if self.is_updating_data:
Expand Down Expand Up @@ -765,9 +819,29 @@ def _init_ui(self) -> None:

if user_data:
username = user_data.get("username")
department = user_data.get("department")
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 if department else "Отдел не выбран")
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))

else:
self.view.set_username("Гость")
Expand All @@ -782,6 +856,7 @@ def _setup_connections(self) -> None:
"""Sets up signal-slot connections."""
# Sidebar
self.view.connect_logout(self._on_logout_clicked)
self.view.connect_profile_action(self._show_profile_dialog)
self.view.connect_departments_selection(self._on_department_selected)
self.view.connect_categories_selection(self._on_category_selected)
self.view.connect_department_edit(self._on_edit_department_clicked)
Expand Down
20 changes: 20 additions & 0 deletions modules/main/mvc/main_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ def get_document_pages(
) -> list[dict]:
pages = self.api.get_document_pages(document_id)
return pages["pages"]


def get_full_user_data(self) -> dict | None:
"""Retrieves full user data for the profile dialog."""
token = self._get_user_token()
if not token:
return None

# This might be the same as get_user_data, but let's assume it could be different
return self.api.get_user_data(token)


def update_user_profile(self, user_id: int, data: dict) -> dict:
"""Updates the user's profile data."""
# The data dict should contain 'username', 'department' (ID)
return self._make_authorized_request(
self.api.update_user_data,
user_id=user_id,
data=data
)


def create_department(
Expand Down
14 changes: 11 additions & 3 deletions modules/main/mvc/main_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,8 +820,7 @@ def __init__(self, ui: MainWindow_UI) -> None:

# Disabled until the next update
for ui_element in[
self.ui.change_view_pushButton,
self.ui.profile_info_label
self.ui.change_view_pushButton
]:
ui_element.setVisible(False)

Expand Down Expand Up @@ -930,7 +929,6 @@ def _setup_profile_menu(self) -> None:
)

# Disable unimplemented actions
self.user_profile_action.setVisible(False)
self.settings_action.setVisible(False)


Expand Down Expand Up @@ -1296,6 +1294,16 @@ def connect_logout(self, handler) -> None:
self.logout_action.triggered.connect(handler)


def connect_profile_action(self, handler) -> None:
"""Connects the user profile action to a handler.

Args:
handler: The callback function.
"""
if self.user_profile_action:
self.user_profile_action.triggered.connect(handler)


def connect_departments_selection(self, handler) -> None:
"""Connects the departments selection signal to a handler.

Expand Down
150 changes: 150 additions & 0 deletions modules/profile/profile_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGraphicsDropShadowEffect
from PyQt5.QtGui import QColor, QRegExpValidator
from PyQt5.QtCore import QRegExp

from ui.ui_converted.profile_dialog import Ui_ProfileDialog
from ui.custom_widgets.modal_window import ShadowContainer, BaseModalDialog

class ProfileDialog(BaseModalDialog):
def __init__(self, parent, user_data: dict, departments: list[dict]):
super().__init__(parent)
self.user_data = user_data
self.departments = departments

# === Setup UI ===
self.ui = Ui_ProfileDialog()
self.ui.setupUi(self)

# Take layout from UI
original_layout = self.layout()

# Create a container widget that will hold the UI and have the shadow
container = ShadowContainer(self)
container.setLayout(original_layout)
container.setObjectName("profileDialogContainer")

# Reparent UI frames into container
self.ui.texts_frame.setParent(container)
self.ui.form_frame.setParent(container)
self.ui.buttons_frame.setParent(container)

# === Add single word validators ===
single_word_validator = QRegExpValidator(QRegExp(r'^\S+$'))
self.ui.firstname_lineEdit.setValidator(single_word_validator)
self.ui.lastname_lineEdit.setValidator(single_word_validator)

# === Shadow ===
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setColor(QColor(0, 0, 0, int(255 * 0.10)))
shadow.setOffset(0, 5)
container.setGraphicsEffect(shadow)

# === Main layout (holds container with shadow margins) ===
main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20) # For shadow space
main_layout.addWidget(container)
self.setLayout(main_layout)

# === Populate data ===
self._populate_initial_data()

# === Connect handlers ===
self.ui.accept_pushButton.clicked.connect(self.accept)
self.ui.cancel_pushButton.clicked.connect(self.reject)
self.ui.firstname_lineEdit.textChanged.connect(self._validate_changes)
self.ui.lastname_lineEdit.textChanged.connect(self._validate_changes)
self.ui.department_comboBox.currentIndexChanged.connect(self._validate_changes)

def _get_original_department_id(self) -> int | None:
"""Robustly determines the original department ID from user_data."""
dept_id = self.user_data.get("department_id")
if dept_id is not None:
return dept_id

dept_name = self.user_data.get("department")
if dept_name is not None:
for dept in self.departments:
if dept.get("name") == dept_name:
return dept.get("id")
return None

def _populate_initial_data(self):
"""Fills the widgets with the current user data."""
username = self.user_data.get("username", "")
parts = username.split()
first_name = parts[0] if parts else ""
last_name = " ".join(parts[1:]) if len(parts) > 1 else ""

self.ui.firstname_lineEdit.setText(first_name)
self.ui.lastname_lineEdit.setText(last_name)

# Populate departments and select the current one
self.ui.department_comboBox.clear()
target_dept_id = self._get_original_department_id()

# Add a placeholder for no department
self.ui.department_comboBox.addItem("Отдел не выбран", user_data=None)

selected_index = 0 # Default to placeholder
for i, dept in enumerate(self.departments):
dept_id = dept.get("id")
self.ui.department_comboBox.addItem(dept.get("name"), user_data=dept_id)

if target_dept_id is not None and str(dept_id) == str(target_dept_id):
# The index in the combobox is i + 1 because of the placeholder
selected_index = i + 1

self.ui.department_comboBox.setCurrentIndex(selected_index)

def _validate_changes(self):
"""Enables the save button only if there are actual changes."""
is_changed = False

username = self.user_data.get("username", "")
parts = username.split()
original_first_name = parts[0] if parts else ""
original_last_name = " ".join(parts[1:]) if len(parts) > 1 else ""

# Check names
if self.ui.firstname_lineEdit.text() != original_first_name:
is_changed = True
if self.ui.lastname_lineEdit.text() != original_last_name:
is_changed = True

# Check department with explicit None checks
selected_dept_id = self.ui.department_comboBox.currentData()
original_dept_id = self._get_original_department_id()

department_changed = False
if selected_dept_id is None and original_dept_id is not None:
department_changed = True
elif selected_dept_id is not None and original_dept_id is None:
department_changed = True
elif selected_dept_id is not None and original_dept_id is not None:
if str(selected_dept_id) != str(original_dept_id):
department_changed = True

if department_changed:
is_changed = True

self.ui.accept_pushButton.setEnabled(is_changed)

def get_updated_data(self) -> dict:
"""Returns the updated user data from the form."""
return {
"username": " ".join([
self.ui.firstname_lineEdit.text(),
self.ui.lastname_lineEdit.text()
]),
"department_id": self.ui.department_comboBox.currentData()
}

@staticmethod
def show_dialog(parent, user_data: dict, departments: list[dict]):
"""Creates, shows the dialog, and returns the updated data if accepted."""
dialog = ProfileDialog(parent, user_data, departments)
if dialog.exec_() == QDialog.Accepted:
return dialog.get_updated_data()
return None

3 changes: 2 additions & 1 deletion ui/custom_widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
from .hints import PasswordHint
from .progress_bar import ProgressBar
from .file_drop import FileDropWidget
from .files import FileWidget, FileListWidget
from .files import FileWidget, FileListWidget
from .combobox import ComboBox
Loading
Loading