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()