From cfdaf144670c866704ac065da98ea73f7afb1a77 Mon Sep 17 00:00:00 2001 From: Aryan Gupta Date: Mon, 8 Jul 2024 17:05:43 +0530 Subject: [PATCH] feat: Allow users to set custom profile pictures (#2405) Enhancing the functionality of the MSColab server to allow users to upload small images which would replace generic initials as identifiers during login. This feature personalizes user accounts and significantly enhances the visual aspect of user identification and interaction within the MSColab platform. --- AUTHORS | 1 + mslib/mscolab/chat_manager.py | 25 ----- mslib/mscolab/conf.py | 2 +- mslib/mscolab/file_manager.py | 76 +++++++++++++ mslib/mscolab/models.py | 5 +- mslib/mscolab/server.py | 40 ++++++- mslib/msui/mscolab.py | 91 +++++++++++++++- mslib/msui/qt5/ui_mscolab_profile_dialog.py | 74 ++++++++----- mslib/msui/ui/ui_mscolab_profile_dialog.ui | 113 ++++++++++++-------- tests/_test_mscolab/test_chat_manager.py | 21 +--- tests/_test_mscolab/test_file_manager.py | 24 ++++- tests/_test_msui/test_mscolab.py | 2 +- 12 files changed, 347 insertions(+), 127 deletions(-) diff --git a/AUTHORS b/AUTHORS index 24c3f1e15..6215a054f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,7 @@ in alphabetic order by first name - Andreas Hilboll - Anveshan Lal - Aravind Murali +- Aryan Gupta - Christian Rolf - Debajyoti Dasgupta - Hrithik Kumar Verma diff --git a/mslib/mscolab/chat_manager.py b/mslib/mscolab/chat_manager.py index 475ed5200..e1d35624e 100644 --- a/mslib/mscolab/chat_manager.py +++ b/mslib/mscolab/chat_manager.py @@ -25,11 +25,7 @@ limitations under the License. """ import datetime -import os -import time - import fs -from werkzeug.utils import secure_filename from mslib.mscolab.conf import mscolab_settings from mslib.mscolab.models import db, Message, MessageType @@ -96,24 +92,3 @@ def delete_message(self, message_id): upload_dir.remove(fs.path.join(str(message.op_id), file_name)) db.session.delete(message) db.session.commit() - - def add_attachment(self, op_id, upload_folder, file, file_token): - with fs.open_fs('/') as home_fs: - file_dir = fs.path.join(upload_folder, str(op_id)) - if '\\' not in file_dir: - if not home_fs.exists(file_dir): - home_fs.makedirs(file_dir) - else: - file_dir = file_dir.replace('\\', '/') - if not os.path.exists(file_dir): - os.makedirs(file_dir) - file_name, file_ext = file.filename.rsplit('.', 1) - file_name = f'{file_name}-{time.strftime("%Y%m%dT%H%M%S")}-{file_token}.{file_ext}' - file_name = secure_filename(file_name) - file_path = fs.path.join(file_dir, file_name) - file.save(file_path) - static_dir = fs.path.basename(upload_folder) - static_dir = static_dir.replace('\\', '/') - static_file_path = os.path.join(static_dir, str(op_id), file_name) - if os.path.exists(file_path): - return static_file_path diff --git a/mslib/mscolab/conf.py b/mslib/mscolab/conf.py index 1c6b02f5c..5123cf843 100644 --- a/mslib/mscolab/conf.py +++ b/mslib/mscolab/conf.py @@ -68,7 +68,7 @@ class default_mscolab_settings: # mscolab file upload settings UPLOAD_FOLDER = os.path.join(DATA_DIR, 'uploads') - MAX_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB + MAX_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MiB # used to generate and parse tokens SECRET_KEY = secrets.token_urlsafe(16) diff --git a/mslib/mscolab/file_manager.py b/mslib/mscolab/file_manager.py index e1a61f979..c2b777352 100644 --- a/mslib/mscolab/file_manager.py +++ b/mslib/mscolab/file_manager.py @@ -24,12 +24,17 @@ See the License for the specific language governing permissions and limitations under the License. """ +import sys +import secrets +import time import datetime import fs import difflib import logging import git import threading +import mimetypes +from werkzeug.utils import secure_filename from sqlalchemy.exc import IntegrityError from mslib.mscolab.models import db, Operation, Permission, User, Change, Message from mslib.mscolab.conf import mscolab_settings @@ -225,6 +230,9 @@ def modify_user(self, user, attribute=None, value=None, action=None): elif action == "delete": user_query = User.query.filter_by(id=user.id).first() if user_query is not None: + # Delete profile image if it exists + if user.profile_image_path: + self.delete_user_profile_image(user.profile_image_path) db.session.delete(user) db.session.commit() user_query = User.query.filter_by(id=user.id).first() @@ -250,6 +258,74 @@ def modify_user(self, user, attribute=None, value=None, action=None): db.session.commit() return True + def delete_user_profile_image(self, image_to_be_deleted): + ''' + This function is called when deleting account or updating the profile picture + ''' + upload_folder = mscolab_settings.UPLOAD_FOLDER + if sys.platform.startswith('win'): + upload_folder = upload_folder.replace('\\', '/') + + with fs.open_fs(upload_folder) as profile_fs: + if profile_fs.exists(image_to_be_deleted): + profile_fs.remove(image_to_be_deleted) + logging.debug(f"Successfully deleted image: {image_to_be_deleted}") + + def upload_file(self, file, subfolder=None, identifier=None, include_prefix=False): + """ + Generic function to save files securely in any specified directory with unique filename + and return the relative file path. + """ + upload_folder = mscolab_settings.UPLOAD_FOLDER + if sys.platform.startswith('win'): + upload_folder = upload_folder.replace('\\', '/') + + subfolder_path = fs.path.join(upload_folder, str(subfolder) if subfolder else "") + with fs.open_fs(subfolder_path, create=True) as _fs: + # Creating unique and secure filename + file_name, _ = file.filename.rsplit('.', 1) + mime_type, _ = mimetypes.guess_type(file.filename) + file_ext = mimetypes.guess_extension(mime_type) if mime_type else '.unknown' + token = secrets.token_urlsafe() + timestamp = time.strftime("%Y%m%dT%H%M%S") + + if identifier: + file_name = f'{identifier}-{timestamp}-{token}{file_ext}' + else: + file_name = f'{file_name}-{timestamp}-{token}{file_ext}' + file_name = secure_filename(file_name) + + # Saving the file + with _fs.open(file_name, mode="wb") as f: + file.save(f) + + # Relative File path + if include_prefix: # ToDo: add a namespace for the chat attachments, similar as for profile images + static_dir = fs.path.basename(upload_folder) + static_file_path = fs.path.join(static_dir, str(subfolder), file_name) + else: + static_file_path = fs.path.relativefrom(upload_folder, fs.path.join(subfolder_path, file_name)) + + logging.debug(f'Relative Path: {static_file_path}') + return static_file_path + + def save_user_profile_image(self, user_id, image_file): + """ + Save the user's profile image path to the database. + """ + relative_file_path = self.upload_file(image_file, subfolder='profile', identifier=user_id) + + user = User.query.get(user_id) + if user: + if user.profile_image_path: + # Delete the previous image + self.delete_user_profile_image(user.profile_image_path) + user.profile_image_path = relative_file_path + db.session.commit() + return True, "Image uploaded successfully" + else: + return False, "User not found" + def update_operation(self, op_id, attribute, value, user): """ op_id: operation id diff --git a/mslib/mscolab/models.py b/mslib/mscolab/models.py index 877041fc0..abac5406c 100644 --- a/mslib/mscolab/models.py +++ b/mslib/mscolab/models.py @@ -58,16 +58,19 @@ class User(db.Model): username = db.Column(db.String(255)) emailid = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) + profile_image_path = db.Column(db.String(255), nullable=True) # relative path registered_on = db.Column(AwareDateTime, nullable=False) confirmed = db.Column(db.Boolean, nullable=False, default=False) confirmed_on = db.Column(AwareDateTime, nullable=True) permissions = db.relationship('Permission', cascade='all,delete,delete-orphan', backref='user') authentication_backend = db.Column(db.String(255), nullable=False, default='local') - def __init__(self, emailid, username, password, confirmed=False, confirmed_on=None, authentication_backend='local'): + def __init__(self, emailid, username, password, profile_image_path=None, confirmed=False, + confirmed_on=None, authentication_backend='local'): self.username = username self.emailid = emailid self.hash_password(password) + self.profile_image_path = profile_image_path self.registered_on = datetime.datetime.now(tz=datetime.timezone.utc) self.confirmed = confirmed self.confirmed_on = confirmed_on diff --git a/mslib/mscolab/server.py b/mslib/mscolab/server.py index 568e78af4..d872d5838 100644 --- a/mslib/mscolab/server.py +++ b/mslib/mscolab/server.py @@ -24,6 +24,8 @@ See the License for the specific language governing permissions and limitations under the License. """ +import fs +import sys import functools import json import logging @@ -351,6 +353,41 @@ def get_user(): return json.dumps({'user': {'id': g.user.id, 'username': g.user.username}}) +@APP.route('/upload_profile_image', methods=["POST"]) +@verify_user +def upload_profile_image(): + user_id = request.form['user_id'] + file = request.files['image'] + if not file: + return jsonify({'message': 'No file provided or invalid file type'}), 400 + if not file.mimetype.startswith('image/'): + return jsonify({'message': 'Invalid file type'}), 400 + if file.content_length > mscolab_settings.MAX_UPLOAD_SIZE: + return jsonify({'message': 'File too large'}), 413 + + success, message = fm.save_user_profile_image(user_id, file) + if success: + return jsonify({'message': message}), 200 + else: + return jsonify({'message': message}), 400 + + +@APP.route('/fetch_profile_image', methods=["GET"]) +@verify_user +def fetch_profile_image(): + user_id = request.form['user_id'] + user = User.query.get(user_id) + if user and user.profile_image_path: + base_path = mscolab_settings.UPLOAD_FOLDER + if sys.platform.startswith('win'): + base_path = base_path.replace('\\', '/') + filename = user.profile_image_path + with fs.open_fs(base_path) as _fs: + return send_from_directory(_fs.getsyspath(""), filename) + else: + abort(404) + + @APP.route("/delete_own_account", methods=["POST"]) @verify_user def delete_own_account(): @@ -381,7 +418,6 @@ def message_attachment(): user = g.user op_id = request.form.get("op_id", None) if fm.is_member(user.id, op_id): - file_token = secrets.token_urlsafe(16) file = request.files['file'] message_type = MessageType(int(request.form.get("message_type"))) user = g.user @@ -389,7 +425,7 @@ def message_attachment(): if users is False: return jsonify({"success": False, "message": "Could not send message. No file uploaded."}) if file is not None: - static_file_path = cm.add_attachment(op_id, APP.config['UPLOAD_FOLDER'], file, file_token) + static_file_path = fm.upload_file(file, subfolder=str(op_id), include_prefix=True) if static_file_path is not None: new_message = cm.add_message(user, static_file_path, op_id, message_type) new_message_dict = get_message_dict(new_message) diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index 2c59ace2d..de00efc13 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -30,6 +30,7 @@ limitations under the License. """ import os +import io import sys import json import hashlib @@ -39,11 +40,12 @@ import requests import re import webbrowser +import mimetypes import urllib.request from urllib.parse import urljoin from fs import open_fs -from PIL import Image +from PIL import Image, UnidentifiedImageError from keyring.errors import NoKeyringError, PasswordSetError, InitError from mslib.msui import flighttrack as ft @@ -54,6 +56,8 @@ import PyQt5 from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtGui import QPixmap from mslib.utils.auth import get_password_from_keyring, save_password_to_keyring from mslib.utils.verify_user_token import verify_user_token @@ -675,7 +679,7 @@ def after_login(self, emailid, url, r): self.ui.usernameLabel.setText(f"{self.user['username']}") self.ui.usernameLabel.show() self.ui.userOptionsTb.show() - self.fetch_gravatar() + self.fetch_profile_image() # enable add operation menu action self.ui.actionAddOperation.setEnabled(True) @@ -693,7 +697,35 @@ def after_login(self, emailid, url, r): self.signal_login_mscolab.emit(self.mscolab_server_url, self.token) - def fetch_gravatar(self, refresh=False): + def set_profile_pixmap(self, img_data): + pixmap = QPixmap() + pixmap.loadFromData(img_data) + resized_pixmap = pixmap.scaled(64, 64) + + # ToDo : verify by a test if the condition can be simplified + if (hasattr(self, 'profile_dialog') and self.profile_dialog is not None and + hasattr(self.profile_dialog, 'gravatarLabel') and self.profile_dialog.gravatarLabel is not None): + self.profile_dialog.gravatarLabel.setPixmap(resized_pixmap) + + icon = QtGui.QIcon() + icon.addPixmap(resized_pixmap, QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.ui.userOptionsTb.setIcon(icon) + + def fetch_profile_image(self, refresh=False): + # Display custom profile picture if exists + url = urljoin(self.mscolab_server_url, 'fetch_profile_image') + data = { + "user_id": str(self.user["id"]), + "token": self.token + } + response = requests.get(url, data=data) + if response.status_code == 200: + self.set_profile_pixmap(response.content) + else: + self.fetch_gravatar(refresh) + + def fetch_gravatar(self, refresh): + # Display default gravatar if custom profile image is not set email_hash = hashlib.md5(bytes(self.email.encode('utf-8')).lower()).hexdigest() email_in_config = self.email in config_loader(dataset="gravatar_ids") gravatar_img_path = fs.path.join(constants.GRAVATAR_DIR_PATH, f"{email_hash}.png") @@ -798,16 +830,61 @@ def on_context_menu(point): self.profile_dialog.mscolabURLLabel_2.setText(self.mscolab_server_url) self.profile_dialog.emailLabel_2.setText(self.email) self.profile_dialog.deleteAccountBtn.clicked.connect(self.delete_account) + self.profile_dialog.uploadImageBtn.clicked.connect(self.upload_image) # add context menu for right click on image self.gravatar_menu = QtWidgets.QMenu() - self.gravatar_menu.addAction('Fetch Gravatar', lambda: self.fetch_gravatar(refresh=True)) + self.gravatar_menu.addAction('Fetch Gravatar', lambda: self.fetch_profile_image(refresh=True)) self.gravatar_menu.addAction('Remove Gravatar', lambda: self.remove_gravatar()) self.profile_dialog.gravatarLabel.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.profile_dialog.gravatarLabel.customContextMenuRequested.connect(on_context_menu) self.prof_diag.show() - self.fetch_gravatar() + self.fetch_profile_image() + + def upload_image(self): + file_name, _ = QFileDialog.getOpenFileName(self.prof_diag, "Open Image", "", + "Image (*.png *.gif *.jpg *.jpeg *.bpm)") + if file_name: + # Determine the image format + mime_type, _ = mimetypes.guess_type(file_name) + file_format = mime_type.split('/')[1].upper() + try: + # Resize the image and set profile image pixmap + image = Image.open(file_name) + image = image.resize((64, 64), Image.ANTIALIAS) + img_byte_arr = io.BytesIO() + image.save(img_byte_arr, format=file_format) + img_byte_arr.seek(0) + self.set_profile_pixmap(img_byte_arr.getvalue()) + + # Prepare the file data for upload + try: + img_byte_arr.seek(0) # Reset buffer position + files = {'image': (os.path.basename(file_name), img_byte_arr, mime_type)} + data = { + "user_id": str(self.user["id"]), + "token": self.token + } + url = urljoin(self.mscolab_server_url, 'upload_profile_image') + response = requests.post(url, files=files, data=data) + + # Check response status + if response.status_code == 200: + QMessageBox.information(self.prof_diag, "Success", "Image uploaded successfully") + self.fetch_profile_image(refresh=True) + else: + QMessageBox.critical(self.prof_diag, "Error", f"Failed to upload image: {response.text}") + + except requests.exceptions.RequestException as e: + QMessageBox.critical(self.prof_diag, "Error", f"Error occurred: {e}") + + except UnidentifiedImageError as e: + QMessageBox.critical(self.prof_diag, "Error", + f'Cannot identify image file. Please check the file format. Error : {e}') + except OSError as e: + QMessageBox.critical(self.prof_diag, "Error", + f'Cannot identify image file. Please check the file format. Error: {e}') def delete_account(self): # ToDo rename to delete_own_account @@ -2084,6 +2161,10 @@ def logout(self): self.operation_archive_browser.hide() + if hasattr(self, 'profile_dialog'): + del self.profile_dialog + self.profile_dialog = None + # activate first local flighttrack after logging out self.ui.listFlightTracks.setCurrentRow(0) self.ui.activate_selected_flight_track() diff --git a/mslib/msui/qt5/ui_mscolab_profile_dialog.py b/mslib/msui/qt5/ui_mscolab_profile_dialog.py index 637e2db7d..2006db807 100644 --- a/mslib/msui/qt5/ui_mscolab_profile_dialog.py +++ b/mslib/msui/qt5/ui_mscolab_profile_dialog.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'mslib/msui/ui/ui_mscolab_profile_dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # # WARNING! All changes made in this file will be lost! @@ -13,42 +13,42 @@ class Ui_ProfileWindow(object): def setupUi(self, ProfileWindow): ProfileWindow.setObjectName("ProfileWindow") - ProfileWindow.resize(242, 146) + ProfileWindow.resize(387, 149) self.gridLayout = QtWidgets.QGridLayout(ProfileWindow) self.gridLayout.setObjectName("gridLayout") self.infoGl = QtWidgets.QGridLayout() self.infoGl.setObjectName("infoGl") - self.usernameLabel_2 = QtWidgets.QLabel(ProfileWindow) - self.usernameLabel_2.setText("") - self.usernameLabel_2.setObjectName("usernameLabel_2") - self.infoGl.addWidget(self.usernameLabel_2, 0, 2, 1, 1) self.emailLabel_2 = QtWidgets.QLabel(ProfileWindow) self.emailLabel_2.setText("") self.emailLabel_2.setObjectName("emailLabel_2") self.infoGl.addWidget(self.emailLabel_2, 1, 2, 1, 1) - self.mscolabURLLabel = QtWidgets.QLabel(ProfileWindow) - self.mscolabURLLabel.setObjectName("mscolabURLLabel") - self.infoGl.addWidget(self.mscolabURLLabel, 2, 0, 1, 1) - self.mscolabURLLabel_2 = QtWidgets.QLabel(ProfileWindow) - self.mscolabURLLabel_2.setText("") - self.mscolabURLLabel_2.setObjectName("mscolabURLLabel_2") - self.infoGl.addWidget(self.mscolabURLLabel_2, 2, 2, 1, 1) + self.label = QtWidgets.QLabel(ProfileWindow) + self.label.setObjectName("label") + self.infoGl.addWidget(self.label, 0, 1, 1, 1, QtCore.Qt.AlignLeft) self.emailLabel = QtWidgets.QLabel(ProfileWindow) self.emailLabel.setObjectName("emailLabel") self.infoGl.addWidget(self.emailLabel, 1, 0, 1, 1) self.usernameLabel = QtWidgets.QLabel(ProfileWindow) self.usernameLabel.setObjectName("usernameLabel") self.infoGl.addWidget(self.usernameLabel, 0, 0, 1, 1) - self.label = QtWidgets.QLabel(ProfileWindow) - self.label.setObjectName("label") - self.infoGl.addWidget(self.label, 0, 1, 1, 1, QtCore.Qt.AlignLeft) + self.usernameLabel_2 = QtWidgets.QLabel(ProfileWindow) + self.usernameLabel_2.setText("") + self.usernameLabel_2.setObjectName("usernameLabel_2") + self.infoGl.addWidget(self.usernameLabel_2, 0, 2, 1, 1) + self.label_3 = QtWidgets.QLabel(ProfileWindow) + self.label_3.setObjectName("label_3") + self.infoGl.addWidget(self.label_3, 2, 1, 1, 1) + self.mscolabURLLabel_2 = QtWidgets.QLabel(ProfileWindow) + self.mscolabURLLabel_2.setText("") + self.mscolabURLLabel_2.setObjectName("mscolabURLLabel_2") + self.infoGl.addWidget(self.mscolabURLLabel_2, 2, 2, 1, 1) self.label_2 = QtWidgets.QLabel(ProfileWindow) self.label_2.setObjectName("label_2") self.infoGl.addWidget(self.label_2, 1, 1, 1, 1, QtCore.Qt.AlignLeft) - self.label_3 = QtWidgets.QLabel(ProfileWindow) - self.label_3.setObjectName("label_3") - self.infoGl.addWidget(self.label_3, 2, 1, 1, 1, QtCore.Qt.AlignLeft) - self.gridLayout.addLayout(self.infoGl, 0, 0, 1, 1) + self.mscolabURLLabel = QtWidgets.QLabel(ProfileWindow) + self.mscolabURLLabel.setObjectName("mscolabURLLabel") + self.infoGl.addWidget(self.mscolabURLLabel, 2, 0, 1, 1) + self.gridLayout.addLayout(self.infoGl, 0, 0, 1, 2) self.gravatarVl = QtWidgets.QVBoxLayout() self.gravatarVl.setObjectName("gravatarVl") self.gravatarLabel = QtWidgets.QLabel(ProfileWindow) @@ -57,15 +57,29 @@ def setupUi(self, ProfileWindow): self.gravatarLabel.setScaledContents(True) self.gravatarLabel.setObjectName("gravatarLabel") self.gravatarVl.addWidget(self.gravatarLabel, 0, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.gridLayout.addLayout(self.gravatarVl, 0, 1, 2, 1) - self.buttonBox = QtWidgets.QDialogButtonBox(ProfileWindow) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.gridLayout.addWidget(self.buttonBox, 2, 1, 1, 1, QtCore.Qt.AlignRight) + self.gridLayout.addLayout(self.gravatarVl, 0, 2, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") self.deleteAccountBtn = QtWidgets.QPushButton(ProfileWindow) + self.deleteAccountBtn.setIconSize(QtCore.QSize(40, 20)) self.deleteAccountBtn.setAutoDefault(False) self.deleteAccountBtn.setObjectName("deleteAccountBtn") - self.gridLayout.addWidget(self.deleteAccountBtn, 2, 0, 1, 1, QtCore.Qt.AlignLeft) + self.horizontalLayout.addWidget(self.deleteAccountBtn) + self.uploadImageBtn = QtWidgets.QPushButton(ProfileWindow) + self.uploadImageBtn.setObjectName("uploadImageBtn") + self.horizontalLayout.addWidget(self.uploadImageBtn) + self.buttonBox = QtWidgets.QDialogButtonBox(ProfileWindow) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.buttonBox.sizePolicy().hasHeightForWidth()) + self.buttonBox.setSizePolicy(sizePolicy) + self.buttonBox.setMouseTracking(False) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setCenterButtons(False) + self.buttonBox.setObjectName("buttonBox") + self.horizontalLayout.addWidget(self.buttonBox, 0, QtCore.Qt.AlignHCenter) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 3) self.retranslateUi(ProfileWindow) QtCore.QMetaObject.connectSlotsByName(ProfileWindow) @@ -73,12 +87,14 @@ def setupUi(self, ProfileWindow): def retranslateUi(self, ProfileWindow): _translate = QtCore.QCoreApplication.translate ProfileWindow.setWindowTitle(_translate("ProfileWindow", "MSColab Profile")) - self.mscolabURLLabel.setText(_translate("ProfileWindow", "Mscolab")) + self.label.setText(_translate("ProfileWindow", ":")) self.emailLabel.setText(_translate("ProfileWindow", "Email")) self.usernameLabel.setText(_translate("ProfileWindow", "Name")) - self.label.setText(_translate("ProfileWindow", ":")) - self.label_2.setText(_translate("ProfileWindow", ":")) self.label_3.setText(_translate("ProfileWindow", ":")) + self.label_2.setText(_translate("ProfileWindow", ":")) + self.mscolabURLLabel.setText(_translate("ProfileWindow", "Mscolab")) self.deleteAccountBtn.setText(_translate("ProfileWindow", "Delete Account")) + self.uploadImageBtn.setText(_translate("ProfileWindow", "Change Avatar")) + from . import resources_rc diff --git a/mslib/msui/ui/ui_mscolab_profile_dialog.ui b/mslib/msui/ui/ui_mscolab_profile_dialog.ui index ab6a7ac7d..b41cd361b 100644 --- a/mslib/msui/ui/ui_mscolab_profile_dialog.ui +++ b/mslib/msui/ui/ui_mscolab_profile_dialog.ui @@ -6,23 +6,16 @@ 0 0 - 242 - 146 + 387 + 149 MSColab Profile - + - - - - - - - @@ -30,17 +23,10 @@ - - - - Mscolab - - - - - + + - + : @@ -58,13 +44,27 @@ - - + + + + + + + + + : + + + + + + + @@ -72,16 +72,16 @@ - - + + - : + Mscolab - + @@ -98,22 +98,51 @@ - - - - QDialogButtonBox::Ok - - - - - - - Delete Account - - - false - - + + + + + + Delete Account + + + + 40 + 20 + + + + false + + + + + + + Change Avatar + + + + + + + + 0 + 0 + + + + false + + + QDialogButtonBox::Ok + + + false + + + + diff --git a/tests/_test_mscolab/test_chat_manager.py b/tests/_test_mscolab/test_chat_manager.py index 5a720f433..4cc1691c4 100644 --- a/tests/_test_mscolab/test_chat_manager.py +++ b/tests/_test_mscolab/test_chat_manager.py @@ -24,14 +24,9 @@ See the License for the specific language governing permissions and limitations under the License. """ -import os -import secrets import pytest -from werkzeug.datastructures import FileStorage - -from mslib.mscolab.conf import mscolab_settings -from mslib.mscolab.models import Operation, Message, MessageType +from mslib.mscolab.models import Message, MessageType from mslib.mscolab.seed import add_user, get_user, add_operation, add_user_to_operation @@ -76,17 +71,3 @@ def test_delete_messages(self): self.cm.delete_message(message.id) message = Message.query.filter(Message.id == message.id).first() assert message is None - - def test_add_attachment(self): - sample_path = os.path.join(os.path.dirname(__file__), "..", "data") - filename = "example.csv" - name, ext = filename.split('.') - open_csv = os.path.join(sample_path, "example.csv") - operation = Operation.query.filter_by(path=self.operation_name).first() - token = secrets.token_urlsafe(16) - with open(open_csv, 'rb') as fp: - file = FileStorage(fp, filename=filename, content_type="text/csv") - static_path = self.cm.add_attachment(operation.id, mscolab_settings.UPLOAD_FOLDER, file, token) - assert name in static_path - assert static_path.endswith(ext) - assert token in static_path diff --git a/tests/_test_mscolab/test_file_manager.py b/tests/_test_mscolab/test_file_manager.py index 1f93cd64f..6ca509764 100644 --- a/tests/_test_mscolab/test_file_manager.py +++ b/tests/_test_mscolab/test_file_manager.py @@ -26,9 +26,12 @@ """ import datetime import pytest +import os + +from werkzeug.datastructures import FileStorage from mslib.mscolab.models import Operation, User -from mslib.mscolab.seed import add_user, get_user +from mslib.mscolab.seed import add_user, get_user, add_operation class Test_FileManager: @@ -246,6 +249,25 @@ def test_save_file(self): assert self.fm.save_file(operation.id, self.content1, self.user) is False assert self.fm.save_file(operation.id, self.content2, self.user) + def test_upload_chat_attachment(self): + ''' + Tests the chat feature to upload files. + i.e. it tests the upload_file method of file manager in case of it being used to upload a chat attachment + ''' + operation_name = "europe" + assert add_operation(operation_name, "test europe") + operation = Operation.query.filter_by(path=operation_name).first() + + sample_path = os.path.join(os.path.dirname(__file__), "..", "data") + filename = "example.csv" + name, ext = filename.split('.') + open_csv = os.path.join(sample_path, "example.csv") + with open(open_csv, 'rb') as fp: + file = FileStorage(fp, filename=filename, content_type="text/csv") + static_path = self.fm.upload_file(file, subfolder=str(operation.id), identifier=None) + assert name in static_path + assert static_path.endswith(ext) + def test_get_file(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path="operation7") diff --git a/tests/_test_msui/test_mscolab.py b/tests/_test_msui/test_mscolab.py index f24344187..4fa1fb3b1 100644 --- a/tests/_test_msui/test_mscolab.py +++ b/tests/_test_msui/test_mscolab.py @@ -897,7 +897,7 @@ def test_profile_dialog(self, qtbot): assert self.window.mscolab.prof_diag is not None # case: trying to fetch non-existing gravatar with mock.patch("PyQt5.QtWidgets.QMessageBox.critical") as critbox: - self.window.mscolab.fetch_gravatar(refresh=True) + self.window.mscolab.fetch_profile_image(refresh=True) critbox.assert_called_once() assert not self.window.mscolab.profile_dialog.gravatarLabel.pixmap().isNull()