Skip to content

Commit

Permalink
- token renewal (oauth2)
Browse files Browse the repository at this point in the history
- error window with error description
- updated user schemas
  • Loading branch information
HardMax71 committed Aug 23, 2024
1 parent ddf5fe0 commit a97bd33
Show file tree
Hide file tree
Showing 78 changed files with 1,259 additions and 1,154 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ across mobile, web, and desktop environments.
| Quality Control | - |||
| Billing and Invoicing | - |||
| Yard Management | - |||
| Security and Compliance ||||
| System Administration | - |||
| Offline Database Management | - | - ||
| Advanced Reporting | - | - ||
Expand Down
2 changes: 1 addition & 1 deletion desktop_app/resources/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
"request_timeout": 10,
"font": "Candara",
"font_size": "Large",

"icons_path": "resources/icons",
"styles_path": "resources/styles",
"templates_path": "resources/templates",
"translations_path": "resources/translations",
"organization_domain": "nexusware.com"
}
183 changes: 83 additions & 100 deletions desktop_app/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from PySide6.QtWidgets import QApplication, QMessageBox
from requests import HTTPError

from desktop_app.src.ui.components.error_dialog import global_exception_handler
from public_api.api import APIClient
from public_api.api import UsersAPI
from services.authentication import AuthenticationService
Expand All @@ -16,118 +17,100 @@
from utils.logger import setup_logger


def load_stylesheet(filename):
file = QFile(filename)
if file.open(QFile.ReadOnly | QFile.Text):
stream = QTextStream(file)
return stream.readAll()
return ""
class AppContext:
def __init__(self):
self.config_manager = ConfigManager()
self.logger = setup_logger("nexusware")
self.api_client = APIClient(base_url=self.config_manager.get("api_base_url",
"http://127.0.0.1:8000/api/v1"))
self.users_api = UsersAPI(self.api_client)
self.auth_service = AuthenticationService(self.users_api)
self.offline_manager = OfflineManager("offline_data.db")
self.update_manager = UpdateManager(self.config_manager)

def initialize_app(self):
app = QApplication(sys.argv)
app.setApplicationName("NexusWare WMS")
app.setOrganizationName("NexusWare")
app.setOrganizationDomain(self.config_manager.get("organization_domain", "nexusware.com"))

QDir.addSearchPath("icons", self.config_manager.get("icons_path", "resources/icons"))
QDir.addSearchPath("styles", self.config_manager.get("styles_path", "resources/styles"))
QDir.addSearchPath("templates", self.config_manager.get("templates_path",
"resources/templates"))
QDir.addSearchPath("translations", self.config_manager.get("translations_path",
"resources/translations"))

app_icon = QIcon("icons:app_icon.png")
app.setWindowIcon(app_icon)
self.apply_appearance_settings(app)

language = self.config_manager.get("language", "English")
translator = QTranslator()
if language != "English":
if translator.load(f"translations:{language.lower()}.qm"):
app.installTranslator(translator)

return app

def apply_appearance_settings(self, app):
theme = self.config_manager.get("theme", "light")
stylesheet = self.load_stylesheet(f"styles:{theme}_theme.qss")
app.setStyleSheet(stylesheet)

font_family = self.config_manager.get("font", "Arial")
font_size_name = self.config_manager.get("font_size", "Medium")
font = QFont(font_family)

if font_size_name == "Small":
font.setPointSize(8)
elif font_size_name == "Medium":
font.setPointSize(10)
elif font_size_name == "Large":
font.setPointSize(12)

app.setFont(font)

def load_stylesheet(self, filename):
file = QFile(filename)
if file.open(QFile.ReadOnly | QFile.Text):
stream = QTextStream(file)
return stream.readAll()
return ""

def create_and_show_main_window(self):
user_permissions = self.users_api.get_current_user_permissions()
main_window = MainWindow(api_client=self.api_client,
config_manager=self.config_manager,
permission_manager=user_permissions)
main_window.show()
return main_window


def apply_appearance_settings(app, config_manager):
# Apply theme
theme = config_manager.get("theme", "light")
stylesheet = load_stylesheet(f"styles:{theme}_theme.qss")
app.setStyleSheet(stylesheet)
def main():
app_context = AppContext()
app_context.logger.info("Starting NexusWare WMS")

# Apply font
font_family = config_manager.get("font", "Arial")
font_size_name = config_manager.get("font_size", "Medium")
font = QFont(font_family)
app = app_context.initialize_app()

if font_size_name == "Small":
font.setPointSize(8)
elif font_size_name == "Medium":
font.setPointSize(10)
elif font_size_name == "Large":
font.setPointSize(12)
sys.excepthook = global_exception_handler(app_context)

app.setFont(font)
app_context.offline_manager.clear_all_actions()

if app_context.config_manager.get("auto_update", True) and app_context.update_manager.check_for_updates():
app_context.update_manager.perform_update()

def main():
# Load configuration
config_manager = ConfigManager()

# Set up logging
logger = setup_logger("nexusware")
logger.info("Starting NexusWare WMS")

# Initialize the application
app = QApplication(sys.argv)
app.setApplicationName("NexusWare WMS")
app.setOrganizationName("NexusWare")
app.setOrganizationDomain(config_manager.get("organization_domain",
"nexusware.com"))

# Adding resource path
QDir.addSearchPath("icons", config_manager.get("icons_path",
"resources/icons"))
QDir.addSearchPath("styles", config_manager.get("styles_path",
"resources/styles"))
QDir.addSearchPath("templates", config_manager.get("templates_path",
"resources/templates"))

# Set application icon
app_icon = QIcon("icons:app_icon.png")
app.setWindowIcon(app_icon)

# Apply appearance settings
apply_appearance_settings(app, config_manager)

# Load language
language = config_manager.get("language", "English")
translator = QTranslator()
if language != "English":
if translator.load(f"resources/translations/{language.lower()}.qm"):
app.installTranslator(translator)

# Initialize API client
api_client = APIClient(base_url=config_manager.get("api_base_url",
"http://127.0.0.1:8000/api/v1"))

# Initialize services
users_api = UsersAPI(api_client)
auth_service = AuthenticationService(users_api)
offline_manager = OfflineManager("offline_data.db")

# TODO: Implement the UpdateManager class correctly
offline_manager.clear_all_actions()
update_manager = UpdateManager(config_manager)

# Check for updates
if config_manager.get("auto_update", True) and update_manager.check_for_updates():
update_manager.perform_update()

# Show login dialog
login_dialog = LoginDialog(auth_service)
login_dialog = LoginDialog(app_context.auth_service)
if login_dialog.exec() != LoginDialog.Accepted:
sys.exit(0)

# Set up main window
user_permissions = users_api.get_current_user_permissions()
main_window = MainWindow(api_client=api_client, config_manager=config_manager, permission_manager=user_permissions)

def handle_auth_error():
QMessageBox.warning(None, "Authentication Error",
"Your session has expired. Please log in again.")
main_window.close()
if login_dialog.exec() == LoginDialog.Accepted:
main_window.show()
else:
sys.exit(0)

# Show main window
main_window.show()

# Start the event loop with error handling
try:
sys.exit(app.exec())
main_window = app_context.create_and_show_main_window() # noqa
except HTTPError as e:
if e.response.status_code == 403:
handle_auth_error()
else:
raise
print(e)
QMessageBox.critical(None, "Error", str(e))

sys.exit(app.exec())


if __name__ == "__main__":
Expand Down
25 changes: 16 additions & 9 deletions desktop_app/src/services/authentication.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
from public_api.api import UsersAPI
from public_api.shared_schemas import (
UserCreate, UserUpdate, UserSanitizedWithRole, Token, Message, User
UserCreate, UserUpdate, UserSanitized, Token, Message
)


class AuthenticationService:
def __init__(self, users_api: UsersAPI):
self.users_api = users_api

def login(self, email: str, password: str) -> Token:
return self.users_api.login(email, password)
def login(self, username: str, password: str) -> Token:
return self.users_api.login(username, password)

def login_2fa(self, email: str, password: str, two_factor_code: str) -> Token:
return self.users_api.login_2fa(email, password, two_factor_code)
def login_2fa(self, username: str, password: str, two_factor_code: str) -> Token:
return self.users_api.login_2fa(username, password, two_factor_code)

def register(self, user_data: UserCreate) -> User:
def register(self, user_data: UserCreate) -> UserSanitized:
return self.users_api.register(user_data)

def reset_password(self, email: str) -> Message:
return self.users_api.reset_password(email)

def get_current_user(self) -> UserSanitizedWithRole:
def refresh_token(self) -> Token:
return self.users_api.refresh_token()

def get_current_user(self) -> UserSanitized:
return self.users_api.get_current_user()

def update_current_user(self, user_data: UserUpdate) -> User:
return self.users_api.update_current_user(user_data)
def update_current_user(self, user_data: UserUpdate) -> UserSanitized:
return self.users_api.update_current_user(user_data)

def change_password(self, current_password: str, new_password: str) -> Message:
return self.users_api.change_password(current_password, new_password)
6 changes: 3 additions & 3 deletions desktop_app/src/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
MessageBox,
FileDialog
)
from .error_dialog import ErrorDialog
from .error_dialog import DetailedErrorDialog, global_exception_handler
from .inventory_widget_dialogs import (
InventoryDialog,
AdjustmentDialog
Expand All @@ -38,10 +38,10 @@
"ProgressDialog",
"MessageBox",
"FileDialog",
"ErrorDialog",
"InventoryDialog",
"AdjustmentDialog",
"OrderDialog",
"OrderDetailsDialog",
"ShippingDialog"
"ShippingDialog",
"DetailedErrorDialog",
]
75 changes: 64 additions & 11 deletions desktop_app/src/ui/components/error_dialog.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,72 @@
import json

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
from PySide6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QTextEdit,
QLabel, QDialogButtonBox, QApplication)
from requests.exceptions import HTTPError


class ErrorDialog(QDialog):
def __init__(self, error_message, parent=None):
super().__init__(parent)
class DetailedErrorDialog(QDialog):
def __init__(self, exctype, value, traceback, app_context):
super().__init__()
self.app_context = app_context
self.setWindowTitle("Error")
self.setFixedSize(300, 150)
self.setMinimumSize(400, 300)
self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint)

layout = QVBoxLayout(self)

self.error_label = QLabel(error_message)
self.error_label.setWordWrap(True) # Wrap long error messages
layout.addWidget(self.error_label)
error_label = QLabel("An error has occurred.")
error_label.setStyleSheet("font-weight: bold; color: red;")
layout.addWidget(error_label)

detailed_text = f"Error Type: {exctype.__name__}\n"
detailed_text += f"Error Message: {str(value)}\n\n"

if isinstance(value, HTTPError):
response = value.response
detailed_text += f"Status Code: {response.status_code}\n"
detailed_text += f"URL: {response.url}\n"
detailed_text += "Response Headers:\n"
for key, value in response.headers.items():
detailed_text += f" {key}: {value}\n"
detailed_text += "\nResponse Content:\n"
try:
content = json.loads(response.content)
detailed_text += json.dumps(content, indent=2)
except json.JSONDecodeError:
detailed_text += response.text

text_edit = QTextEdit()
text_edit.setPlainText(detailed_text)
text_edit.setReadOnly(True)
layout.addWidget(text_edit)

button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box.accepted.connect(self.accept)
layout.addWidget(button_box)

relogin_button = QPushButton("Re-login")
relogin_button.clicked.connect(self.relogin)
button_box.addButton(relogin_button, QDialogButtonBox.ActionRole)

def relogin(self):
self.accept()
main_window = QApplication.activeWindow()
if main_window:
main_window.close()

from desktop_app.src.ui import LoginDialog
login_dialog = LoginDialog(self.app_context.auth_service)
if login_dialog.exec() == LoginDialog.Accepted:
self.app_context.create_and_show_main_window()
else:
QApplication.quit()


def global_exception_handler(app_context):
def handler(exctype, value, traceback):
error_dialog = DetailedErrorDialog(exctype, value, traceback, app_context)
error_dialog.exec()

self.ok_button = QPushButton("OK")
self.ok_button.clicked.connect(self.accept)
layout.addWidget(self.ok_button, alignment=Qt.AlignCenter)
return handler
3 changes: 1 addition & 2 deletions desktop_app/src/ui/components/order_view_dialogs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime
from typing import Optional

from PySide6.QtWidgets import (QVBoxLayout, QTableWidget, QTableWidgetItem,
QDialog, QComboBox,
Expand All @@ -15,7 +14,7 @@

class OrderDialog(QDialog):
def __init__(self, orders_api: OrdersAPI, customers_api: CustomersAPI, products_api: ProductsAPI,
order_data: Optional[OrderWithDetails] = None, parent=None):
order_data: OrderWithDetails | None = None, parent=None):
super().__init__(parent)
self.orders_api = orders_api
self.customers_api = customers_api
Expand Down
Loading

0 comments on commit a97bd33

Please sign in to comment.