# Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2019 Scille SAS from PyQt5.QtCore import Qt, pyqtSignal, QTimer from PyQt5.QtWidgets import QWidget, QMenu, QGraphicsDropShadowEffect from PyQt5.QtGui import QColor from parsec.api.data import UserProfile from parsec.core.backend_connection import BackendConnectionError, BackendNotAvailable from parsec.core.gui.trio_thread import JobResultError, ThreadSafeQtSignal, QtToTrioJob from parsec.core.gui.invite_user_widget import InviteUserWidget from parsec.core.gui.custom_dialogs import show_error, show_info, ask_question from parsec.core.gui.lang import translate as _, format_datetime from parsec.core.gui.ui.user_button import Ui_UserButton from parsec.core.gui.ui.users_widget import Ui_UsersWidget class UserButton(QWidget, Ui_UserButton): revoke_clicked = pyqtSignal(QWidget) def __init__( self, user_id, user_display, is_current_user, is_admin, certified_on, is_revoked, current_user_is_admin, ): super().__init__() self.setupUi(self) self.current_user_is_admin = current_user_is_admin self.is_admin = is_admin self.is_revoked = is_revoked self._is_revoked = is_revoked self.certified_on = certified_on self.is_current_user = is_current_user self.user_id = user_id self.user_display = user_display self.label_username.setText(user_display) self.user_icon.apply_style() self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) self.label_created_on.setText(format_datetime(self.certified_on, full=True)) self.label_role.setText( _("TEXT_USER_ROLE_ADMIN") if self.is_admin else _("TEXT_USER_ROLE_CONTRIBUTOR") ) if self.is_current_user: self.label_user_is_current.setText("({})".format(_("TEXT_USER_IS_CURRENT"))) effect = QGraphicsDropShadowEffect(self) effect.setColor(QColor(0x99, 0x99, 0x99)) effect.setBlurRadius(10) effect.setXOffset(2) effect.setYOffset(2) self.setGraphicsEffect(effect) @property def is_revoked(self): return self._is_revoked @is_revoked.setter def is_revoked(self, value): self._is_revoked = value if value: self.label_revoked.setText(_("TEXT_USER_IS_REVOKED")) self.setStyleSheet( "#UserButton, #widget { background-color: #E3E3E3; border-radius: 4px; }" ) else: self.label_revoked.setText("") self.setStyleSheet( "#UserButton, #widget { background-color: #FFFFFF; border-radius: 4px; }" ) def show_context_menu(self, pos): if self.is_revoked or self.is_current_user or not self.current_user_is_admin: return global_pos = self.mapToGlobal(pos) menu = QMenu(self) action = menu.addAction(_("ACTION_USER_MENU_REVOKE")) action.triggered.connect(self.revoke) menu.exec_(global_pos) def revoke(self): self.revoke_clicked.emit(self) async def _do_revoke_user(core, user_id, button): try: await core.revoke_user(user_id) return button except BackendNotAvailable as exc: raise JobResultError("offline") from exc except BackendConnectionError as exc: raise JobResultError("error") from exc async def _do_list_users(core): try: users, total = await core.find_humans() # TODO: handle pagination ! (currently we only display the first 100 users...) return users except BackendNotAvailable as exc: raise JobResultError("offline") from exc except BackendConnectionError as exc: raise JobResultError("error") from exc class UsersWidget(QWidget, Ui_UsersWidget): revoke_success = pyqtSignal(QtToTrioJob) revoke_error = pyqtSignal(QtToTrioJob) list_success = pyqtSignal(QtToTrioJob) list_error = pyqtSignal(QtToTrioJob) def __init__(self, core, jobs_ctx, event_bus, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.core = core self.jobs_ctx = jobs_ctx self.event_bus = event_bus self.users = [] self.button_add_user.apply_style() if core.device.is_admin: self.button_add_user.clicked.connect(self.invite_user) else: self.button_add_user.hide() self.filter_timer = QTimer() self.filter_timer.setInterval(300) self.line_edit_search.textChanged.connect(self.filter_timer.start) self.filter_timer.timeout.connect(self.on_filter_timer_timeout) self.revoke_success.connect(self.on_revoke_success) self.revoke_error.connect(self.on_revoke_error) self.list_success.connect(self.on_list_success) self.list_error.connect(self.on_list_error) self.reset() def on_filter_timer_timeout(self): self.filter_users(self.line_edit_search.text()) def filter_users(self, pattern): pattern = pattern.lower() for i in range(self.layout_users.count()): item = self.layout_users.itemAt(i) if item: w = item.widget() if pattern and pattern not in w.user_name.lower(): w.hide() else: w.show() def invite_user(self): InviteUserWidget.exec_modal(core=self.core, jobs_ctx=self.jobs_ctx, parent=self) self.reset() def add_user(self, user_id, user_display, is_current_user, is_admin, certified_on, is_revoked): if user_id in self.users: return button = UserButton( user_id, user_display, is_current_user, is_admin, certified_on, is_revoked, current_user_is_admin=self.core.device.is_admin, ) self.layout_users.addWidget(button) button.revoke_clicked.connect(self.revoke_user) button.show() self.users.append(user_id) def on_revoke_success(self, job): button = job.ret show_info(self, _("TEXT_USER_REVOKE_SUCCESS_user").format(user=button.user_display)) button.is_revoked = True def on_revoke_error(self, job): status = job.status if status == "already_revoked": errmsg = _("TEXT_USER_REVOCATION_USER_ALREADY_REVOKED") elif status == "not_found": errmsg = _("TEXT_USER_REVOCATION_USER_NOT_FOUND") elif status == "not_allowed": errmsg = _("TEXT_USER_REVOCATION_NOT_ENOUGH_PERMISSIONS") elif status == "offline": errmsg = _("TEXT_USER_REVOCATION_BACKEND_OFFLINE") else: errmsg = _("TEXT_USER_REVOCATION_UNKNOWN_FAILURE") show_error(self, errmsg, exception=job.exc) def revoke_user(self, user_button): result = ask_question( self, _("TEXT_USER_REVOCATION_TITLE"), _("TEXT_USER_REVOCATION_INSTRUCTIONS_user").format(user=user_button.user_display), [_("ACTION_USER_REVOCATION_CONFIRM"), _("ACTION_CANCEL")], ) if result != _("ACTION_USER_REVOCATION_CONFIRM"): return self.jobs_ctx.submit_job( ThreadSafeQtSignal(self, "revoke_success", QtToTrioJob), ThreadSafeQtSignal(self, "revoke_error", QtToTrioJob), _do_revoke_user, core=self.core, user_id=user_button.user_id, button=user_button, ) def on_list_success(self, job): self.users = [] while self.layout_users.count() != 0: item = self.layout_users.takeAt(0) if item: w = item.widget() self.layout_users.removeWidget(w) w.hide() w.setParent(None) current_user = self.core.device.user_id for user in job.ret: self.add_user( user_id=user.user_id, user_display=user.user_display, is_current_user=current_user == user.user_id, is_admin=user.profile == UserProfile.ADMIN, certified_on=user.created_on, is_revoked=user.is_revoked, ) def on_list_error(self, job): status = job.status if status == "offline": return else: errmsg = _("TEXT_USER_LIST_RETRIEVABLE_FAILURE") show_error(self, errmsg, exception=job.exc) def reset(self): self.jobs_ctx.submit_job( ThreadSafeQtSignal(self, "list_success", QtToTrioJob), ThreadSafeQtSignal(self, "list_error", QtToTrioJob), _do_list_users, core=self.core, )