file(GLOB_RECURSE TEST_DB_SOURCES
tests/test_databasemanager.cpp
+ tests/test_databasemanager.h
src/db/*.cpp
src/utils/*.cpp
)
target_link_libraries(test_databasemanager PRIVATE Qt${QT_VERSION_MAJOR}::Test Qt${QT_VERSION_MAJOR}::Sql)
add_test(NAME DatabaseManagerTest COMMAND test_databasemanager)
-add_executable(test_calculations tests/test_calculations.cpp)
+add_executable(test_calculations tests/test_calculations.cpp tests/test_calculations.h)
target_include_directories(test_calculations PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(test_calculations PRIVATE Qt${QT_VERSION_MAJOR}::Test)
add_test(NAME CalculationsTest COMMAND test_calculations)
--- /dev/null
+#ifndef RECIPEREPOSITORY_H
+#define RECIPEREPOSITORY_H
+
+#include <QDateTime>
+#include <QString>
+#include <vector>
+
+struct RecipeItem {
+ int id;
+ QString uuid;
+ QString name;
+ QString instructions;
+ QDateTime created;
+ double totalCalories = 0.0; // Calculated on the fly ideally
+};
+
+struct RecipeIngredient {
+ int foodId;
+ QString foodName;
+ double amount; // grams
+ // Potential for more info (calories contribution etc)
+};
+
+class RecipeRepository {
+public:
+ RecipeRepository();
+
+ // CRUD
+ int createRecipe(const QString& name, const QString& instructions = "");
+ bool updateRecipe(int id, const QString& name, const QString& instructions);
+ bool deleteRecipe(int id);
+
+ std::vector<RecipeItem> getAllRecipes();
+ RecipeItem getRecipe(int id);
+
+ // Ingredients
+ bool addIngredient(int recipeId, int foodId, double amount);
+ bool removeIngredient(int recipeId, int foodId);
+ bool updateIngredient(int recipeId, int foodId, double amount);
+ std::vector<RecipeIngredient> getIngredients(int recipeId);
+};
+
+#endif // RECIPEREPOSITORY_H
#include "widgets/dailylogwidget.h"
#include "widgets/detailswidget.h"
#include "widgets/mealwidget.h"
+#include "widgets/recipewidget.h"
#include "widgets/searchwidget.h"
class MainWindow : public QMainWindow {
SearchWidget* searchWidget;
DetailsWidget* detailsWidget;
MealWidget* mealWidget;
+ RecipeWidget* recipeWidget;
DailyLogWidget* dailyLogWidget;
FoodRepository repository;
--- /dev/null
+#ifndef RECIPEWIDGET_H
+#define RECIPEWIDGET_H
+
+#include <QLabel>
+#include <QLineEdit>
+#include <QListWidget>
+#include <QPushButton>
+#include <QTableWidget>
+#include <QTextEdit>
+#include <QWidget>
+
+#include "db/foodrepository.h"
+#include "db/reciperepository.h"
+
+class RecipeWidget : public QWidget {
+ Q_OBJECT
+
+public:
+ explicit RecipeWidget(QWidget* parent = nullptr);
+
+signals:
+ void recipeSelected(int recipeId);
+
+private slots:
+ void onNewRecipe();
+ void onSaveRecipe();
+ void onDeleteRecipe();
+ void onRecipeListSelectionChanged();
+ void onAddIngredient();
+ void onRemoveIngredient();
+
+private:
+ void setupUi();
+ void loadRecipes();
+ void loadRecipeDetails(int recipeId);
+ void clearDetails();
+
+ RecipeRepository repository;
+ FoodRepository foodRepo; // For ingredient search/lookup
+
+ QListWidget* recipeList;
+ QLineEdit* nameEdit;
+ QTableWidget* ingredientsTable;
+ QTextEdit* instructionsEdit;
+
+ QPushButton* saveButton;
+ QPushButton* deleteButton;
+ QPushButton* newButton;
+ QPushButton* addIngredientButton;
+ QPushButton* removeIngredientButton;
+
+ int currentRecipeId = -1;
+};
+
+#endif // RECIPEWIDGET_H
-Subproject commit acd5af5d0d87f7683086788ebcba94197cb5b660
+Subproject commit 97934ada10143640d4a3aa326cf1c9c8f245c65a
QString schemaPath = QDir::currentPath() + "/lib/ntsqlite/sql/tables.sql";
if (!QFileInfo::exists(schemaPath)) {
// Fallback for installed location
- schemaPath = "/usr/share/nutra/sql/tables.sql";
+ QString fallbackPath = "/usr/share/nutra/sql/tables.sql";
+ if (QFileInfo::exists(fallbackPath)) {
+ schemaPath = fallbackPath;
+ } else {
+ qCritical() << "Schema file not found at:" << schemaPath << "or" << fallbackPath;
+ return;
+ }
}
applySchema(query, schemaPath);
- } else {
- // Migration logic would go here
}
}
void DatabaseManager::applySchema(QSqlQuery& query, const QString& schemaPath) {
+ if (!QFileInfo::exists(schemaPath)) {
+ qCritical() << "applySchema: Schema file does not exist:" << schemaPath;
+ return;
+ }
QFile schemaFile(schemaPath);
if (!schemaFile.open(QIODevice::ReadOnly)) {
- qCritical() << "Could not find or open schema file:" << schemaPath;
+ qCritical() << "Could not open schema file:" << schemaPath;
return;
}
}
}
// Ensure version and ID are set
- query.exec(QString("PRAGMA user_version = %1").arg(USER_SCHEMA_VERSION));
- query.exec(QString("PRAGMA application_id = %1").arg(APP_ID_USER));
+ if (!query.exec(QString("PRAGMA user_version = %1").arg(USER_SCHEMA_VERSION))) {
+ qCritical() << "Failed to set user_version:" << query.lastError().text();
+ }
+ if (!query.exec(QString("PRAGMA application_id = %1").arg(APP_ID_USER))) {
+ qCritical() << "Failed to set application_id:" << query.lastError().text();
+ }
qDebug() << "Upgraded user database version to" << USER_SCHEMA_VERSION << "and set App ID.";
// --- Seeding Data ---
--- /dev/null
+#include "db/reciperepository.h"
+
+#include <QDebug>
+#include <QSqlDatabase>
+#include <QSqlError>
+#include <QSqlQuery>
+#include <QVariant>
+
+#include "db/databasemanager.h"
+
+RecipeRepository::RecipeRepository() {}
+
+int RecipeRepository::createRecipe(const QString& name, const QString& instructions) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return -1;
+
+ QSqlQuery query(db);
+ query.prepare("INSERT INTO recipe (name, instructions) VALUES (?, ?)");
+ query.addBindValue(name);
+ query.addBindValue(instructions);
+
+ if (query.exec()) {
+ return query.lastInsertId().toInt();
+ } else {
+ qCritical() << "Failed to create recipe:" << query.lastError().text();
+ return -1;
+ }
+}
+
+bool RecipeRepository::updateRecipe(int id, const QString& name, const QString& instructions) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return false;
+
+ QSqlQuery query(db);
+ query.prepare("UPDATE recipe SET name = ?, instructions = ? WHERE id = ?");
+ query.addBindValue(name);
+ query.addBindValue(instructions);
+ query.addBindValue(id);
+
+ return query.exec();
+}
+
+bool RecipeRepository::deleteRecipe(int id) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return false;
+
+ QSqlQuery query(db);
+ query.prepare("DELETE FROM recipe WHERE id = ?");
+ query.addBindValue(id);
+ return query.exec();
+}
+
+std::vector<RecipeItem> RecipeRepository::getAllRecipes() {
+ std::vector<RecipeItem> recipes;
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return recipes;
+
+ QSqlQuery query(db);
+ // TODO: Join with ingredient amounts * food nutrient values to get calories?
+ // For now, simple list.
+ if (query.exec("SELECT id, uuid, name, instructions, created FROM recipe ORDER BY name ASC")) {
+ while (query.next()) {
+ RecipeItem item;
+ item.id = query.value(0).toInt();
+ item.uuid = query.value(1).toString();
+ item.name = query.value(2).toString();
+ item.instructions = query.value(3).toString();
+ item.created = QDateTime::fromSecsSinceEpoch(query.value(4).toLongLong());
+ recipes.push_back(item);
+ }
+ } else {
+ qCritical() << "Failed to fetch recipes:" << query.lastError().text();
+ }
+ return recipes;
+}
+
+RecipeItem RecipeRepository::getRecipe(int id) {
+ RecipeItem item;
+ item.id = -1;
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return item;
+
+ QSqlQuery query(db);
+ query.prepare("SELECT id, uuid, name, instructions, created FROM recipe WHERE id = ?");
+ query.addBindValue(id);
+ if (query.exec() && query.next()) {
+ item.id = query.value(0).toInt();
+ item.uuid = query.value(1).toString();
+ item.name = query.value(2).toString();
+ item.instructions = query.value(3).toString();
+ item.created = QDateTime::fromSecsSinceEpoch(query.value(4).toLongLong());
+ }
+ return item;
+}
+
+bool RecipeRepository::addIngredient(int recipeId, int foodId, double amount) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return false;
+
+ QSqlQuery query(db);
+ query.prepare("INSERT INTO recipe_ingredient (recipe_id, food_id, amount) VALUES (?, ?, ?)");
+ query.addBindValue(recipeId);
+ query.addBindValue(foodId);
+ query.addBindValue(amount);
+
+ if (!query.exec()) {
+ qCritical() << "Failed to add ingredient:" << query.lastError().text();
+ return false;
+ }
+ return true;
+}
+
+bool RecipeRepository::removeIngredient(int recipeId, int foodId) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return false;
+
+ QSqlQuery query(db);
+ query.prepare("DELETE FROM recipe_ingredient WHERE recipe_id = ? AND food_id = ?");
+ query.addBindValue(recipeId);
+ query.addBindValue(foodId);
+ return query.exec();
+}
+
+bool RecipeRepository::updateIngredient(int recipeId, int foodId, double amount) {
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return false;
+
+ QSqlQuery query(db);
+ query.prepare("UPDATE recipe_ingredient SET amount = ? WHERE recipe_id = ? AND food_id = ?");
+ query.addBindValue(amount);
+ query.addBindValue(recipeId);
+ query.addBindValue(foodId);
+ return query.exec();
+}
+
+std::vector<RecipeIngredient> RecipeRepository::getIngredients(int recipeId) {
+ std::vector<RecipeIngredient> ingredients;
+ QSqlDatabase db = DatabaseManager::instance().userDatabase();
+ if (!db.isOpen()) return ingredients;
+
+ // We need to join with USDA db 'food_des' to get names?
+ // USDA db is attached as what? 'main' is User db usually?
+ // Wait, DatabaseManager opens main USDA db as 'db' (default connection) and User DB as
+ // 'user_db' (named connection). They are SEPARATE connections. Cross-database joins require
+ // attaching. DatabaseManager doesn't seem to attach them by default. workaround: Get IDs then
+ // query USDA db for names.
+
+ QSqlQuery query(db);
+ query.prepare("SELECT food_id, amount FROM recipe_ingredient WHERE recipe_id = ?");
+ query.addBindValue(recipeId);
+
+ if (query.exec()) {
+ while (query.next()) {
+ RecipeIngredient ing;
+ ing.foodId = query.value(0).toInt();
+ ing.amount = query.value(1).toDouble();
+
+ // Fetch name from main DB
+ // This is inefficient (N+1 queries), but simple for now without ATTACH logic.
+ // Or we could pass a list of IDs to FoodRepository.
+
+ QSqlDatabase usdaDb = DatabaseManager::instance().database();
+ if (usdaDb.isOpen()) {
+ QSqlQuery nameQuery(usdaDb);
+ nameQuery.prepare("SELECT long_desc FROM food_des WHERE ndb_no = ?");
+ nameQuery.addBindValue(ing.foodId);
+ if (nameQuery.exec() && nameQuery.next()) {
+ ing.foodName = nameQuery.value(0).toString();
+ } else {
+ ing.foodName = "Unknown Food (" + QString::number(ing.foodId) + ")";
+ }
+ }
+ ingredients.push_back(ing);
+ }
+ }
+ return ingredients;
+}
QApplication::setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png"));
// Prevent multiple instances
- QString lockPath =
- QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/nutra.lock";
+ QString lockPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
+ if (lockPath.isEmpty()) {
+ lockPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
+ }
+ lockPath += "/nutra.lock";
QLockFile lockFile(lockPath);
if (!lockFile.tryLock(100)) {
QMessageBox::warning(nullptr, "Nutra is already running",
#include "db/databasemanager.h"
#include "widgets/preferencesdialog.h"
#include "widgets/rdasettingswidget.h"
+#include "widgets/recipewidget.h"
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
for (auto& recentFileAction : recentFileActions) {
mealWidget = new MealWidget(this);
tabs->addTab(mealWidget, "Meal Builder");
+ // Recipes Tab
+ recipeWidget = new RecipeWidget(this);
+ tabs->addTab(recipeWidget, "Recipes");
+
// Daily Log Tab
dailyLogWidget = new DailyLogWidget(this);
tabs->addTab(dailyLogWidget, "Daily Log");
// Activity Level
activitySlider = new QSlider(Qt::Horizontal, this);
activitySlider->setRange(1, 5);
+ activitySlider->setValue(2); // Default to Lightly Active
activitySlider->setTickPosition(QSlider::TicksBelow);
activitySlider->setTickInterval(1);
--- /dev/null
+#include "widgets/recipewidget.h"
+
+#include <QDebug>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QInputDialog>
+#include <QMessageBox>
+#include <QSplitter>
+#include <QVBoxLayout>
+
+// Simple dialog to pick a food (MVP specific for ingredients)
+// Ideally we reuse SearchWidget, but wrapping it might be cleaner later.
+// For now, let's use a simple input dialog for ID, or we can make a tiny search dialog.
+#include <QDialog>
+
+#include "widgets/searchwidget.h"
+
+class IngredientSearchDialog : public QDialog {
+public:
+ IngredientSearchDialog(QWidget* parent) : QDialog(parent) {
+ setWindowTitle("Add Ingredient");
+ resize(600, 400);
+ auto* layout = new QVBoxLayout(this);
+ searchWidget = new SearchWidget(this);
+ layout->addWidget(searchWidget);
+
+ connect(searchWidget, &SearchWidget::foodSelected, this,
+ [this](int id, const QString& name) {
+ selectedFoodId = id;
+ selectedFoodName = name;
+ accept();
+ });
+ }
+
+ int selectedFoodId = -1;
+ QString selectedFoodName;
+ SearchWidget* searchWidget;
+};
+
+RecipeWidget::RecipeWidget(QWidget* parent) : QWidget(parent) {
+ setupUi();
+ loadRecipes();
+}
+
+void RecipeWidget::setupUi() {
+ auto* mainLayout = new QHBoxLayout(this);
+
+ auto* splitter = new QSplitter(Qt::Horizontal, this);
+ mainLayout->addWidget(splitter);
+
+ // Left Pane: Recipe List
+ auto* leftWidget = new QWidget();
+ auto* leftLayout = new QVBoxLayout(leftWidget);
+ leftLayout->setContentsMargins(0, 0, 0, 0);
+
+ leftLayout->addWidget(new QLabel("Recipes", this));
+ recipeList = new QListWidget(this);
+ leftLayout->addWidget(recipeList);
+
+ newButton = new QPushButton("New Recipe", this);
+ leftLayout->addWidget(newButton);
+
+ leftWidget->setLayout(leftLayout);
+ splitter->addWidget(leftWidget);
+
+ // Right Pane: Details
+ auto* rightWidget = new QWidget();
+ auto* rightLayout = new QVBoxLayout(rightWidget);
+ rightLayout->setContentsMargins(0, 0, 0, 0);
+
+ // Name
+ auto* nameLayout = new QHBoxLayout();
+ nameLayout->addWidget(new QLabel("Name:", this));
+ nameEdit = new QLineEdit(this);
+ nameLayout->addWidget(nameEdit);
+ rightLayout->addLayout(nameLayout);
+
+ // Ingredients
+ rightLayout->addWidget(new QLabel("Ingredients:", this));
+ ingredientsTable = new QTableWidget(this);
+ ingredientsTable->setColumnCount(3);
+ ingredientsTable->setHorizontalHeaderLabels({"ID", "Food", "Amount (g)"});
+ ingredientsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ ingredientsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ rightLayout->addWidget(ingredientsTable);
+
+ auto* ingButtonsLayout = new QHBoxLayout();
+ addIngredientButton = new QPushButton("Add Ingredient", this);
+ removeIngredientButton = new QPushButton("Remove Selected", this);
+ ingButtonsLayout->addWidget(addIngredientButton);
+ ingButtonsLayout->addWidget(removeIngredientButton);
+ ingButtonsLayout->addStretch();
+ rightLayout->addLayout(ingButtonsLayout);
+
+ // Instructions
+ rightLayout->addWidget(new QLabel("Instructions:", this));
+ instructionsEdit = new QTextEdit(this);
+ rightLayout->addWidget(instructionsEdit);
+
+ // Action Buttons
+ auto* actionLayout = new QHBoxLayout();
+ saveButton = new QPushButton("Save", this);
+ deleteButton = new QPushButton("Delete", this);
+ // deleteButton->setStyleSheet("background-color: #e74c3c; color: white;"); // User prefers
+ // styled?
+
+ actionLayout->addStretch();
+ actionLayout->addWidget(deleteButton);
+ actionLayout->addWidget(saveButton);
+ rightLayout->addLayout(actionLayout);
+
+ rightWidget->setLayout(rightLayout);
+ splitter->addWidget(rightWidget);
+
+ splitter->setStretchFactor(1, 2); // Give details more space
+
+ // Connections
+ connect(newButton, &QPushButton::clicked, this, &RecipeWidget::onNewRecipe);
+ connect(recipeList, &QListWidget::itemSelectionChanged, this,
+ &RecipeWidget::onRecipeListSelectionChanged);
+ connect(saveButton, &QPushButton::clicked, this, &RecipeWidget::onSaveRecipe);
+ connect(deleteButton, &QPushButton::clicked, this, &RecipeWidget::onDeleteRecipe);
+ connect(addIngredientButton, &QPushButton::clicked, this, &RecipeWidget::onAddIngredient);
+ connect(removeIngredientButton, &QPushButton::clicked, this, &RecipeWidget::onRemoveIngredient);
+
+ clearDetails();
+}
+
+void RecipeWidget::loadRecipes() {
+ recipeList->clear();
+ auto recipes = repository.getAllRecipes();
+ for (const auto& r : recipes) {
+ auto* item = new QListWidgetItem(r.name, recipeList);
+ item->setData(Qt::UserRole, r.id);
+ }
+}
+
+void RecipeWidget::onRecipeListSelectionChanged() {
+ auto items = recipeList->selectedItems();
+ if (items.isEmpty()) {
+ clearDetails();
+ return;
+ }
+ int id = items.first()->data(Qt::UserRole).toInt();
+ loadRecipeDetails(id);
+}
+
+void RecipeWidget::loadRecipeDetails(int recipeId) {
+ currentRecipeId = recipeId;
+ RecipeItem item = repository.getRecipe(recipeId);
+ if (item.id == -1) return; // Error
+
+ nameEdit->setText(item.name);
+ instructionsEdit->setText(item.instructions);
+
+ // Load ingredients
+ ingredientsTable->setRowCount(0);
+ auto ingredients = repository.getIngredients(recipeId);
+ ingredientsTable->setRowCount(ingredients.size());
+ for (int i = 0; i < ingredients.size(); ++i) {
+ const auto& ing = ingredients[i];
+ ingredientsTable->setItem(i, 0, new QTableWidgetItem(QString::number(ing.foodId)));
+ ingredientsTable->setItem(i, 1, new QTableWidgetItem(ing.foodName));
+ ingredientsTable->setItem(i, 2, new QTableWidgetItem(QString::number(ing.amount)));
+ }
+
+ saveButton->setEnabled(true);
+ deleteButton->setEnabled(true);
+}
+
+void RecipeWidget::clearDetails() {
+ currentRecipeId = -1;
+ nameEdit->clear();
+ instructionsEdit->clear();
+ ingredientsTable->setRowCount(0);
+ saveButton->setEnabled(false);
+ deleteButton->setEnabled(false);
+}
+
+void RecipeWidget::onNewRecipe() {
+ recipeList->clearSelection();
+ clearDetails();
+ nameEdit->setFocus();
+ saveButton->setEnabled(true); // Allow saving a new one
+}
+
+void RecipeWidget::onSaveRecipe() {
+ QString name = nameEdit->text().trimmed();
+ if (name.isEmpty()) {
+ QMessageBox::warning(this, "Validation Error", "Recipe name cannot be empty.");
+ return;
+ }
+ QString instructions = instructionsEdit->toPlainText();
+
+ if (currentRecipeId == -1) {
+ // Create
+ int newId = repository.createRecipe(name, instructions);
+ if (newId != -1) {
+ currentRecipeId = newId;
+ // Add ingredients from table if any (though usually table is empty on new)
+ // But if user added ingredients before saving, we should handle that.
+ // Currently, adding ingredient requires a recipe ID?
+ // If strict: enforce save before adding ingredients.
+ // Or: keep ingredients in memory until save.
+ // For MVP: Simplest is: Create Recipe -> Then Add Ingredients.
+ // So if new, save creates empty recipe, then reloads it, allowing adds.
+ loadRecipes();
+ // Select the new item
+ for (int i = 0; i < recipeList->count(); ++i) {
+ if (recipeList->item(i)->data(Qt::UserRole).toInt() == newId) {
+ recipeList->setCurrentRow(i);
+ break;
+ }
+ }
+ }
+ } else {
+ // Update
+ repository.updateRecipe(currentRecipeId, name, instructions);
+
+ // Update ingredients?
+ // Ingredients are updated immediately in onAdd/RemoveIngredient for now?
+ // Or we should do batch save.
+ // Current implementation of onAddIngredient writes to DB immediately.
+ // So here just update name/instructions.
+
+ // Refresh list name if changed
+ auto items = recipeList->findItems(name, Qt::MatchExactly); // This might find others?
+ // Just reload list to be safe or update current item
+ if (!recipeList->selectedItems().isEmpty()) {
+ recipeList->selectedItems().first()->setText(name);
+ }
+ }
+}
+
+void RecipeWidget::onDeleteRecipe() {
+ if (currentRecipeId == -1) return;
+
+ auto reply = QMessageBox::question(this, "Confirm Delete",
+ "Are you sure you want to delete this recipe?",
+ QMessageBox::Yes | QMessageBox::No);
+ if (reply == QMessageBox::Yes) {
+ repository.deleteRecipe(currentRecipeId);
+ loadRecipes();
+ clearDetails();
+ }
+}
+
+void RecipeWidget::onAddIngredient() {
+ if (currentRecipeId == -1) {
+ QMessageBox::information(this, "Save Required",
+ "Please save the recipe before adding ingredients.");
+ return;
+ }
+
+ IngredientSearchDialog dlg(this);
+ if (dlg.exec() == QDialog::Accepted) {
+ int foodId = dlg.selectedFoodId;
+ QString foodName = dlg.selectedFoodName;
+
+ // Ask for amount
+ bool ok;
+ double amount = QInputDialog::getDouble(
+ this, "Ingredient Amount", QString("Enter amount (grams) for %1:").arg(foodName), 100.0,
+ 0.1, 10000.0, 1, &ok);
+
+ if (ok) {
+ if (repository.addIngredient(currentRecipeId, foodId, amount)) {
+ loadRecipeDetails(currentRecipeId); // Refresh table
+ }
+ }
+ }
+}
+
+void RecipeWidget::onRemoveIngredient() {
+ if (currentRecipeId == -1) return;
+
+ int row = ingredientsTable->currentRow();
+ if (row < 0) return;
+
+ int foodId = ingredientsTable->item(row, 0)->text().toInt();
+
+ if (repository.removeIngredient(currentRecipeId, foodId)) {
+ ingredientsTable->removeRow(row);
+ }
+}
}
void SearchWidget::onCompleterActivated(const QString& text) {
+ searchInput->blockSignals(true);
searchInput->setText(text);
+ searchInput->blockSignals(false);
performSearch();
}
-#include <QtTest>
+#include "test_calculations.h"
-class TestCalculations : public QObject {
- Q_OBJECT
+void TestCalculations::testBMR() {
+ // TDD: Fail mainly because not implemented
+ QEXPECT_FAIL("", "BMR calculation not yet implemented", Continue);
+ QVERIFY(false);
+}
-private slots:
- void testBMR() {
- // TDD: Fail mainly because not implemented
- QEXPECT_FAIL("", "BMR calculation not yet implemented", Continue);
- QVERIFY(false);
- }
-
- void testBodyFat() {
- QEXPECT_FAIL("", "Body Fat calculation not yet implemented", Continue);
- QVERIFY(false);
- }
-};
+void TestCalculations::testBodyFat() {
+ QEXPECT_FAIL("", "Body Fat calculation not yet implemented", Continue);
+ QVERIFY(false);
+}
QTEST_MAIN(TestCalculations)
-#include "test_calculations.moc"
--- /dev/null
+#ifndef TEST_CALCULATIONS_H
+#define TEST_CALCULATIONS_H
+
+#include <QObject>
+#include <QtTest>
+
+class TestCalculations : public QObject {
+ Q_OBJECT
+
+private slots:
+ void testBMR();
+ void testBodyFat();
+};
+
+#endif // TEST_CALCULATIONS_H
+#include "test_databasemanager.h"
+
#include <QDir>
#include <QFileInfo>
#include <QSqlError>
#include <QSqlQuery>
-#include <QtTest>
#include "db/databasemanager.h"
-class TestDatabaseManager : public QObject {
- Q_OBJECT
-
-private slots:
- void testUserDatabaseInit() {
- // Use a temporary database path
- QString dbPath = QDir::tempPath() + "/nutra_test_db.sqlite3";
- if (QFileInfo::exists(dbPath)) {
- QFile::remove(dbPath);
- }
-
- // We can't easily instruct DatabaseManager to use a specific path for userDatabase()
- // without modifying it to accept a path injection or using a mock.
- // However, `DatabaseManager::connect` allows opening arbitrary databases.
-
- // Let's test the validity check on a fresh DB.
-
- QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "test_connection");
- db.setDatabaseName(dbPath);
- QVERIFY(db.open());
+void TestDatabaseManager::testUserDatabaseInit() {
+ // Use a temporary database path
+ QString dbPath = QDir::tempPath() + "/nutra_test_db.sqlite3";
+ if (QFileInfo::exists(dbPath)) {
+ QFile::remove(dbPath);
+ }
- // Initialize schema manually (simulating initUserDatabase behavior if we can't invoke it
- // directly) OR, verify the one in ~/.nutra if we want integration test. Let's assume we
- // want to verify the logic in DatabaseManager::getDatabaseInfo which requires a db on disk.
+ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "test_connection");
+ db.setDatabaseName(dbPath);
+ QVERIFY(db.open());
- // Let's create a minimal valid user DB
- QSqlQuery q(db);
- q.exec("PRAGMA application_id = 1314145346"); // 'NTDB'
- q.exec("PRAGMA user_version = 9");
- q.exec("CREATE TABLE log_food (id int)");
+ // Initialize schema manually (simulating initUserDatabase behavior)
+ QSqlQuery q(db);
+ q.exec("PRAGMA application_id = 1314145346"); // 'NTDB'
+ q.exec("PRAGMA user_version = 9");
+ q.exec("CREATE TABLE log_food (id int)");
- db.close();
+ db.close();
- auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
- QCOMPARE(info.type, QString("User"));
- QVERIFY(info.isValid);
- QCOMPARE(info.version, 9);
+ auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
+ QCOMPARE(info.type, QString("User"));
+ QVERIFY(info.isValid);
+ QCOMPARE(info.version, 9);
- QFile::remove(dbPath);
- }
+ QSqlDatabase::removeDatabase("test_connection");
+ QFile::remove(dbPath);
+}
- void testInvalidDatabase() {
- QString dbPath = QDir::tempPath() + "/nutra_invalid.sqlite3";
- if (QFileInfo::exists(dbPath)) QFile::remove(dbPath);
+void TestDatabaseManager::testInvalidDatabase() {
+ QString dbPath = QDir::tempPath() + "/nutra_invalid.sqlite3";
+ if (QFileInfo::exists(dbPath)) QFile::remove(dbPath);
- QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "invalid_conn");
- db.setDatabaseName(dbPath);
- QVERIFY(db.open());
- // Empty DB
- db.close();
+ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "invalid_conn");
+ db.setDatabaseName(dbPath);
+ QVERIFY(db.open());
+ // Empty DB
+ db.close();
- auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
- QVERIFY(info.isValid == false);
+ auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
+ QVERIFY(info.isValid == false);
- QFile::remove(dbPath);
- }
-};
+ QSqlDatabase::removeDatabase("invalid_conn");
+ QFile::remove(dbPath);
+}
QTEST_MAIN(TestDatabaseManager)
-#include "test_databasemanager.moc"
--- /dev/null
+#ifndef TEST_DATABASEMANAGER_H
+#define TEST_DATABASEMANAGER_H
+
+#include <QObject>
+#include <QtTest>
+
+class TestDatabaseManager : public QObject {
+ Q_OBJECT
+
+private slots:
+ void testUserDatabaseInit();
+ void testInvalidDatabase();
+};
+
+#endif // TEST_DATABASEMANAGER_H