diff options
| author | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2024-10-21 15:56:35 +0200 |
|---|---|---|
| committer | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2024-11-13 14:35:33 +0100 |
| commit | 7a1426d6f05adc3334af434623221f4d6dcbfc26 (patch) | |
| tree | c2b7da835ba0cf9d59b2b2cabc8c40368391dede /examples/tutorials | |
| parent | abcc4d78fe0b1ac275e2a08ecec10750cb17abe2 (diff) | |
Finance Manager - Part 2 - Example
- Example that extends on the Finance Manager application from Part 1
to use a SQLite database to store the data, thereby showing the
how to use SqlAlchemy with PySide6.
Pick-to: 6.8
Task-number: PYSIDE-2850
Change-Id: I2cce6d033812796ea2fe5cc200c1854494a308f5
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'examples/tutorials')
11 files changed, 656 insertions, 0 deletions
diff --git a/examples/tutorials/finance_manager/part2/Finance/AddDialog.qml b/examples/tutorials/finance_manager/part2/Finance/AddDialog.qml new file mode 100644 index 000000000..b61c7f931 --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/AddDialog.qml @@ -0,0 +1,129 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Dialog { + id: dialog + + signal finished(string itemName, string category, real cost, string date) + + contentItem: ColumnLayout { + id: form + spacing: 10 + property alias itemName: itemName + property alias category: category + property alias cost: cost + property alias date: date + + GridLayout { + columns: 2 + columnSpacing: 20 + rowSpacing: 10 + Layout.fillWidth: true + + Label { + text: qsTr("Item Name:") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: itemName + focus: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + Label { + text: qsTr("Category:") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: category + focus: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + Label { + text: qsTr("Cost:") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: cost + focus: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: qsTr("€") + inputMethodHints: Qt.ImhFormattedNumbersOnly + } + + Label { + text: qsTr("Date:") + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + } + + TextField { + id: date + focus: true + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline + placeholderText: qsTr("dd-mm-yyyy") + validator: RegularExpressionValidator { regularExpression: /^[0-3]?\d-[01]?\d-\d{4}$/ } + // code to add the - automatically + onTextChanged: { + if (date.text.length === 2 || date.text.length === 5) { + date.text += "-" + } + } + Component.onCompleted: { + var today = new Date(); + var day = String(today.getDate()).padStart(2, '0'); + var month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based + var year = today.getFullYear(); + date.placeholderText = day + "-" + month + "-" + year; + } + } + } + } + + function createEntry() { + form.itemName.clear() + form.category.clear() + form.cost.clear() + form.date.clear() + dialog.title = qsTr("Add Finance Item") + dialog.open() + } + + x: parent.width / 2 - width / 2 + y: parent.height / 2 - height / 2 + + focus: true + modal: true + title: qsTr("Add Finance Item") + standardButtons: Dialog.Ok | Dialog.Cancel + + Component.onCompleted: { + dialog.visible = false + Qt.inputMethod.visibleChanged.connect(adjustDialogPosition) + } + + function adjustDialogPosition() { + if (Qt.inputMethod.visible) { + // If the keyboard is visible, move the dialog up + dialog.y = parent.height / 4 - height / 2 + } else { + // If the keyboard is not visible, center the dialog + dialog.y = parent.height / 2 - height / 2 + } + } + + onAccepted: { + finished(form.itemName.text, form.category.text, parseFloat(form.cost.text), form.date.text) + } +} diff --git a/examples/tutorials/finance_manager/part2/Finance/FinanceDelegate.qml b/examples/tutorials/finance_manager/part2/Finance/FinanceDelegate.qml new file mode 100644 index 000000000..267d57d57 --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/FinanceDelegate.qml @@ -0,0 +1,86 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material + +ItemDelegate { + id: delegate + checkable: true + width: parent.width + height: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.15 : + Math.min(window.width, window.height) * 0.1 + + contentItem: + RowLayout { + Label { + id: dateLabel + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.03 : + Math.min(window.width, window.height) * 0.02 + text: date + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredWidth: 1 + color: Material.primaryTextColor + } + + ColumnLayout { + spacing: 5 + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Label { + text: item_name + color: "#5c8540" + font.bold: true + elide: Text.ElideRight + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.04 : + Math.min(window.width, window.height) * 0.02 + Layout.fillWidth: true + } + + Label { + text: category + elide: Text.ElideRight + Layout.fillWidth: true + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.03 : + Math.min(window.width, window.height) * 0.02 + } + } + + Item { + Layout.fillWidth: true // This item will take up the remaining space + } + + ColumnLayout { + spacing: 5 + Layout.fillWidth: true + Layout.preferredWidth: 1 + + Label { + text: "you spent:" + color: "#5c8540" + elide: Text.ElideRight + Layout.fillWidth: true + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.03 : + Math.min(window.width, window.height) * 0.02 + } + + Label { + text: cost + "€" + elide: Text.ElideRight + Layout.fillWidth: true + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.03 : + Math.min(window.width, window.height) * 0.02 + } + } + } +} diff --git a/examples/tutorials/finance_manager/part2/Finance/FinancePieChart.qml b/examples/tutorials/finance_manager/part2/Finance/FinancePieChart.qml new file mode 100644 index 000000000..f5386326e --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/FinancePieChart.qml @@ -0,0 +1,47 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtGraphs +import QtQuick.Controls.Material + +Item { + width: Screen.width + height: Screen.height + + GraphsView { + id: chart + anchors.fill: parent + antialiasing: true + + theme: GraphsTheme { + colorScheme: Qt.Dark + theme: GraphsTheme.Theme.QtGreenNeon + } + + PieSeries { + id: pieSeries + } + } + + Text { + id: chartTitle + text: "Total Expenses Breakdown by Category" + color: "#5c8540" + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.04 : + Math.min(window.width, window.height) * 0.03 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 20 + } + + function updateChart(data) { + pieSeries.clear() + for (var category in data) { + var slice = pieSeries.append(category, data[category]) + slice.label = category + ": " + data[category] + "€" + slice.labelVisible = true + } + } +} diff --git a/examples/tutorials/finance_manager/part2/Finance/FinanceView.qml b/examples/tutorials/finance_manager/part2/Finance/FinanceView.qml new file mode 100644 index 000000000..8a943e045 --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/FinanceView.qml @@ -0,0 +1,46 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material + +ListView { + id: listView + anchors.fill: parent + height: parent.height + property var financeModel + + delegate: FinanceDelegate { + id: delegate + width: listView.width + } + + model: financeModel + + section.property: "month" // Group items by the "month" property + section.criteria: ViewSection.FullString + section.delegate: Component { + id: sectionHeading + Rectangle { + width: listView.width + height: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.05 : + Math.min(window.width, window.height) * 0.03 + color: "#5c8540" + + required property string section + + Text { + text: parent.section + font.bold: true + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.03 : + Math.min(window.width, window.height) * 0.02 + color: Material.primaryTextColor + } + } + } + + ScrollBar.vertical: ScrollBar { } +} diff --git a/examples/tutorials/finance_manager/part2/Finance/Main.qml b/examples/tutorials/finance_manager/part2/Finance/Main.qml new file mode 100644 index 000000000..b06b3f53d --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/Main.qml @@ -0,0 +1,128 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Controls.Material +import Finance + +ApplicationWindow { + id: window + Material.theme: Material.Dark + Material.accent: Material.Gray + width: Screen.width * 0.3 + height: Screen.height * 0.5 + visible: true + title: qsTr("Finance Manager") + + // Add a toolbar for the application, only visible on mobile + header: ToolBar { + Material.primary: "#5c8540" + visible: Qt.platform.os == "android" + RowLayout { + anchors.fill: parent + Label { + text: qsTr("Finance Manager") + font.pixelSize: 20 + Layout.alignment: Qt.AlignCenter + } + } + } + + ColumnLayout { + anchors.fill: parent + + TabBar { + id: tabBar + Layout.fillWidth: true + + TabButton { + text: qsTr("Expenses") + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.04 : + Math.min(window.width, window.height) * 0.02 + onClicked: stackView.currentIndex = 0 + } + + TabButton { + text: qsTr("Charts") + font.pixelSize: Qt.platform.os == "android" ? + Math.min(window.width, window.height) * 0.04 : + Math.min(window.width, window.height) * 0.02 + onClicked: stackView.currentIndex = 1 + } + } + + StackLayout { + id: stackView + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + id: expensesView + Layout.fillWidth: true + Layout.fillHeight: true + + FinanceView { + id: financeView + anchors.fill: parent + financeModel: finance_model + } + } + + Item { + id: chartsView + Layout.fillWidth: true + Layout.fillHeight: true + + FinancePieChart { + id: financePieChart + anchors.fill: parent + Component.onCompleted: { + var categoryData = finance_model.getCategoryData() + updateChart(categoryData) + } + } + } + } + } + + // Model to store the finance data. Created from Python. + FinanceModel { + id: finance_model + } + + // Add a dialog to add new entries + AddDialog { + id: addDialog + onFinished: function(item_name, category, cost, date) { + finance_model.append(item_name, category, cost, date) + var categoryData = finance_model.getCategoryData() + financePieChart.updateChart(categoryData) + } + } + + // Add a button to open the dialog + ToolButton { + id: roundButton + text: qsTr("+") + highlighted: true + Material.elevation: 6 + width: Qt.platform.os === "android" ? + Math.min(parent.width * 0.2, Screen.width * 0.15) : + Math.min(parent.width * 0.060, Screen.width * 0.05) + height: width // Keep the button circular + anchors.margins: 10 + anchors.right: parent.right + anchors.bottom: parent.bottom + background: Rectangle { + color: "#5c8540" + radius: roundButton.width / 2 + } + font.pixelSize: width * 0.4 + onClicked: { + addDialog.createEntry() + } + } +} diff --git a/examples/tutorials/finance_manager/part2/Finance/qmldir b/examples/tutorials/finance_manager/part2/Finance/qmldir new file mode 100644 index 000000000..5362013c2 --- /dev/null +++ b/examples/tutorials/finance_manager/part2/Finance/qmldir @@ -0,0 +1,6 @@ +module Finance +Main 1.0 Main.qml +FinanceView 1.0 FinanceView.qml +FinancePieChart 1.0 FinancePieChart.qml +FinanceDelegate 1.0 FinanceDelegate.qml +AddDialog 1.0 AddDialog.qml diff --git a/examples/tutorials/finance_manager/part2/database.py b/examples/tutorials/finance_manager/part2/database.py new file mode 100644 index 000000000..08cbb62ca --- /dev/null +++ b/examples/tutorials/finance_manager/part2/database.py @@ -0,0 +1,68 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from sqlalchemy import create_engine, Column, Integer, String, Float +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os +import platform +from pathlib import Path + +Base = declarative_base() + + +class Finance(Base): + __tablename__ = 'finances' + id = Column(Integer, primary_key=True) + item_name = Column(String) + category = Column(String) + cost = Column(Float) + date = Column(String) + + +# Check for an environment variable for the database path +env_db_path = os.getenv('FINANCE_MANAGER_DB_PATH') + +if env_db_path: + db_path = Path(env_db_path) +else: + # Determine the application data directory based on the operating system using pathlib + if platform.system() == 'Windows': + app_data_location = Path(os.getenv('APPDATA')) / 'FinanceManager' + elif platform.system() == 'Darwin': # macOS + app_data_location = Path.home() / 'Library' / 'Application Support' / 'FinanceManager' + else: # Linux and other Unix-like systems + app_data_location = Path.home() / '.local' / 'share' / 'FinanceManager' + + db_path = app_data_location / 'finances.db' + +DATABASE_URL = f'sqlite:///{db_path}' +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) + +# Default data to be added to the database +default_data = [ + {"item_name": "Mobile Prepaid", "category": "Electronics", "cost": 20.00, "date": "15-02-2024"}, + {"item_name": "Groceries-Feb-Week1", "category": "Groceries", "cost": 60.75, + "date": "16-01-2024"}, + {"item_name": "Bus Ticket", "category": "Transport", "cost": 5.50, "date": "17-01-2024"}, + {"item_name": "Book", "category": "Education", "cost": 25.00, "date": "18-01-2024"}, +] + + +def initialize_database(): + if db_path.exists(): + print(f"Database '{db_path}' already exists.") + return + + app_data_location.mkdir(parents=True, exist_ok=True) + Base.metadata.create_all(engine) + print(f"Database '{db_path}' created successfully.") + session = Session() + + for data in default_data: + finance = Finance(**data) + session.add(finance) + + session.commit() + print("Default data has been added to the database.") diff --git a/examples/tutorials/finance_manager/part2/financemodel.py b/examples/tutorials/finance_manager/part2/financemodel.py new file mode 100644 index 000000000..0326697ba --- /dev/null +++ b/examples/tutorials/finance_manager/part2/financemodel.py @@ -0,0 +1,101 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from datetime import datetime +from dataclasses import dataclass +from enum import IntEnum +from collections import defaultdict + +from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot, + QByteArray) +from PySide6.QtQml import QmlElement +import database + +QML_IMPORT_NAME = "Finance" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class FinanceModel(QAbstractListModel): + + @QEnum + class FinanceRole(IntEnum): + ItemNameRole = Qt.DisplayRole + CategoryRole = Qt.UserRole + CostRole = Qt.UserRole + 1 + DateRole = Qt.UserRole + 2 + MonthRole = Qt.UserRole + 3 + + @dataclass + class Finance: + item_name: str + category: str + cost: float + date: str + + @property + def month(self): + return datetime.strptime(self.date, "%d-%m-%Y").strftime("%B %Y") + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.session = database.Session() + self.m_finances = self.load_finances() + + def load_finances(self): + finances = [] + for finance in self.session.query(database.Finance).all(): + finances.append(self.Finance(finance.item_name, finance.category, finance.cost, + finance.date)) + return finances + + def rowCount(self, parent=QModelIndex()): + return len(self.m_finances) + + def data(self, index: QModelIndex, role: int): + row = index.row() + if row < self.rowCount(): + finance = self.m_finances[row] + if role == FinanceModel.FinanceRole.ItemNameRole: + return finance.item_name + if role == FinanceModel.FinanceRole.CategoryRole: + return finance.category + if role == FinanceModel.FinanceRole.CostRole: + return finance.cost + if role == FinanceModel.FinanceRole.DateRole: + return finance.date + if role == FinanceModel.FinanceRole.MonthRole: + return finance.month + return None + + @Slot(result=dict) + def getCategoryData(self): + category_data = defaultdict(float) + for finance in self.m_finances: + category_data[finance.category] += finance.cost + return dict(category_data) + + def roleNames(self): + roles = super().roleNames() + roles[FinanceModel.FinanceRole.ItemNameRole] = QByteArray(b"item_name") + roles[FinanceModel.FinanceRole.CategoryRole] = QByteArray(b"category") + roles[FinanceModel.FinanceRole.CostRole] = QByteArray(b"cost") + roles[FinanceModel.FinanceRole.DateRole] = QByteArray(b"date") + roles[FinanceModel.FinanceRole.MonthRole] = QByteArray(b"month") + return roles + + @Slot(int, result='QVariantMap') + def get(self, row: int): + finance = self.m_finances[row] + return {"item_name": finance.item_name, "category": finance.category, + "cost": finance.cost, "date": finance.date} + + @Slot(str, str, float, str) + def append(self, item_name: str, category: str, cost: float, date: str): + finance = self.Finance(item_name, category, cost, date) + self.session.add(database.Finance(item_name=item_name, category=category, cost=cost, + date=date)) + self.beginInsertRows(QModelIndex(), 0, 0) # Insert at the front + self.m_finances.insert(0, finance) # Insert at the front of the list + self.endInsertRows() + self.session.commit() diff --git a/examples/tutorials/finance_manager/part2/main.py b/examples/tutorials/finance_manager/part2/main.py new file mode 100644 index 000000000..3b8616a45 --- /dev/null +++ b/examples/tutorials/finance_manager/part2/main.py @@ -0,0 +1,30 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +from pathlib import Path + +from PySide6.QtWidgets import QApplication +from PySide6.QtQml import QQmlApplicationEngine + +from financemodel import FinanceModel # noqa: F401 +from database import initialize_database + +if __name__ == '__main__': + # Initialize the database if it does not exist + initialize_database() + + app = QApplication(sys.argv) + QApplication.setOrganizationName("QtProject") + QApplication.setApplicationName("Finance Manager") + engine = QQmlApplicationEngine() + + engine.addImportPath(Path(__file__).parent) + engine.loadFromModule("Finance", "Main") + + if not engine.rootObjects(): + sys.exit(-1) + + ex = app.exec() + del engine + sys.exit(ex) diff --git a/examples/tutorials/finance_manager/part2/part2.pyproject b/examples/tutorials/finance_manager/part2/part2.pyproject new file mode 100644 index 000000000..74b88adcc --- /dev/null +++ b/examples/tutorials/finance_manager/part2/part2.pyproject @@ -0,0 +1,14 @@ +{ + "files": [ + "database.py", + "main.py", + "financemodel.py", + "Finance/AddDialog.qml", + "Finance/FinanceDelegate.qml", + "Finance/FinancePieChart.qml", + "Finance/FinanceView.qml", + "Finance/Main.qml", + "Finance/qmldir", + "requirements.txt" + ] +} diff --git a/examples/tutorials/finance_manager/part2/requirements.txt b/examples/tutorials/finance_manager/part2/requirements.txt new file mode 100644 index 000000000..39fb2befb --- /dev/null +++ b/examples/tutorials/finance_manager/part2/requirements.txt @@ -0,0 +1 @@ +sqlalchemy |
