diff --git a/examples/basic_demo.py b/examples/basic_demo.py new file mode 100644 index 0000000..13e6fa8 --- /dev/null +++ b/examples/basic_demo.py @@ -0,0 +1,231 @@ +""" +Demo application using pyqt6_scaffold. +Run via start.sh / start.cmd - environment variables must be set. + +start.sh: + export SQLITE_PATH=demo.db + python demo_app.py +""" +import sys +import sqlite3 +from PyQt6.QtWidgets import ( + QApplication, QLabel, QLineEdit, + QPushButton, QVBoxLayout, QHBoxLayout, + QWidget, QTableView, QMessageBox +) +from PyQt6.QtCore import Qt + +from pyqt6_scaffold import ( + AbstractDatabase, BaseWindow, Composer, + BaseUser, NavigateRequest, NavigationContext, + BaseTableModel +) +from pyqt6_scaffold.contrib.auth import Role + + +# --- Database ----------------------------------------------------------- + +class AppDatabase(AbstractDatabase): + @property + def placeholder(self) -> str: + return "?" + + def _connect(self): + import os + path = os.getenv("SQLITE_PATH", "demo.db") + conn = sqlite3.connect(path) + self._bootstrap(conn) + return conn + + def _bootstrap(self, conn): + """Create tables and seed data if not exists.""" + conn.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + login TEXT UNIQUE, + password TEXT, + role TEXT, + level INTEGER + ); + + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY, + name TEXT, + category TEXT, + price REAL, + stock INTEGER + ); + + INSERT OR IGNORE INTO users VALUES (1, 'admin', 'admin', 'Administrator', 100); + INSERT OR IGNORE INTO users VALUES (2, 'user', 'user', 'User', 25); + + INSERT OR IGNORE INTO products VALUES (1, 'Notebook', 'Electronics', 79999, 5); + INSERT OR IGNORE INTO products VALUES (2, 'Mouse', 'Electronics', 1299, 0); + INSERT OR IGNORE INTO products VALUES (3, 'Table', 'Furniture', 12500, 3); + INSERT OR IGNORE INTO products VALUES (4, 'Chair', 'Furniture', 8900, 0); + INSERT OR IGNORE INTO products VALUES (5, 'Monitor', 'Electronics', 24999, 2); + """) + conn.commit() + + def auth(self, login: str, password: str) -> BaseUser | None: + with self.execute( + f"SELECT id, login, role, level FROM users WHERE login = {self.placeholder} AND password = {self.placeholder}", + (login, password) + ) as cursor: + row = cursor.fetchone() + if not row: + return None + role = Role(name=row[2], level=row[3]) + return BaseUser(id=row[0], name=row[1], role=role) + + def get_products(self) -> list: + with self.execute( + "SELECT id, name, category, price, stock FROM products ORDER BY id" + ) as cursor: + return cursor.fetchall() + + +# --- Models ----------------------------------------------------------- + +class ProductModel(BaseTableModel): + headers = ["ID", "Name", "Category", "Price", "In stock"] + + def row_background(self, data): + from PyQt6.QtGui import QColor + if data[4] == 0: # if out of stock - blue + return QColor("#cce5ff") + return None + + def row_foreground(self, data): + return None + + def row_font(self, data): + return None + + +# --- Windows ----------------------------------------------------------- + +class LoginWindow(BaseWindow): + def _define_widgets(self): + self._title = QLabel("Entry in system") + self._login = QLineEdit() + self._password = QLineEdit() + self._password.setEchoMode(QLineEdit.EchoMode.Password) + self._btn = QPushButton("Enter") + self._hint = QLabel("admin/admin or user/user") + + self._login.setPlaceholderText("Login") + self._password.setPlaceholderText("Password") + self._hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + + def _tune_layouts(self): + layout = QVBoxLayout() + layout.setSpacing(10) + layout.setContentsMargins(40, 40, 40, 40) + layout.addWidget(self._title) + layout.addWidget(self._login) + layout.addWidget(self._password) + layout.addWidget(self._btn) + layout.addWidget(self._hint) + + container = QWidget() + container.setLayout(layout) + self.setCentralWidget(container) + + def _connect_slots(self): + self._btn.clicked.connect(self._on_login) + self._password.returnPressed.connect(self._on_login) + + def _apply_windows_settings(self): + self.setWindowTitle("Authorization") + self.setFixedSize(300, 220) + + def _on_login(self): + user = self._db.auth( + self._login.text().strip(), + self._password.text().strip() + ) + if user is None: + QMessageBox.warning(self, "Error", "Incorrect login or password") + return + + self._composer.navigate_request.emit( + NavigateRequest( + target="main", + context=NavigationContext(data={"user": user}) + ) + ) + + +class MainWindow(BaseWindow): + def _define_widgets(self): + user = self._composer.context.data.get("user") + name = user.name if user else "" + role = user.role.name if user else "" + + self._info = QLabel(f"User: {name} | Role: {role}") + self._table = QTableView() + self._model = ProductModel() + self._logout = QPushButton("Exit") + + self._table.setModel(self._model) + self._table.horizontalHeader().setStretchLastSection(True) + self._table.setSelectionBehavior( + QTableView.SelectionBehavior.SelectRows + ) + self._table.setEditTriggers( + QTableView.EditTrigger.NoEditTriggers + ) + + rows = self._db.get_products() + self._model.refresh(rows) + + def _tune_layouts(self): + top = QHBoxLayout() + top.addWidget(self._info) + top.addStretch() + top.addWidget(self._logout) + + layout = QVBoxLayout() + layout.setContentsMargins(16, 16, 16, 16) + layout.setSpacing(8) + layout.addLayout(top) + layout.addWidget(self._table) + + container = QWidget() + container.setLayout(layout) + self.setCentralWidget(container) + + def _connect_slots(self): + self._logout.clicked.connect(self._on_logout) + + def _apply_windows_settings(self): + self.setWindowTitle("Product catalog") + self.resize(700, 400) + + def _on_logout(self): + self._composer.navigate_request.emit( + NavigateRequest( + target="login", + context=NavigationContext() + ) + ) + + +# --- Entry point ----------------------------------------------------------- + +def main(): + app = QApplication(sys.argv) + + db = AppDatabase() + db.connect() + + composer = Composer(app=app, db=db) + composer.register("login", LoginWindow) + composer.register("main", MainWindow) + + sys.exit(composer.run(start="login")) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7271c2b..9048ef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ all = ["psycopg2-binary>=2.9.0", "pymysql>=1.0.0"] [tool.setuptools.packages.find] where = ["."] include = ["pyqt6_scaffold*"] -exclude = ["docs*", "README.md"] +exclude = ["docs*", "README.md", "examples*"] [project.urls] repository = "https://codeberg.org/zerumarex/pyqt6-scaffold" \ No newline at end of file