Compare commits

..

5 Commits

Author SHA1 Message Date
a8f642a1c9 docs: add README with full UI and workflow documentation 2026-04-09 11:05:43 +03:00
704e01669f fix: db update bug, add action log with 30-day purge, rebuild exe
- main.py: _pending_record_id locks resolved DB key at Add/Update time;
  show original barcode in update frame; auto-focus mass field on open;
  clear all fields and return focus to ID input after confirm/reset
- database_manager.py: buffered=True cursors on all SELECTs; no
  fetchall() after DML; replace ON DUPLICATE KEY UPDATE VALUES() with
  explicit UPDATE then INSERT fallback; add app_actions.log with
  structured per-action entries; purge_old_action_logs(30) on startup
- dist/DatabaseApp.exe: rebuilt single-file Windows binary (30.9 MB)
- remove unused files: README, WINDOWS_README, run_app.sh,
  setup_database.sh, setup_user.sql, test_database.py, sept.csv"
2026-04-09 11:00:37 +03:00
3604a46421 fix: restore collect_all for mysql.connector to fix no localization for language eng 2026-04-07 17:09:26 +03:00
0f7e157406 fix: dynamic height layout + bundle mysql locales to fix 'no localization for eng' error\n\n- Replace all fixed pixel heights in content_layout with size_hint_y proportions\n so the 6 rows fill available space on any screen (800p, 1080p, etc.)\n- Remove AnchorLayout wrapper that caused dead space above content\n- Bundle mysql/connector/locales in PyInstaller build (spec + build_windows.py)\n- Add mysql.connector.plugins hidden imports to spec and build script" 2026-04-07 16:52:10 +03:00
b51e8bcc2a updated to not view the log file 2026-04-07 16:05:41 +03:00
12 changed files with 458 additions and 10672 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ ENV/
*.egg-info/ *.egg-info/
build/ build/
*.egg *.egg
build_log.txt
# PyInstaller # PyInstaller
*.spec.bak *.spec.bak

294
README.md
View File

@@ -1,152 +1,172 @@
# Database Manager Kivy App # Database Search & Update — User Guide
A simple Kivy application for managing a MariaDB database with two columns (id and mass) for the `offsystemsCounting` table. A fullscreen touchscreen application for querying and updating article/box weight (mass) records stored in a MariaDB database.
## Features ---
- **Search**: Look up records by ID ## UI Layout (top to bottom)
- **Add/Update**: Add new records or update existing ones
- **Delete**: Remove records from the database
- **View All**: Display all records in the database
- **Auto-create**: Table is created automatically if it doesn't exist
## Prerequisites ```
┌─────────────────────────────────────┬────────┐
### MariaDB Server Setup │ Database Search & Update │ Exit │
├──────┬──────────────────────────────┴────────┤
1. **Install MariaDB server** (if not already installed): │ ID: │ [scan / type here] │
```bash │ Mass:│ [current mass] Last update: ... │
sudo apt update ├──────┴───────────────────────────────────────┤
sudo apt install mariadb-server │ Article type detected: PRODUCT [Override] │
``` ├───────────────┬──────────────┬───────────────┤
│ Add/Update │ Reset Values │ Settings │
2. **Start MariaDB service**: ├───────────────┴──────────────┴───────────────┤
```bash │ Update Values │
sudo systemctl start mariadb │ ID: [read-only — original barcode] │
sudo systemctl enable mariadb │ Mass: [editable] │
``` │ [Confirm Add/Update] [Delete] │
├──────────────────────────────────────────────┤
3. **Secure MariaDB installation**: │ Status bar │
```bash ├──────────────────────────────────────────────┤
sudo mysql_secure_installation │ [ 1 ] [ 2 ] [ 3 ] │
``` │ [ 4 ] [ 5 ] [ 6 ] Numeric keypad │
│ [ 7 ] [ 8 ] [ 9 ] │
4. **Create the database and user**: │ [ . ] [ 0 ] [ ⌫ ] │
```bash │ [ Enter ] │
sudo mysql -u root -p └──────────────────────────────────────────────┘
```
Then run these SQL commands:
```sql
CREATE DATABASE cantare_injectie;
CREATE USER 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
5. **Test the connection**:
```bash
mysql -u omron -p cantare_injectie
```
Enter password: `Initial01!`
## Installation
1. Install Python 3.7+ if not already installed
2. Install the required dependencies:
```bash
pip install -r requirements.txt
```
Or using virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
## Usage
1. **Make sure MariaDB server is running**:
```bash
sudo systemctl status mariadb
```
2. **Run the application**:
```bash
python main.py
```
Or:
```bash
chmod +x run_app.sh
./run_app.sh
```
3. **To search for a record:**
- Enter an ID in the "ID" field (max 20 characters)
- Click "Search"
- If found, the mass will be displayed in the "Mass" field
4. **To add or update a record:**
- Enter both ID and mass value
- Click "Add/Update"
- If the ID exists, it will be updated; otherwise, a new record will be created
5. **To delete a record:**
- Enter the ID to delete
- Click "Delete"
- Confirm the deletion in the popup
6. **To refresh the display:**
- Click "Refresh All" to reload all data from the database
## Database Structure
The app connects to MariaDB database `cantare_injectie` with the following table structure:
```sql
CREATE TABLE offsystemsCounting (
id VARCHAR(20) PRIMARY KEY,
mass REAL NOT NULL
)
``` ```
## Files ---
- `main.py`: Main Kivy application ## Article / Box Auto-Detection
- `database_manager.py`: MariaDB database operations class
- `requirements.txt`: Python dependencies
- `test_database.py`: Test script for database operations
- `run_app.sh`: Startup script
- `README.md`: This documentation
## Connection Settings When a barcode is entered in the **ID** field the app automatically classifies it:
The application connects to MariaDB with these settings: | Rule | Type | DB key used |
- **Host**: localhost |---|---|---|
- **Database**: cantare_injectie | Exactly 8 digits | **BOX** | Leading zeros stripped — e.g. `00000003``3` |
- **User**: omron | Anything else | **PRODUCT** | Used as-is |
- **Password**: Initial01!
- **Table**: offsystemsCounting
## Troubleshooting The detected type is shown in the **mode bar** (blue = PRODUCT, amber = BOX).
1. **Connection Error 1698**: ### Manual Override
- Make sure MariaDB is running: `sudo systemctl start mariadb` Press **Override type** to flip the detected type between BOX and PRODUCT.
- Verify user exists and has correct password The mode bar shows `[Manual]` while an override is active.
- Check database exists: `SHOW DATABASES;` Typing a new ID resets the override back to auto-detection.
2. **Access Denied**: ---
- Verify user permissions: `SHOW GRANTS FOR 'omron'@'localhost';`
- Reset password if needed: `ALTER USER 'omron'@'localhost' IDENTIFIED BY 'Initial01!';`
3. **Database/Table doesn't exist**: ## Typical Workflow
- The application will create the table automatically
- Make sure the database `cantare_injectie` exists
## Notes ### 1 — Search
1. Scan or type the article/box code into the **ID** field.
2. Press **Enter** on the keypad or the physical Enter key.
3. The app queries the database and fills:
- **Mass** — current stored weight
- **Last update** — date and time of the last mass change
- IDs must be unique and max 20 characters If the article is not in the database yet, Mass stays empty and the status bar shows "not found".
- Mass values must be valid decimal numbers
- The app includes comprehensive error handling and user feedback ### 2 — Add / Update
- All database operations use parameterized queries for security 1. After a search (found or not found), press **Add/Update**.
2. The **Update Values** frame activates:
- **ID** (read-only) — shows the original barcode as scanned
- **Mass** — pre-filled with the current value, fully editable; keyboard focus moves here automatically
3. Clear the Mass field and type the new weight using the numeric keypad or a physical keyboard.
4. Press **Confirm Add/Update**.
- If the record exists → `UPDATE mass, t_update WHERE id = ?`
- If the record is new → `INSERT` with the current timestamp
5. On success all fields are cleared and focus returns to the **ID** field.
### 3 — Delete
1. After a search (found), press **Add/Update** to load the record into the Update Values frame.
2. Press **Delete** → a confirmation popup appears.
3. Confirm to permanently remove the record from the database.
### 4 — Reset Values
Clears all fields (ID, Mass, Update Values frame) and returns focus to the **ID** field without touching the database.
---
## Numeric Keypad
| Key | Action |
|---|---|
| `0``9` | Append digit to the active field |
| `.` | Append decimal point |
| `⌫` | Delete last character |
| **Enter** | Trigger database search (when ID field is active) |
The keypad always targets the currently focused field (ID or Mass).
---
## Settings
Press **Settings** to change the database server address.
| Field | Default |
|---|---|
| Server IP / hostname | `localhost` (loaded from `config.json`) |
**Test Connection** checks connectivity before saving.
**Save** persists the new address to `config.json` next to the executable.
---
## Log Files
Both files are created next to `config.json` (same folder as the `.exe` when running the built binary, or the project root when running from source).
### `app_actions.log`
One line per database action:
```
2026-04-09 15:30:45 | SEARCH_FOUND | id=4 | mass=3446.0, t_update=None
2026-04-09 15:31:02 | UPDATE | id=4 | mass=3440.0
2026-04-09 15:45:10 | INSERT | id=12345 | mass=1200.0
2026-04-09 16:00:00 | DELETE | id=7 | rows_deleted=1
```
| Action | Description |
|---|---|
| `APP_START` | Application launched |
| `SEARCH_FOUND` | ID found in database |
| `SEARCH_NOT_FOUND` | ID not in database |
| `UPDATE` | Mass updated on existing record |
| `INSERT` | New record created |
| `DELETE` | Record removed |
| `DELETE_NOT_FOUND` | Delete attempted on non-existent ID |
| `LOG_PURGE` | Old log entries removed at startup |
| `*_ERROR` | Any database exception |
Lines older than **30 days** are automatically removed each time the app starts.
### `db_debug.log`
Verbose internal debug log (SQL statements, connection events, row counts) for troubleshooting.
---
## Database
| Detail | Value |
|---|---|
| Engine | MariaDB / MySQL |
| Database | `cantare_injectie` |
| Table | `offsystemsCounting` |
| Columns | `id VARCHAR(20)` · `mass REAL` · `t_update DATETIME` |
The table and the `t_update` column are created automatically on first run if they do not exist.
---
## Running from Source
```powershell
cd "C:\Users\Dell\Desktop\db_interface"
.\venv\Scripts\python.exe main.py
```
## Running the Built Binary
```
dist\DatabaseApp.exe
```
Copy your existing `config.json` next to the `.exe` to keep the saved server IP, or set it via **Settings** on first launch.

View File

@@ -1,82 +0,0 @@
# Windows Setup Instructions
## Prerequisites
1. Python 3.8 or higher
2. MariaDB or MySQL server (local or remote)
## Installation Steps
### 1. Install Python
- Download from: https://www.python.org/downloads/
- During installation, check "Add Python to PATH"
### 2. Setup the Application
Open Command Prompt in the application folder and run:
```cmd
# Create virtual environment
python -m venv venv
# Activate virtual environment
venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
```
### 3. Database Setup (if using local database)
- Install MariaDB or MySQL
- Run the setup script:
```cmd
mysql -u root -p < setup_user.sql
```
### 4. Run the Application
#### Option A: Using the batch file
Simply double-click `run_app.bat`
#### Option B: Using command line
```cmd
venv\Scripts\activate
python main.py
```
## Configuration
- Use the **Settings** button in the app to configure the database server IP address
- Default connection:
- Host: localhost
- Database: cantare_injectie
- User: omron
- Password: Initial01!
## Troubleshooting
### Missing modules error
```cmd
venv\Scripts\activate
pip install -r requirements.txt
```
### Database connection error
- Check if MariaDB/MySQL service is running
- Verify database credentials
- Use Settings button to update server IP address
### Kivy installation issues
```cmd
pip install --upgrade pip
pip install kivy --pre --extra-index-url https://kivy.org/downloads/simple/
```
## Features
- **Fullscreen mode**: App starts in fullscreen by default
- **Search**: Enter ID and press Enter
- **Add/Update**: Click Add/Update button to enable editing
- **Delete**: Use Delete button in update section
- **Reset**: Clear all fields with Reset Values button
- **Settings**: Configure database server IP address
## Keyboard Shortcuts
- **F11** or **Esc**: Exit fullscreen
- **Enter**: Search for ID (when in ID field)

View File

@@ -22,6 +22,13 @@ def build_executable():
else: else:
print("No icon file found (optional)") print("No icon file found (optional)")
# Locate mysql connector locales inside the venv
import site
venv_site = os.path.join('venv', 'Lib', 'site-packages')
mysql_locales = os.path.join(venv_site, 'mysql', 'connector', 'locales')
add_data_sep = ';' if sys.platform == 'win32' else ':'
mysql_locales_arg = f'--add-data={mysql_locales}{add_data_sep}mysql/connector/locales'
# PyInstaller command - simplified to avoid module collection issues # PyInstaller command - simplified to avoid module collection issues
cmd = [ cmd = [
'pyinstaller', 'pyinstaller',
@@ -29,11 +36,17 @@ def build_executable():
'--onefile', '--onefile',
'--windowed', '--windowed',
'--hidden-import=mysql.connector', '--hidden-import=mysql.connector',
'--hidden-import=mysql.connector.locales',
'--hidden-import=mysql.connector.locales.eng',
'--hidden-import=mysql.connector.plugins',
'--hidden-import=mysql.connector.plugins.mysql_native_password',
'--hidden-import=mysql.connector.plugins.caching_sha2_password',
'--hidden-import=kivy.core.window.window_sdl2', '--hidden-import=kivy.core.window.window_sdl2',
'--hidden-import=win32timezone', '--hidden-import=win32timezone',
'--exclude-module=_tkinter', '--exclude-module=_tkinter',
'--exclude-module=matplotlib', '--exclude-module=matplotlib',
'--exclude-module=numpy', '--exclude-module=numpy',
mysql_locales_arg,
] + icon_param + ['main.py'] ] + icon_param + ['main.py']
try: try:

View File

@@ -4,6 +4,8 @@ from typing import List, Tuple, Optional
import json import json
import os import os
import sys import sys
import logging
from datetime import datetime, timedelta
# When frozen by PyInstaller, __file__ points to a temp folder that is deleted on exit. # When frozen by PyInstaller, __file__ points to a temp folder that is deleted on exit.
# sys.executable points to the .exe location, which is persistent. # sys.executable points to the .exe location, which is persistent.
@@ -12,7 +14,59 @@ if getattr(sys, 'frozen', False):
else: else:
_BASE_DIR = os.path.dirname(os.path.abspath(__file__)) _BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(_BASE_DIR, 'config.json') CONFIG_FILE = os.path.join(_BASE_DIR, 'config.json')
LOG_FILE = os.path.join(_BASE_DIR, 'db_debug.log')
ACTION_LOG_FILE = os.path.join(_BASE_DIR, 'app_actions.log')
# ---- Action log helpers (module-level, no class dependency) ----
def _log_action(action: str, record_id: str = '', detail: str = '') -> None:
"""Append one structured line to app_actions.log."""
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
line = f"{ts} | {action:<18} | id={record_id:<20} | {detail}\n"
try:
with open(ACTION_LOG_FILE, 'a', encoding='utf-8') as f:
f.write(line)
except Exception as e:
print(f"Action log write error: {e}")
def purge_old_action_logs(days: int = 30) -> None:
"""Remove lines older than *days* from app_actions.log."""
if not os.path.exists(ACTION_LOG_FILE):
return
cutoff = datetime.now() - timedelta(days=days)
kept = []
removed = 0
try:
with open(ACTION_LOG_FILE, 'r', encoding='utf-8') as f:
for line in f:
# Every valid line starts with YYYY-MM-DD HH:MM:SS
try:
ts = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
if ts >= cutoff:
kept.append(line)
else:
removed += 1
except ValueError:
kept.append(line) # malformed line — keep it
with open(ACTION_LOG_FILE, 'w', encoding='utf-8') as f:
f.writelines(kept)
if removed:
_log_action('LOG_PURGE', '', f'removed {removed} entries older than {days} days')
print(f"Action log purged: {removed} old entries removed")
except Exception as e:
print(f"Action log purge error: {e}")
# File logger appends on every run so history is preserved
logging.basicConfig(
filename=LOG_FILE,
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
log = logging.getLogger('db_manager')
log.info('=== DatabaseManager module loaded ===')
class DatabaseManager: class DatabaseManager:
""" """
@@ -61,7 +115,8 @@ class DatabaseManager:
user=self.user, user=self.user,
password=self.password, password=self.password,
connection_timeout=5, connection_timeout=5,
use_pure=True use_pure=True,
autocommit=True
) )
if test_conn.is_connected(): if test_conn.is_connected():
test_conn.close() test_conn.close()
@@ -70,123 +125,187 @@ class DatabaseManager:
return False, str(e) return False, str(e)
return False, "Connection failed" return False, "Connection failed"
def _new_conn(self):
"""Open and return a fresh connection. Caller must close it."""
return mysql.connector.connect(
host=self.host,
database=self.database,
user=self.user,
password=self.password,
connection_timeout=5,
use_pure=True,
autocommit=True
)
def get_connection(self): def get_connection(self):
"""Get a database connection.""" """Get a reusable connection (kept for test_connection compatibility)."""
try: try:
if self.connection is None or not self.connection.is_connected(): if self.connection is None or not self.connection.is_connected():
self.connection = mysql.connector.connect( log.info(f'Opening persistent connection to {self.host}/{self.database}')
host=self.host, self.connection = self._new_conn()
database=self.database, log.info('Connection opened OK')
user=self.user,
password=self.password,
connection_timeout=5,
use_pure=True
)
return self.connection return self.connection
except Error as e: except Error as e:
log.error(f'Connection error: {e}')
print(f"Database connection error: {e}") print(f"Database connection error: {e}")
return None return None
def init_database(self): def init_database(self):
"""Initialize the database connection and create the table if it doesn't exist.""" """Initialize the database connection and create the table if it doesn't exist."""
# Purge action log entries older than 30 days on every startup
purge_old_action_logs(30)
_log_action('APP_START', '', f'host={self.host}')
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor() cursor.execute('''
cursor.execute(''' CREATE TABLE IF NOT EXISTS offsystemsCounting (
CREATE TABLE IF NOT EXISTS offsystemsCounting ( id VARCHAR(20) PRIMARY KEY,
id VARCHAR(20) PRIMARY KEY, mass REAL NOT NULL
mass REAL NOT NULL )
) ''')
''') # Add t_update column if it doesn't exist yet (MySQL-compatible check)
conn.commit() cursor.execute("""
print(f"Connected to MariaDB database: {self.database}") SELECT COUNT(*) FROM information_schema.COLUMNS
print("Table 'offsystemsCounting' ready") WHERE TABLE_SCHEMA = %s
AND TABLE_NAME = 'offsystemsCounting'
AND COLUMN_NAME = 't_update'
""", (self.database,))
col_exists = cursor.fetchone()[0]
cursor.close()
if not col_exists:
log.info("Adding t_update column to offsystemsCounting")
c2 = conn.cursor()
c2.execute("ALTER TABLE offsystemsCounting ADD COLUMN t_update DATETIME DEFAULT NULL")
c2.close()
log.info("t_update column added")
conn.close()
log.info("init_database complete")
print(f"Connected to MariaDB database: {self.database}")
print("Table 'offsystemsCounting' ready")
except Error as e: except Error as e:
log.error(f"Database initialization error: {e}")
print(f"Database initialization error: {e}") print(f"Database initialization error: {e}")
def read_all_data(self) -> List[Tuple[str, float]]: def read_all_data(self) -> List[Tuple[str, float]]:
"""Read all data from the database.""" """Read all data from the database."""
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor() cursor.execute("SELECT id, mass FROM offsystemsCounting ORDER BY id")
cursor.execute("SELECT id, mass FROM offsystemsCounting ORDER BY id") rows = cursor.fetchall()
return cursor.fetchall() cursor.close()
conn.close()
return rows
except Error as e: except Error as e:
log.error(f"read_all_data error: {e}")
print(f"Error reading data: {e}") print(f"Error reading data: {e}")
return [] return []
def search_by_id(self, record_id: str) -> Optional[Tuple[str, float]]: def search_by_id(self, record_id: str) -> Optional[Tuple]:
"""Search for a record by ID.""" """Search for a record by ID. Returns (id, mass, t_update) or None."""
log.info(f'search_by_id: looking up id={record_id!r}')
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor() cursor.execute("SELECT id, mass, t_update FROM offsystemsCounting WHERE id = %s", (record_id,))
cursor.execute("SELECT id, mass FROM offsystemsCounting WHERE id = %s", (record_id,)) row = cursor.fetchone()
return cursor.fetchone() # buffered=True already fetched the full result; no extra drain needed
cursor.close()
conn.close()
log.info(f'search_by_id: result={row}')
if row:
_log_action('SEARCH_FOUND', record_id, f'mass={row[1]}, t_update={row[2]}')
else:
_log_action('SEARCH_NOT_FOUND', record_id, '')
return row
except Error as e: except Error as e:
log.error(f'search_by_id error: {e}')
_log_action('SEARCH_ERROR', record_id, str(e))
print(f"Error searching data: {e}") print(f"Error searching data: {e}")
return None return None
def add_or_update_record(self, record_id: str, mass: float) -> bool: def add_or_update_record(self, record_id: str, mass: float) -> bool:
"""Add a new record or update existing one if ID already exists.""" """Update mass/t_update for an existing record, or INSERT if it doesn't exist yet."""
log.info(f'add_or_update_record: id={record_id!r} mass={mass}')
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor()
# Try UPDATE first
# Check if record exists update_sql = (
existing = self.search_by_id(record_id) "UPDATE offsystemsCounting "
"SET mass = %s, t_update = NOW() "
if existing: "WHERE id = %s"
# Update existing record )
cursor.execute( log.debug(f'Executing SQL: {update_sql} | params=({mass}, {record_id!r})')
"UPDATE offsystemsCounting SET mass = %s WHERE id = %s", cursor.execute(update_sql, (mass, record_id))
(mass, record_id) affected = cursor.rowcount
)
print(f"Updated record: {record_id} = {mass}") if affected == 0:
else: # Record does not exist yet — INSERT it
# Insert new record insert_sql = (
cursor.execute( "INSERT INTO offsystemsCounting (id, mass, t_update) "
"INSERT INTO offsystemsCounting (id, mass) VALUES (%s, %s)", "VALUES (%s, %s, NOW())"
(record_id, mass) )
) log.debug(f'Executing SQL: {insert_sql} | params=({record_id!r}, {mass})')
print(f"Added new record: {record_id} = {mass}") cursor.execute(insert_sql, (record_id, mass))
affected = cursor.rowcount
conn.commit() log.info(f'add_or_update_record: inserted new record, rowcount={affected}')
return True _log_action('INSERT', record_id, f'mass={mass}')
print(f"Inserted new record: {record_id} = {mass}")
else:
log.info(f'add_or_update_record: updated existing record, rowcount={affected}')
_log_action('UPDATE', record_id, f'mass={mass}')
print(f"Updated record: {record_id} = {mass} (rowcount={affected})")
cursor.close()
conn.close()
return True
except Error as e: except Error as e:
log.error(f'add_or_update_record error: {e}')
_log_action('UPDATE_ERROR', record_id, str(e))
print(f"Error adding/updating record: {e}") print(f"Error adding/updating record: {e}")
return False return False
def delete_record(self, record_id: str) -> bool: def delete_record(self, record_id: str) -> bool:
"""Delete a record by ID.""" """Delete a record by ID."""
log.info(f'delete_record: id={record_id!r}')
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor() cursor.execute("DELETE FROM offsystemsCounting WHERE id = %s", (record_id,))
cursor.execute("DELETE FROM offsystemsCounting WHERE id = %s", (record_id,)) deleted = cursor.rowcount
# DML produces no result set — do NOT fetchall()
if cursor.rowcount > 0: cursor.close()
conn.commit() conn.close()
print(f"Deleted record: {record_id}") if deleted > 0:
return True log.info(f'delete_record: deleted {deleted} row(s)')
else: _log_action('DELETE', record_id, f'rows_deleted={deleted}')
print(f"No record found with ID: {record_id}") print(f"Deleted record: {record_id}")
return False return True
else:
log.info(f'delete_record: no row found for id={record_id!r}')
_log_action('DELETE_NOT_FOUND', record_id, '')
print(f"No record found with ID: {record_id}")
return False
except Error as e: except Error as e:
log.error(f'delete_record error: {e}')
_log_action('DELETE_ERROR', record_id, str(e))
print(f"Error deleting record: {e}") print(f"Error deleting record: {e}")
return False return False
def get_record_count(self) -> int: def get_record_count(self) -> int:
"""Get the total number of records in the database.""" """Get the total number of records in the database."""
try: try:
conn = self.get_connection() conn = self._new_conn()
if conn and conn.is_connected(): cursor = conn.cursor(buffered=True)
cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM offsystemsCounting")
cursor.execute("SELECT COUNT(*) FROM offsystemsCounting") count = cursor.fetchone()[0]
return cursor.fetchone()[0] # fetchone() on a buffered cursor is fully consumed — no drain needed
cursor.close()
conn.close()
return count
except Error as e: except Error as e:
print(f"Error getting record count: {e}") print(f"Error getting record count: {e}")
return 0 return 0

BIN
dist/DatabaseApp.exe vendored

Binary file not shown.

169
main.py
View File

@@ -3,7 +3,6 @@ from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'system') Config.set('kivy', 'keyboard_mode', 'system')
from kivy.app import App from kivy.app import App
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label from kivy.uix.label import Label
@@ -20,70 +19,50 @@ class DatabaseApp(App):
super().__init__(**kwargs) super().__init__(**kwargs)
self.db_manager = DatabaseManager() self.db_manager = DatabaseManager()
self.active_numpad_input = None self.active_numpad_input = None
self._pending_record_id = None # resolved (trimmed) ID locked at show_update_frame time
def build(self): def build(self):
# Set window to fullscreen first so Window.height reflects the screen # Set window to fullscreen first so Window.height reflects the screen
Window.fullscreen = 'auto' Window.fullscreen = 'auto'
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Responsive sizing: derive all dimensions from the actual screen height # Responsive sizing: derive dimensions from the actual screen height
# so the layout fits on any display (800p, 900p, 1080p, etc.)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
wh = Window.height # actual screen height after fullscreen wh = Window.height
# Numpad occupies 29% of screen height (fixed proportion) # Scale factor (1.0 at 1080p, down to 0.65 on small screens)
h_numpad_wr = int(wh * 0.29) s = max(0.65, min(1.0, wh / 1080.0))
# Main layout outer padding and spacing (scaled) # Padding / spacing (all scaled)
s = max(0.65, min(1.0, wh / 1080.0)) pad_v = max(8, int(20 * s))
pad_v = max(8, int(20 * s)) # top / bottom padding pad_h = max(8, int(30 * s))
pad_h = max(8, int(30 * s)) # left / right padding m_spacing = max(4, int(10 * s))
m_spacing = max(4, int(10 * s)) # gap between content and numpad c_spacing = max(4, int(12 * s))
sp = max(6, int(10 * s))
upd_pad = max(4, int(8 * s))
upd_spc = max(4, int(8 * s))
np_pad_v = max(3, int(6 * s))
np_spc = max(3, int(6 * s))
# Space available for the 6 content rows (after numpad + padding + gap) # Numpad (fixed height at bottom 29 % of screen)
avail_total = wh - h_numpad_wr - 2 * pad_v - m_spacing h_numpad_wr = int(wh * 0.29)
c_spacing = max(4, int(12 * s)) # gap between content rows h_enter_btn = max(34, int(h_numpad_wr * 0.24))
avail_items = avail_total - c_spacing * 5 # 6 rows → 5 gaps h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
# Distribute height proportionally among rows
# Reference weights: title=50, search=100, mode=38, buttons=65, update=187, status=40
_w = [50, 100, 38, 65, 187, 40]
_t = sum(_w)
def _h(weight):
return max(24, int(avail_items * weight / _t))
h_title = _h(_w[0])
h_search = _h(_w[1])
h_row = max(20, h_search // 2)
h_mode = _h(_w[2])
h_buttons = _h(_w[3])
h_update = _h(_w[4])
h_status = _h(_w[5])
# Update-frame internal heights
upd_pad = max(4, int(8 * s))
upd_spc = max(4, int(8 * s))
h_upd_title = max(20, int(h_update * 0.20))
h_upd_row = max(20, int(h_update * 0.24))
h_upd_inputs = h_upd_row * 2 + upd_spc
h_upd_btns = max(28, h_update - h_upd_title - h_upd_inputs - 2*upd_pad - 2*upd_spc)
# Numpad internal heights
np_pad_v = max(3, int(6 * s))
np_spc = max(3, int(6 * s))
h_enter_btn = max(34, int(h_numpad_wr * 0.24))
h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
# Font sizes (scaled) # Font sizes (scaled)
f_title = max(14, int(26 * s)) f_title = max(14, int(26 * s))
f_normal = max(11, int(18 * s)) f_normal = max(11, int(18 * s))
f_btn = max(12, int(20 * s)) f_btn = max(12, int(20 * s))
f_numpad = max(15, int(26 * s)) f_numpad = max(15, int(26 * s))
f_enter = max(14, int(24 * s)) f_enter = max(14, int(24 * s))
f_mode = max(10, int(16 * s)) f_mode = max(10, int(16 * s))
f_override = max(10, int(14 * s)) f_override = max(10, int(14 * s))
f_status = max(12, int(20 * s)) f_status = max(12, int(20 * s))
sp = max(6, int(10 * s)) # generic widget spacing
# Proportional size_hint_y weights for the 6 content rows
# title | search | mode | buttons | update-frame | status
_w = [50, 100, 38, 65, 187, 40]
_t = sum(_w)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Build UI # Build UI
@@ -98,12 +77,11 @@ class DatabaseApp(App):
pos_hint={'x': 0, 'y': 0} pos_hint={'x': 0, 'y': 0}
) )
# --- Content container (fills remaining space above numpad) --- # --- Content container: size_hint_y=1 so it fills all space above numpad ---
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=None) content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=1)
content_layout.bind(minimum_height=content_layout.setter('height'))
# Title row: title + Exit button # Title row: title + Exit button
title_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_title, spacing=sp) title_row = BoxLayout(orientation='horizontal', size_hint_y=_w[0]/_t, spacing=sp)
title = Label(text='Database Search & Update', font_size=f_title, bold=True) title = Label(text='Database Search & Update', font_size=f_title, bold=True)
title_row.add_widget(title) title_row.add_widget(title)
exit_btn = Button( exit_btn = Button(
@@ -121,8 +99,8 @@ class DatabaseApp(App):
# Search section (ID + Mass) # Search section (ID + Mass)
search_layout = GridLayout( search_layout = GridLayout(
cols=2, size_hint_y=None, height=h_search, cols=2, size_hint_y=_w[1]/_t,
spacing=sp, row_force_default=True, row_default_height=h_row spacing=sp
) )
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True)) search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
self.id_input = TextInput( self.id_input = TextInput(
@@ -135,17 +113,28 @@ class DatabaseApp(App):
self.id_input.bind(focus=self.on_id_input_focus) self.id_input.bind(focus=self.on_id_input_focus)
search_layout.add_widget(self.id_input) search_layout.add_widget(self.id_input)
search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True)) search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True))
# Mass input + last-update label share the 0.75 right side equally
mass_row = BoxLayout(orientation='horizontal', size_hint_x=0.75, spacing=sp)
self.mass_input = TextInput( self.mass_input = TextInput(
multiline=False, size_hint_x=0.75, multiline=False, size_hint_x=0.5,
hint_text='Mass (read-only)', hint_text='Mass (read-only)',
readonly=True, font_size=f_normal, padding=[7, 7] readonly=True, font_size=f_normal, padding=[7, 7]
) )
search_layout.add_widget(self.mass_input) mass_row.add_widget(self.mass_input)
self.last_update_label = Label(
text='Last update: never',
size_hint_x=0.5, font_size=max(9, int(13 * s)),
bold=False, color=(0.7, 0.7, 0.7, 1),
halign='left', valign='middle'
)
self.last_update_label.bind(size=self.last_update_label.setter('text_size'))
mass_row.add_widget(self.last_update_label)
search_layout.add_widget(mass_row)
content_layout.add_widget(search_layout) content_layout.add_widget(search_layout)
# Mode indicator row # Mode indicator row
self.manual_override = None self.manual_override = None
mode_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_mode, spacing=sp) mode_row = BoxLayout(orientation='horizontal', size_hint_y=_w[2]/_t, spacing=sp)
self.mode_label = Label( self.mode_label = Label(
text='Article type detected: PRODUCT', text='Article type detected: PRODUCT',
size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1) size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1)
@@ -160,7 +149,7 @@ class DatabaseApp(App):
content_layout.add_widget(mode_row) content_layout.add_widget(mode_row)
# Action buttons (Add/Update, Reset, Settings) # Action buttons (Add/Update, Reset, Settings)
button_layout = GridLayout(cols=3, size_hint_y=None, height=h_buttons, spacing=sp) button_layout = GridLayout(cols=3, size_hint_y=_w[3]/_t, spacing=sp)
add_update_btn = Button(text='Add/Update', font_size=f_btn, bold=True) add_update_btn = Button(text='Add/Update', font_size=f_btn, bold=True)
add_update_btn.bind(on_press=self.show_update_frame) add_update_btn.bind(on_press=self.show_update_frame)
button_layout.add_widget(add_update_btn) button_layout.add_widget(add_update_btn)
@@ -175,16 +164,15 @@ class DatabaseApp(App):
# Update frame # Update frame
self.update_frame = BoxLayout( self.update_frame = BoxLayout(
orientation='vertical', padding=upd_pad, spacing=upd_spc, orientation='vertical', padding=upd_pad, spacing=upd_spc,
size_hint_y=None, height=h_update size_hint_y=_w[4]/_t
) )
self.update_frame_label = Label( self.update_frame_label = Label(
text='Update Values', size_hint_y=None, height=h_upd_title, text='Update Values', size_hint_y=0.20,
font_size=f_btn, bold=True font_size=f_btn, bold=True
) )
self.update_frame.add_widget(self.update_frame_label) self.update_frame.add_widget(self.update_frame_label)
update_inputs = GridLayout( update_inputs = GridLayout(
cols=2, spacing=sp, size_hint_y=None, height=h_upd_inputs, cols=2, spacing=sp, size_hint_y=0.52
row_force_default=True, row_default_height=h_upd_row
) )
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True)) update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
self.update_id_input = TextInput( self.update_id_input = TextInput(
@@ -198,7 +186,7 @@ class DatabaseApp(App):
self.update_mass_input.bind(focus=self.on_mass_input_focus) self.update_mass_input.bind(focus=self.on_mass_input_focus)
update_inputs.add_widget(self.update_mass_input) update_inputs.add_widget(self.update_mass_input)
self.update_frame.add_widget(update_inputs) self.update_frame.add_widget(update_inputs)
update_buttons = GridLayout(cols=2, size_hint_y=None, height=h_upd_btns, spacing=sp) update_buttons = GridLayout(cols=2, size_hint_y=0.28, spacing=sp)
self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True) self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True)
self.update_confirm_btn.bind(on_press=self.add_update_record) self.update_confirm_btn.bind(on_press=self.add_update_record)
update_buttons.add_widget(self.update_confirm_btn) update_buttons.add_widget(self.update_confirm_btn)
@@ -212,16 +200,12 @@ class DatabaseApp(App):
# Status label # Status label
self.status_label = Label( self.status_label = Label(
text='Ready', size_hint_y=None, height=h_status, text='Ready', size_hint_y=_w[5]/_t,
color=(0, 0.8, 0, 1), font_size=f_status, bold=True color=(0, 0.8, 0, 1), font_size=f_status, bold=True
) )
content_layout.add_widget(self.status_label) content_layout.add_widget(self.status_label)
# Wrap content in an AnchorLayout that fills all space above the numpad main_layout.add_widget(content_layout)
# so the content block is vertically centred regardless of screen size
content_anchor = AnchorLayout(anchor_x='center', anchor_y='center', size_hint_y=1)
content_anchor.add_widget(content_layout)
main_layout.add_widget(content_anchor)
# --- Numeric keypad --- # --- Numeric keypad ---
numpad_wrapper = BoxLayout( numpad_wrapper = BoxLayout(
@@ -299,7 +283,7 @@ class DatabaseApp(App):
self._refocus_active() self._refocus_active()
def set_update_frame_enabled(self, enabled): def set_update_frame_enabled(self, enabled):
self.update_id_input.readonly = not enabled self.update_id_input.readonly = True # ID is always readonly always set from search
self.update_mass_input.readonly = not enabled self.update_mass_input.readonly = not enabled
self.update_confirm_btn.disabled = not enabled self.update_confirm_btn.disabled = not enabled
self.delete_btn.disabled = not enabled self.delete_btn.disabled = not enabled
@@ -341,17 +325,21 @@ class DatabaseApp(App):
self.override_btn.background_color = (0.6, 0.2, 0.6, 1) if is_override else (0.3, 0.3, 0.3, 1) self.override_btn.background_color = (0.6, 0.2, 0.6, 1) if is_override else (0.3, 0.3, 0.3, 1)
def show_update_frame(self, instance): def show_update_frame(self, instance):
# If no value in search, copy from search fields
record_id = self.id_input.text.strip() record_id = self.id_input.text.strip()
mass_text = self.mass_input.text.strip() mass_text = self.mass_input.text.strip()
self.set_update_frame_enabled(True) self.set_update_frame_enabled(True)
# If mass field is empty, just clear update frame
if not record_id: if not record_id:
self.update_id_input.text = '' self.update_id_input.text = ''
self.update_mass_input.text = '' self.update_mass_input.text = ''
self._pending_record_id = None
return return
self.update_id_input.text = record_id # Lock in the resolved (trimmed) DB id now; display original scan for the operator
self._pending_record_id = self._resolve_id(record_id)
self.update_id_input.text = record_id # show original barcode value
self.update_mass_input.text = mass_text self.update_mass_input.text = mass_text
# Direct numpad and keyboard focus to mass field so operator can immediately enter new mass
self.active_numpad_input = self.update_mass_input
Clock.schedule_once(lambda dt: setattr(self.update_mass_input, 'focus', True), 0.05)
def search_record(self, instance): def search_record(self, instance):
record_id = self.id_input.text.strip() record_id = self.id_input.text.strip()
@@ -372,11 +360,17 @@ class DatabaseApp(App):
def _update(dt): def _update(dt):
if record: if record:
self.mass_input.text = str(record[1]) self.mass_input.text = str(record[1])
t_update = record[2] if len(record) > 2 else None
if t_update:
self.last_update_label.text = f'Last update: {t_update.strftime("%d/%m/%Y %H:%M")}'
else:
self.last_update_label.text = 'Last update: never'
self.show_status(f"Found: {record[0]} = {record[1]}") self.show_status(f"Found: {record[0]} = {record[1]}")
self.highlight_record(resolved_id) self.highlight_record(resolved_id)
else: else:
self.show_status(f"ID '{record_id}' not found in database", error=True)
self.mass_input.text = "" self.mass_input.text = ""
self.last_update_label.text = 'Last update: never'
self.show_status(f"ID '{record_id}' not found in database", error=True)
Clock.schedule_once(_update) Clock.schedule_once(_update)
except Exception as e: except Exception as e:
err = str(e) err = str(e)
@@ -385,7 +379,7 @@ class DatabaseApp(App):
def add_update_record(self, instance): def add_update_record(self, instance):
"""Add or update a record from the update frame.""" """Add or update a record from the update frame."""
record_id = self.update_id_input.text.strip() record_id = self._pending_record_id
mass_text = self.update_mass_input.text.strip() mass_text = self.update_mass_input.text.strip()
if not record_id or not mass_text: if not record_id or not mass_text:
self.show_status("Please enter both ID and mass in update frame", error=True) self.show_status("Please enter both ID and mass in update frame", error=True)
@@ -406,9 +400,16 @@ class DatabaseApp(App):
def _update(dt): def _update(dt):
if success: if success:
self.show_status(f"Successfully added/updated: {record_id} = {mass}") self.show_status(f"Successfully added/updated: {record_id} = {mass}")
# Clear all fields and return focus to ID input
self.update_id_input.text = "" self.update_id_input.text = ""
self.update_mass_input.text = "" self.update_mass_input.text = ""
self.id_input.text = ""
self.mass_input.text = ""
self.last_update_label.text = 'Last update: never'
self._pending_record_id = None
self.set_update_frame_enabled(False) self.set_update_frame_enabled(False)
self.active_numpad_input = self.id_input
Clock.schedule_once(lambda dt2: setattr(self.id_input, 'focus', True), 0.05)
self.refresh_data(None) self.refresh_data(None)
else: else:
self.show_status("Failed to add/update record", error=True) self.show_status("Failed to add/update record", error=True)
@@ -420,11 +421,10 @@ class DatabaseApp(App):
def delete_record(self, instance): def delete_record(self, instance):
"""Delete a record using the update frame fields.""" """Delete a record using the update frame fields."""
record_id = self.update_id_input.text.strip() record_id = self._pending_record_id
if not record_id: if not record_id:
self.show_status("Please enter an ID in the update fields to delete", error=True) self.show_status("Please enter an ID in the update fields to delete", error=True)
return return
# Confirm deletion # Confirm deletion
self.show_confirmation_popup( self.show_confirmation_popup(
f"Are you sure you want to delete ID '{record_id}'?", f"Are you sure you want to delete ID '{record_id}'?",
@@ -463,6 +463,11 @@ class DatabaseApp(App):
"""Reset/clear the first ID and mass fields and set focus on ID field.""" """Reset/clear the first ID and mass fields and set focus on ID field."""
self.id_input.text = "" self.id_input.text = ""
self.mass_input.text = "" self.mass_input.text = ""
self.last_update_label.text = 'Last update: never'
self.update_id_input.text = ""
self.update_mass_input.text = ""
self._pending_record_id = None
self.set_update_frame_enabled(False)
self.active_numpad_input = self.id_input self.active_numpad_input = self.id_input
self.id_input.focus = True self.id_input.focus = True
self.show_status("Fields cleared", error=False) self.show_status("Fields cleared", error=False)

View File

@@ -1,20 +0,0 @@
#!/bin/bash
# Startup script for the Database Manager Kivy App
echo "Starting Database Manager App..."
echo "================================"
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo "Virtual environment not found. Creating one..."
python3 -m venv venv
echo "Installing dependencies..."
source venv/bin/activate
pip install -r requirements.txt
else
source venv/bin/activate
fi
echo "Launching application..."
python main.py

10079
sept.csv

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +0,0 @@
#!/bin/bash
# MariaDB Database Setup Script
# This script helps set up the MariaDB database for the Kivy app
echo "MariaDB Database Setup for Kivy App"
echo "=================================="
echo
# Check if MariaDB is installed
if ! command -v mysql &> /dev/null; then
echo "❌ MariaDB is not installed."
echo "Please install MariaDB first:"
echo " sudo apt update"
echo " sudo apt install mariadb-server"
exit 1
fi
# Check if MariaDB service is running
if ! systemctl is-active --quiet mariadb; then
echo "⚠️ MariaDB service is not running."
echo "Starting MariaDB service..."
sudo systemctl start mariadb
if systemctl is-active --quiet mariadb; then
echo "✅ MariaDB service started successfully."
else
echo "❌ Failed to start MariaDB service."
exit 1
fi
else
echo "✅ MariaDB service is running."
fi
echo
echo "Setting up database and user..."
echo "You will be prompted for the MySQL root password."
echo
# Create the setup SQL script
cat > /tmp/setup_db.sql << EOF
CREATE DATABASE IF NOT EXISTS cantare_injectie;
CREATE USER IF NOT EXISTS 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
FLUSH PRIVILEGES;
USE cantare_injectie;
CREATE TABLE IF NOT EXISTS offsystemsCountin (
id VARCHAR(20) PRIMARY KEY,
mass REAL NOT NULL
);
-- Show the created database and table
SHOW DATABASES LIKE 'cantare_injectie';
DESCRIBE offsystemsCounting;
EOF
# Execute the SQL script
mysql -u root -p < /tmp/setup_db.sql
if [ $? -eq 0 ]; then
echo
echo "✅ Database setup completed successfully!"
echo
echo "Database details:"
echo " Host: localhost"
echo " Database: cantare_injectie"
echo " User: omron"
echo " Password: Initial01!"
echo " Table: offsystemsCounting"
echo
echo "You can now run the Kivy application:"
echo " python main.py"
echo
else
echo
echo "❌ Database setup failed."
echo "Please check the error messages above."
fi
# Clean up
rm -f /tmp/setup_db.sql

View File

@@ -1,8 +0,0 @@
-- Create user and grant privileges for MariaDB
CREATE USER IF NOT EXISTS 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
FLUSH PRIVILEGES;
-- Show the user and their privileges
SELECT User, Host FROM mysql.user WHERE User='omron';
SHOW GRANTS FOR 'omron'@'localhost';

View File

@@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the MariaDB Database Manager
This script tests the basic functionality without the GUI
"""
from database_manager import DatabaseManager
def test_database_operations():
print("Testing MariaDB Database Manager...")
print("-" * 40)
# Initialize database
db = DatabaseManager()
# Test 1: Add some initial data
print("1. Adding initial test data...")
test_data = [
("SYS001", 125.5),
("SYS002", 89.7),
("SYS003", 234.1)
]
for record_id, mass in test_data:
success = db.add_or_update_record(record_id, mass)
print(f" Added {record_id}: {'' if success else ''}")
print()
# Test 2: Read all data
print("2. Reading all data...")
records = db.read_all_data()
for record in records:
print(f" ID: {record[0]}, Mass: {record[1]}")
print(f" Total records: {len(records)}")
print()
# Test 3: Search for existing record
print("3. Searching for existing record 'SYS001'...")
result = db.search_by_id("SYS001")
if result:
print(f" Found: {result[0]} = {result[1]}")
else:
print(" Not found")
print()
# Test 4: Search for non-existing record
print("4. Searching for non-existing record 'SYS999'...")
result = db.search_by_id("SYS999")
if result:
print(f" Found: {result[0]} = {result[1]}")
else:
print(" Not found - this is expected!")
print()
# Test 5: Add the missing record
print("5. Adding the missing record 'SYS999'...")
success = db.add_or_update_record("SYS999", 456.8)
print(f" Added SYS999: {'' if success else ''}")
# Verify it was added
result = db.search_by_id("SYS999")
if result:
print(f" Verification: Found {result[0]} = {result[1]}")
print()
# Test 6: Update existing record
print("6. Updating existing record 'SYS001'...")
success = db.add_or_update_record("SYS001", 150.0)
print(f" Updated SYS001: {'' if success else ''}")
result = db.search_by_id("SYS001")
if result:
print(f" New value: {result[0]} = {result[1]}")
print()
# Test 7: Final count
print("7. Final database state...")
records = db.read_all_data()
print(f" Total records: {len(records)}")
for record in records:
print(f" {record[0]} = {record[1]}")
print()
# Test 8: Delete a record
print("8. Testing delete functionality...")
success = db.delete_record("SYS999")
print(f" Deleted SYS999: {'' if success else ''}")
final_records = db.read_all_data()
print(f" Final record count: {len(final_records)}")
print()
# Close connection
db.close_connection()
print("✓ All tests completed successfully!")
print("Note: Test records remain in the MariaDB database.")
if __name__ == "__main__":
test_database_operations()