diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index f974db170b..cc225c4feb 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -3806,6 +3806,10 @@ Supported extensions are: %1.
Search toggle for this and sub groups
+
+ Exclude from database reports
+
+
EditWidgetIcons
@@ -9499,6 +9503,47 @@ This option is deprecated, use --set-key-file instead.
+
+ ReportsWidgetBase
+
+ Please wait, report is being calculated…
+
+
+
+ Edit Entry…
+
+
+
+ Expire Entry(s)…
+
+
+
+
+
+
+ Delete Entry(s)…
+
+
+
+
+
+
+ Exclude Entry(s) from reports
+
+
+
+ Exclude Group(s) from reports
+
+
+
+ The Group for "%1" is excluded. Would you like to include all Entries from there as well?
+
+
+
+ Include Group?
+
+
+
ReportsWidgetBrowserStatistics
@@ -9533,10 +9578,6 @@ This option is deprecated, use --set-key-file instead.
This entry is being excluded from reports
-
- Please wait, browser statistics is being calculated…
-
-
No entries with a URL, or none has browser extension settings saved.
@@ -9553,28 +9594,6 @@ This option is deprecated, use --set-key-file instead.
URLs
-
- Edit Entry…
-
-
-
- Delete Entry(s)…
-
-
-
-
-
-
- Exclude from reports
-
-
-
- Expire Entry(s)…
-
-
-
-
-
Only show entries that have a URL
@@ -9598,6 +9617,14 @@ This option is deprecated, use --set-key-file instead.
+
+ (Group Excluded)
+
+
+
+ The group for this entry is being excluded from reports
+
+
ReportsWidgetHealthcheck
@@ -9633,10 +9660,6 @@ This option is deprecated, use --set-key-file instead.
This entry is being excluded from reports
-
- Please wait, health data is being calculated…
-
-
Congratulations, everything is healthy!
@@ -9658,29 +9681,15 @@ This option is deprecated, use --set-key-file instead.
- Edit Entry…
+ Show entries that have been excluded from reports
-
- Delete Entry(s)…
-
-
-
-
-
- Exclude from reports
+ (Group Excluded)
-
- Expire Entry(s)…
-
-
-
-
-
- Show entries that have been excluded from reports
+ The group for this entry is being excluded from reports
@@ -9767,27 +9776,13 @@ This option is deprecated, use --set-key-file instead.
- Edit Entry…
+ (Group Excluded)
-
- Delete Entry(s)…
-
-
-
-
-
- Exclude from reports
+ The group for this entry is being excluded from reports
-
- Expire Entry(s)…
-
-
-
-
-
ReportsWidgetPasskeys
@@ -9850,10 +9845,6 @@ This option is deprecated, use --set-key-file instead.
The passkey file will be vulnerable to theft and unauthorized use, if left unsecured. Are you sure you want to continue?
-
- Please wait, list of entries with passkeys is being updated…
-
-
No entries with passkeys.
@@ -9988,6 +9979,14 @@ This option is deprecated, use --set-key-file instead.
+
+ Groups excluded from reports
+
+
+
+ Excluding entire groups from reports isn't necessarily a problem but please exercise caution when excluding entire groups.
+
+
SSHAgent
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6a5eb5d801..c16a25b8d7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -194,6 +194,7 @@ set(gui_SOURCES
gui/remote/RemoteProcess.cpp
gui/remote/RemoteSettings.cpp
gui/reports/ReportsWidget.cpp
+ gui/reports/ReportsWidgetBase.cpp
gui/reports/ReportsDialog.cpp
gui/reports/ReportsWidgetHealthcheck.cpp
gui/reports/ReportsPageHealthcheck.cpp
diff --git a/src/core/DatabaseStats.cpp b/src/core/DatabaseStats.cpp
index cf2364b08f..fbe985cf63 100644
--- a/src/core/DatabaseStats.cpp
+++ b/src/core/DatabaseStats.cpp
@@ -76,6 +76,10 @@ void DatabaseStats::gatherStats(const QList& groups)
++groupCount;
+ if (group->excludeFromReports()) {
+ ++excludedGroups;
+ }
+
for (const auto* entry : group->entries()) {
// Don't count anything in the recycle bin
if (entry->isRecycled()) {
@@ -107,7 +111,7 @@ void DatabaseStats::gatherStats(const QList& groups)
++weakPasswords;
}
- if (entry->excludeFromReports()) {
+ if (entry->excludeFromReports() || group->excludeFromReports()) {
++excludedEntries;
}
diff --git a/src/core/DatabaseStats.h b/src/core/DatabaseStats.h
index 2c0ad7c767..d6446e8341 100644
--- a/src/core/DatabaseStats.h
+++ b/src/core/DatabaseStats.h
@@ -30,6 +30,7 @@ class DatabaseStats
int entryCount = 0; // Number of entries (across all groups)
int expiredEntries = 0; // Number of expired entries
int excludedEntries = 0; // Number of known bad entries
+ int excludedGroups = 0; // Number of excluded groups from reports
int weakPasswords = 0; // Number of weak or poor passwords
int shortPasswords = 0; // Number of passwords 8 characters or less in size
int uniquePasswords = 0; // Number of unique passwords
diff --git a/src/core/Group.cpp b/src/core/Group.cpp
index 8b6cd75c51..29c765a6af 100644
--- a/src/core/Group.cpp
+++ b/src/core/Group.cpp
@@ -1281,6 +1281,33 @@ void Group::setPreviousParentGroup(const Group* group)
setPreviousParentGroupUuid(group ? group->uuid() : QUuid());
}
+void Group::setExcludeFromReports(bool excluded)
+{
+ customData()->set(CustomData::ExcludeFromReportsLegacy, excluded ? TRUE_STR : FALSE_STR);
+
+ // Clear out the exclusion flag on entries when we set it on the
+ // group because it'll make it easier to individually set it for an
+ // entry later on
+ if (excluded) {
+ for (auto& entry : m_entries) {
+ entry->setExcludeFromReports(false);
+ }
+ }
+}
+
+void Group::markAllEntriesExcludedFromReports()
+{
+ for (auto& entry : m_entries) {
+ entry->setExcludeFromReports(true);
+ }
+}
+
+bool Group::excludeFromReports() const
+{
+ return customData()->contains(CustomData::ExcludeFromReportsLegacy)
+ && customData()->value(CustomData::ExcludeFromReportsLegacy) == TRUE_STR;
+}
+
bool Group::GroupData::operator==(const Group::GroupData& other) const
{
return equals(other, CompareItemDefault);
diff --git a/src/core/Group.h b/src/core/Group.h
index 01c0b21202..b6cfbfe71f 100644
--- a/src/core/Group.h
+++ b/src/core/Group.h
@@ -107,6 +107,7 @@ class Group : public ModifiableObject
QString resolveCustomDataString(const QString& key, bool checkParent = true) const;
const Group* previousParentGroup() const;
QUuid previousParentGroupUuid() const;
+ bool excludeFromReports() const;
bool equals(const Group* other, CompareItemOptions options) const;
@@ -140,6 +141,8 @@ class Group : public ModifiableObject
void setMergeMode(MergeMode newMode);
void setPreviousParentGroup(const Group* group);
void setPreviousParentGroupUuid(const QUuid& uuid);
+ void setExcludeFromReports(bool exclude);
+ void markAllEntriesExcludedFromReports();
bool canUpdateTimeinfo() const;
void setUpdateTimeinfo(bool value);
diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp
index 4e406b0d0a..77c51e7776 100644
--- a/src/gui/group/EditGroupWidget.cpp
+++ b/src/gui/group/EditGroupWidget.cpp
@@ -126,6 +126,7 @@ void EditGroupWidget::setupModifiedTracking()
connect(m_mainUi->autoTypeSequenceInherit, SIGNAL(toggled(bool)), SLOT(setModified()));
connect(m_mainUi->autoTypeSequenceCustomRadio, SIGNAL(toggled(bool)), SLOT(setModified()));
connect(m_mainUi->autoTypeSequenceCustomEdit, SIGNAL(textChanged(QString)), SLOT(setModified()));
+ connect(m_mainUi->excludeReportsCheckBox, SIGNAL(stateChanged(int)), SLOT(setModified()));
// Icon tab
connect(m_editGroupWidgetIcons, SIGNAL(widgetUpdated()), SLOT(setModified()));
@@ -171,6 +172,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
m_mainUi->autoTypeSequenceCustomRadio->setChecked(true);
}
m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence());
+ m_mainUi->excludeReportsCheckBox->setChecked(m_group->excludeFromReports());
if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
m_mainUi->editNotes->setFont(Font::fixedFont());
@@ -265,6 +267,7 @@ void EditGroupWidget::apply()
m_temporaryGroup->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex()));
m_temporaryGroup->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex()));
+ m_temporaryGroup->setExcludeFromReports(m_mainUi->excludeReportsCheckBox->isChecked());
if (m_mainUi->autoTypeSequenceInherit->isChecked()) {
m_temporaryGroup->setDefaultAutoTypeSequence(QString());
diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui
index faa8a30ff3..26af28b6e0 100644
--- a/src/gui/group/EditGroupWidgetMain.ui
+++ b/src/gui/group/EditGroupWidgetMain.ui
@@ -37,7 +37,7 @@
523
-
+
0
@@ -56,7 +56,7 @@
8
- -
+
-
Toggle expiration
@@ -66,10 +66,10 @@
- -
-
-
- Name field
+
-
+
+
+ Set default Auto-Type sequence
@@ -86,30 +86,10 @@
- -
-
-
- Use default Auto-Type sequence of parent group
-
-
-
- -
-
-
- Auto-Type:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
-
- -
-
-
- Search:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
-
+
+
+ Name field
@@ -120,6 +100,13 @@
+ -
+
+
+ Search toggle for this and sub groups
+
+
+
-
-
@@ -199,42 +186,62 @@
- -
-
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
- Name:
+ Search:
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
- -
-
+
-
+
- Set default Auto-Type sequence
+ Auto-Type:
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
- -
-
-
- Search toggle for this and sub groups
+
-
+
+
+ Use default Auto-Type sequence of parent group
- -
-
-
- Qt::Vertical
+
-
+
+
+ Name:
-
-
- 20
- 40
-
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
+
+ -
+
+
+ Exclude from database reports
+
+
diff --git a/src/gui/reports/ProxyModels.h b/src/gui/reports/ProxyModels.h
new file mode 100644
index 0000000000..6311afeff2
--- /dev/null
+++ b/src/gui/reports/ProxyModels.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_PROXYMODELS_H
+#define KEEPASSXC_PROXYMODELS_H
+
+#include
+#include
+
+enum class SortProxyModelKind
+{
+ Default = 0,
+ Hibp,
+ Healthcheck,
+};
+
+class HibpReportSortProxyModel : public QSortFilterProxyModel
+{
+public:
+ HibpReportSortProxyModel(QObject* parent)
+ : QSortFilterProxyModel(parent)
+ {
+ }
+ ~HibpReportSortProxyModel() override = default;
+
+protected:
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+ {
+ // Sort count column by user data
+ if (left.column() == 2) {
+ return sourceModel()->data(left, Qt::UserRole).toInt() < sourceModel()->data(right, Qt::UserRole).toInt();
+ }
+ // Otherwise use default sorting
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+};
+
+class HealthcheckReportSortProxyModel : public QSortFilterProxyModel
+{
+public:
+ HealthcheckReportSortProxyModel(QObject* parent)
+ : QSortFilterProxyModel(parent)
+ {
+ }
+ ~HealthcheckReportSortProxyModel() override = default;
+
+protected:
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+ {
+ // Check if the display data is a number, convert and compare if so
+ bool ok = false;
+ int leftInt = sourceModel()->data(left).toString().toInt(&ok);
+ if (ok) {
+ return leftInt < sourceModel()->data(right).toString().toInt();
+ }
+ // Otherwise use default sorting
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+};
+
+#endif // KEEPASSXC_PROXYMODELS_H
diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp
new file mode 100644
index 0000000000..02b469c66b
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetBase.cpp
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "ReportsWidgetBase.h"
+
+#include "core/Group.h"
+#include "core/Metadata.h"
+#include "gui/GuiTools.h"
+#include "gui/Icons.h"
+#include "gui/MessageBox.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyModel)
+ : QWidget{parent}
+ , m_referencesModel(new QStandardItemModel(this))
+{
+ // We have to initialize this here; if we do it in the constructor initializer list,
+ // the base object isn't setup enough and the constructor for QSortFilterProxyModel
+ // crashes.
+ switch (proxyModel) {
+ case SortProxyModelKind::Default:
+ m_modelProxy.reset(new QSortFilterProxyModel(this));
+ break;
+ case SortProxyModelKind::Healthcheck:
+ m_modelProxy.reset(new HealthcheckReportSortProxyModel(this));
+ break;
+ case SortProxyModelKind::Hibp:
+ m_modelProxy.reset(new HibpReportSortProxyModel(this));
+ break;
+ }
+}
+
+ReportsWidgetBase::~ReportsWidgetBase()
+{
+}
+
+void ReportsWidgetBase::loadSettings(QSharedPointer db)
+{
+ m_db = std::move(db);
+ m_widgetDataCalculated = false;
+ m_referencesModel->clear();
+ m_rowToEntry.clear();
+
+ auto row = QList();
+ row << new QStandardItem(tr("Please wait, report is being calculated…"));
+ m_referencesModel->appendRow(row);
+}
+
+void ReportsWidgetBase::saveSettings()
+{
+ // Most report tabs are passive, so override them in derived classes if they need to
+ // save settings
+}
+
+QMenu* ReportsWidgetBase::customMenuRequestedBase()
+{
+ auto selected = getTableView()->selectionModel()->selectedRows();
+ if (selected.isEmpty()) {
+ return nullptr;
+ }
+
+ // Create the context menu
+ const auto menu = new QMenu(this);
+ menu->setObjectName("customMenu");
+
+ // Create the "edit entry" menu item (only if 1 row is selected)
+ if (selected.size() == 1) {
+ const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
+ edit->setObjectName("contextMenuEditAction");
+ menu->addAction(edit);
+ connect(edit, &QAction::triggered, edit, [this, selected] {
+ auto row = m_modelProxy->mapToSource(selected[0]).row();
+ auto entry = m_rowToEntry[row].second;
+ emit entryActivated(entry);
+ });
+ }
+
+ // Create the "Expire entry" menu item
+ const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
+ expEntry->setObjectName("contextMenuExpireAction");
+ menu->addAction(expEntry);
+ connect(expEntry, &QAction::triggered, this, &ReportsWidgetBase::expireSelectedEntries);
+
+ // Create the "delete entry" menu item
+ const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
+ menu->addAction(delEntry);
+ connect(delEntry, &QAction::triggered, this, &ReportsWidgetBase::deleteSelectedEntries);
+
+ // Create the "exclude from reports" menu item
+ const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this);
+ excludeAction->setObjectName("contextMenuExcludeAction");
+ const auto excludeGroupsAction =
+ new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this);
+ excludeGroupsAction->setObjectName("contextMenuExcludeGroupAction");
+
+ bool isExcluded = false;
+ bool isGroupExcluded = false;
+
+ for (auto index : selected) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+ if (entry) {
+ // If at least one entry is excluded switch to inclusion
+ if (entry->excludeFromReports() || entry->group()->excludeFromReports()) {
+ isExcluded = true;
+ }
+ if (entry->group()->excludeFromReports()) {
+ isGroupExcluded = true;
+ }
+
+ break;
+ }
+ }
+ excludeAction->setCheckable(true);
+ excludeAction->setChecked(isExcluded);
+
+ excludeGroupsAction->setCheckable(true);
+ excludeGroupsAction->setChecked(isGroupExcluded);
+
+ menu->addAction(excludeAction);
+ connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) {
+ QSet groups;
+
+ // If we are including entries (checked is false) but a group is excluded, ask the user if they
+ // would like to include the rest of the group as well (or keep it excluded).
+ // If they choose "No", we need to include the whole group, and then exclude
+ // the entries that aren't selected here.
+ if (!checked) {
+ for (const auto index : selected) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+
+ if (entry) {
+ auto* group = entry->group();
+ if (group->excludeFromReports() && !groups.contains(group)) {
+ QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from "
+ "there as well?")
+ .arg(entry->title());
+ auto response = MessageBox::question(this,
+ tr("Include Group?"),
+ msg,
+ MessageBox::Yes | MessageBox::No | MessageBox::Cancel,
+ MessageBox::No);
+
+ if (response == MessageBox::Cancel) {
+ return;
+ } else if (response == MessageBox::Yes) {
+ group->setExcludeFromReports(false);
+ } else if (response == MessageBox::No) {
+ // We'll exclude all entries from the group here and then
+ // include the selected ones below
+ group->setExcludeFromReports(false);
+ group->markAllEntriesExcludedFromReports();
+ }
+
+ groups.insert(group);
+ }
+ }
+ }
+ }
+
+ for (auto index : selected) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+
+ // If the containing group is excluded but the user wants to include
+ // this entry, ask if they want to keep the remaining items in the group
+ // excluded or included
+ if (entry) {
+ entry->setExcludeFromReports(checked);
+ }
+ }
+ updateWidget();
+ });
+
+ menu->addAction(excludeGroupsAction);
+ connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) {
+ for (const auto index : selected) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+ if (entry) {
+ entry->group()->setExcludeFromReports(checked);
+ }
+ }
+ updateWidget();
+ });
+
+ return menu;
+}
+
+QList ReportsWidgetBase::getSelectedEntries() const
+{
+ QList selectedEntries;
+ for (auto index : getTableView()->selectionModel()->selectedRows()) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+ if (entry) {
+ selectedEntries << entry;
+ }
+ }
+ return selectedEntries;
+}
+
+void ReportsWidgetBase::expireSelectedEntries()
+{
+ for (auto entry : getSelectedEntries()) {
+ entry->expireNow();
+ }
+
+ updateWidget();
+}
+
+void ReportsWidgetBase::deleteSelectedEntries()
+{
+ const auto& selectedEntries = getSelectedEntries();
+ bool permanent = !m_db->metadata()->recycleBinEnabled();
+
+ if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
+ GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
+ }
+
+ updateWidget();
+}
+
+void ReportsWidgetBase::emitEntryActivated(const QModelIndex& index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ auto mappedIndex = m_modelProxy->mapToSource(index);
+ const auto row = m_rowToEntry[mappedIndex.row()];
+ const auto group = row.first;
+ const auto entry = row.second;
+
+ if (group && entry) {
+ emit entryActivated(const_cast(entry));
+ }
+}
diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h
new file mode 100644
index 0000000000..c0ac484a36
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetBase.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGETBASE_H
+#define KEEPASSXC_REPORTSWIDGETBASE_H
+
+#include
+
+#include "gui/entry/EntryModel.h"
+#include "gui/reports/ProxyModels.h"
+
+class Database;
+class Entry;
+class Group;
+class PasswordHealth;
+class QSortFilterProxyModel;
+class QStandardItemModel;
+class QMenu;
+class QTableView;
+
+/**
+ * @brief The ReportsWidgetBase class implements functionality common across the various
+ * database report widgets.
+ */
+class ReportsWidgetBase : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ReportsWidgetBase(QWidget* parent, SortProxyModelKind);
+ virtual ~ReportsWidgetBase();
+
+ virtual void loadSettings(QSharedPointer db);
+ virtual void saveSettings();
+
+protected:
+ virtual QTableView* getTableView() const = 0;
+ virtual void updateWidget() = 0;
+
+ QMenu* customMenuRequestedBase();
+
+public slots:
+ QList getSelectedEntries() const;
+ void expireSelectedEntries();
+ void deleteSelectedEntries();
+ void emitEntryActivated(const QModelIndex& index);
+
+signals:
+ void entryActivated(Entry*);
+
+protected:
+ bool m_widgetDataCalculated = false;
+ QScopedPointer m_referencesModel;
+ QScopedPointer m_modelProxy;
+ QSharedPointer m_db;
+ QList> m_rowToEntry;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGETBASE_H
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
index 63267d77fd..d2caeb066f 100644
--- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
@@ -50,7 +50,7 @@ namespace
, entry(e)
, hasUrls(hU)
, hasSettings(hS)
- , exclude(e->excludeFromReports())
+ , exclude(e->excludeFromReports() || g->excludeFromReports())
{
}
};
@@ -92,10 +92,8 @@ BrowserStatistics::BrowserStatistics(QSharedPointer db)
}
ReportsWidgetBrowserStatistics::ReportsWidgetBrowserStatistics(QWidget* parent)
- : QWidget(parent)
+ : ReportsWidgetBase(parent, SortProxyModelKind::Default)
, m_ui(new Ui::ReportsWidgetBrowserStatistics())
- , m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new QSortFilterProxyModel(this))
{
m_ui->setupUi(this);
@@ -141,7 +139,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls,
auto title = entry->title();
if (excluded) {
- title.append(tr(" (Excluded)"));
+ if (group->excludeFromReports()) {
+ title.append(tr(" (Group Excluded)"));
+ } else {
+ title.append(tr(" (Excluded)"));
+ }
}
if (entry->isExpired()) {
title.append(tr(" (Expired)"));
@@ -159,7 +161,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls,
row[3]->setToolTip(allowedUrlsToolTip);
row[4]->setToolTip(deniedUrlsToolTip);
if (excluded) {
- row[0]->setToolTip(tr("This entry is being excluded from reports"));
+ if (group->excludeFromReports()) {
+ row[0]->setToolTip(tr("The group for this entry is being excluded from reports"));
+ } else {
+ row[0]->setToolTip(tr("This entry is being excluded from reports"));
+ }
}
// Store entry pointer per table row (used in double click handler)
@@ -167,25 +173,13 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls,
m_rowToEntry.append({group, entry});
}
-void ReportsWidgetBrowserStatistics::loadSettings(QSharedPointer db)
-{
- m_db = std::move(db);
- m_statisticsCalculated = false;
- m_referencesModel->clear();
- m_rowToEntry.clear();
-
- auto row = QList();
- row << new QStandardItem(tr("Please wait, browser statistics is being calculated…"));
- m_referencesModel->appendRow(row);
-}
-
void ReportsWidgetBrowserStatistics::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
- if (!m_statisticsCalculated) {
+ if (!m_widgetDataCalculated) {
// Perform stats calculation on next event loop to allow widget to appear
- m_statisticsCalculated = true;
+ m_widgetDataCalculated = true;
QTimer::singleShot(0, this, SLOT(calculateBrowserStatistics()));
}
}
@@ -238,134 +232,30 @@ void ReportsWidgetBrowserStatistics::calculateBrowserStatistics()
m_ui->browserStatisticsTableView->resizeColumnsToContents();
}
-void ReportsWidgetBrowserStatistics::emitEntryActivated(const QModelIndex& index)
-{
- if (!index.isValid()) {
- return;
- }
-
- auto mappedIndex = m_modelProxy->mapToSource(index);
- const auto row = m_rowToEntry[mappedIndex.row()];
- const auto group = row.first;
- const auto entry = row.second;
-
- if (group && entry) {
- emit entryActivated(const_cast(entry));
- }
-}
-
void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos)
{
- auto selected = m_ui->browserStatisticsTableView->selectionModel()->selectedRows();
- if (selected.isEmpty()) {
- return;
- }
+ auto menu = customMenuRequestedBase();
- // Create the context menu
- const auto menu = new QMenu(this);
-
- // Create the "edit entry" menu item (only if 1 row is selected)
- if (selected.size() == 1) {
- const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
- menu->addAction(edit);
- connect(edit, &QAction::triggered, edit, [this, selected] {
- auto row = m_modelProxy->mapToSource(selected[0]).row();
- auto entry = m_rowToEntry[row].second;
- emit entryActivated(entry);
- });
+ if (!menu) {
+ return;
}
- // Create the "expire entry" menu item
- const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
- menu->addAction(expEntry);
- connect(expEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::expireSelectedEntries);
-
- // Create the "delete entry" menu item
- const auto deleteEntry =
- new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
- menu->addAction(deleteEntry);
- connect(deleteEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::deleteSelectedEntries);
+ auto selected = getTableView()->selectionModel()->selectedRows();
// Create the "delete plugin data" menu item
const auto deletePluginData =
new QAction(icons()->icon("entry-delete"), tr("Delete plugin data from Entry(s)…", "", selected.size()), this);
- menu->addAction(deletePluginData);
+ menu->insertAction(menu->actions().at(3),
+ deletePluginData); // Index 3 is the one after "Delete Entry" so place "Delete plugin" before it
connect(deletePluginData,
&QAction::triggered,
this,
&ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries);
- // Create the "exclude from reports" menu item
- const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
-
- bool isExcluded = false;
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry && entry->excludeFromReports()) {
- // If at least one entry is excluded switch to inclusion
- isExcluded = true;
- break;
- }
- }
- exclude->setCheckable(true);
- exclude->setChecked(isExcluded);
-
- menu->addAction(exclude);
- connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) {
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- entry->setExcludeFromReports(state);
- }
- }
- calculateBrowserStatistics();
- });
-
// Show the context menu
menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos));
}
-void ReportsWidgetBrowserStatistics::saveSettings()
-{
- // Nothing to do - the tab is passive
-}
-
-QList ReportsWidgetBrowserStatistics::getSelectedEntries()
-{
- QList selectedEntries;
- for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- selectedEntries << entry;
- }
- }
- return selectedEntries;
-}
-
-void ReportsWidgetBrowserStatistics::expireSelectedEntries()
-{
- for (auto entry : getSelectedEntries()) {
- entry->expireNow();
- }
-
- calculateBrowserStatistics();
-}
-
-void ReportsWidgetBrowserStatistics::deleteSelectedEntries()
-{
- const auto& selectedEntries = getSelectedEntries();
- bool permanent = !m_db->metadata()->recycleBinEnabled();
-
- if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
- GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
- }
-
- calculateBrowserStatistics();
-}
-
void ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries()
{
const auto& selectedEntries = getSelectedEntries();
@@ -414,16 +304,12 @@ QMap ReportsWidgetBrowserStatistics::getBrowserConfigFromE
return configList;
}
-QList ReportsWidgetBrowserStatistics::getSelectedEntries() const
+QTableView* ReportsWidgetBrowserStatistics::getTableView() const
{
- QList selectedEntries;
- for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- selectedEntries << entry;
- }
- }
+ return m_ui->browserStatisticsTableView;
+}
- return selectedEntries;
+void ReportsWidgetBrowserStatistics::updateWidget()
+{
+ calculateBrowserStatistics();
}
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h
index 9b1cc7d602..71a08a3a2e 100644
--- a/src/gui/reports/ReportsWidgetBrowserStatistics.h
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h
@@ -19,6 +19,7 @@
#define KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
#include "gui/entry/EntryModel.h"
+#include "gui/reports/ReportsWidgetBase.h"
#include
class Database;
@@ -33,43 +34,28 @@ namespace Ui
class ReportsWidgetBrowserStatistics;
}
-class ReportsWidgetBrowserStatistics : public QWidget
+class ReportsWidgetBrowserStatistics : public ReportsWidgetBase
{
Q_OBJECT
public:
explicit ReportsWidgetBrowserStatistics(QWidget* parent = nullptr);
~ReportsWidgetBrowserStatistics() override;
- void loadSettings(QSharedPointer db);
- void saveSettings();
-
protected:
void showEvent(QShowEvent* event) override;
-
-signals:
- void entryActivated(Entry*);
+ void updateWidget() override;
+ QTableView* getTableView() const override;
public slots:
void calculateBrowserStatistics();
- void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
- QList getSelectedEntries();
- void expireSelectedEntries();
- void deleteSelectedEntries();
void deletePluginDataFromSelectedEntries();
private:
void addStatisticsRow(bool hasUrls, bool hasSettings, Group*, Entry*, bool);
- QList getSelectedEntries() const;
QMap getBrowserConfigFromEntry(Entry* entry) const;
QScopedPointer m_ui;
-
- bool m_statisticsCalculated = false;
- QScopedPointer m_referencesModel;
- QScopedPointer m_modelProxy;
- QSharedPointer m_db;
- QList> m_rowToEntry;
};
#endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
index 1c34c2f36d..086dd776dc 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.cpp
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -24,11 +24,11 @@
#include "core/PasswordHealth.h"
#include "gui/GuiTools.h"
#include "gui/Icons.h"
+#include "gui/reports/ProxyModels.h"
#include "gui/styles/StateColorPalette.h"
#include
#include
-#include
#include
namespace
@@ -47,7 +47,7 @@ namespace
: group(g)
, entry(e)
, health(h)
- , exclude(e->excludeFromReports())
+ , exclude(e->excludeFromReports() || g->excludeFromReports())
{
}
@@ -75,27 +75,6 @@ namespace
QList> m_items;
bool m_anyExcludedEntries = false;
};
-
- class ReportSortProxyModel : public QSortFilterProxyModel
- {
- public:
- ReportSortProxyModel(QObject* parent)
- : QSortFilterProxyModel(parent){};
- ~ReportSortProxyModel() override = default;
-
- protected:
- bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
- {
- // Check if the display data is a number, convert and compare if so
- bool ok = false;
- int leftInt = sourceModel()->data(left).toString().toInt(&ok);
- if (ok) {
- return leftInt < sourceModel()->data(right).toString().toInt();
- }
- // Otherwise use default sorting
- return QSortFilterProxyModel::lessThan(left, right);
- }
- };
} // namespace
Health::Health(QSharedPointer db)
@@ -137,10 +116,8 @@ Health::Health(QSharedPointer db)
}
ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
- : QWidget(parent)
+ : ReportsWidgetBase(parent, SortProxyModelKind::Healthcheck)
, m_ui(new Ui::ReportsWidgetHealthcheck())
- , m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new ReportSortProxyModel(this))
{
m_ui->setupUi(this);
@@ -197,7 +174,11 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt
auto title = entry->title();
if (excluded) {
- title.append(tr(" (Excluded)"));
+ if (group->excludeFromReports()) {
+ title.append(tr(" (Group Excluded)"));
+ } else {
+ title.append(tr(" (Excluded)"));
+ }
}
if (entry->isExpired()) {
title.append(tr(" (Expired)"));
@@ -219,7 +200,11 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt
// Set tooltips
row[0]->setToolTip(tip);
if (excluded) {
- row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ if (group->excludeFromReports()) {
+ row[1]->setToolTip(tr("The group for this entry is being excluded from reports"));
+ } else {
+ row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ }
}
row[4]->setToolTip(health->scoreDetails());
@@ -230,15 +215,7 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt
void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db)
{
- m_db = std::move(db);
- m_healthCalculated = false;
- m_referencesModel->clear();
- m_rowToEntry.clear();
-
- auto row = QList();
- row << new QStandardItem(tr("Please wait, health data is being calculated…"));
- m_referencesModel->appendRow(row);
- // Default sort by first column (health score)
+ ReportsWidgetBase::loadSettings(db);
m_ui->healthcheckTableView->sortByColumn(0, Qt::AscendingOrder);
}
@@ -246,9 +223,9 @@ void ReportsWidgetHealthcheck::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
- if (!m_healthCalculated) {
+ if (!m_widgetDataCalculated) {
// Perform stats calculation on next event loop to allow widget to appear
- m_healthCalculated = true;
+ m_widgetDataCalculated = true;
QTimer::singleShot(0, this, SLOT(calculateHealth()));
}
}
@@ -294,120 +271,28 @@ void ReportsWidgetHealthcheck::calculateHealth()
// Only show the "show excluded" checkbox if there are any excluded entries in the database
m_ui->showExcluded->setVisible(health->anyExcludedEntries());
-}
-void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
-{
- if (!index.isValid()) {
- return;
- }
-
- auto mappedIndex = m_modelProxy->mapToSource(index);
- const auto row = m_rowToEntry[mappedIndex.row()];
- const auto group = row.first;
- const auto entry = row.second;
- if (group && entry) {
- emit entryActivated(const_cast(entry));
- }
+ emit tablePopulated();
}
void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
{
- auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows();
- if (selected.isEmpty()) {
- return;
- }
+ auto menu = customMenuRequestedBase();
- // Create the context menu
- const auto menu = new QMenu(this);
-
- // Create the "edit entry" menu item (only if 1 row is selected)
- if (selected.size() == 1) {
- const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
- menu->addAction(edit);
- connect(edit, &QAction::triggered, edit, [this, selected] {
- auto row = m_modelProxy->mapToSource(selected[0]).row();
- auto entry = m_rowToEntry[row].second;
- emit entryActivated(entry);
- });
+ if (!menu) {
+ return;
}
- // Create the "Expire entry" menu item
- const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
- menu->addAction(expEntry);
- connect(expEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::expireSelectedEntries);
-
- // Create the "delete entry" menu item
- const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
- menu->addAction(delEntry);
- connect(delEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::deleteSelectedEntries);
-
- // Create the "exclude from reports" menu item
- const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
-
- bool isExcluded = false;
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry && entry->excludeFromReports()) {
- // If at least one entry is excluded switch to inclusion
- isExcluded = true;
- break;
- }
- }
- exclude->setCheckable(true);
- exclude->setChecked(isExcluded);
-
- menu->addAction(exclude);
- connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) {
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- entry->setExcludeFromReports(state);
- }
- }
- calculateHealth();
- });
-
// Show the context menu
menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
}
-void ReportsWidgetHealthcheck::saveSettings()
-{
- // nothing to do - the tab is passive
-}
-
-QList ReportsWidgetHealthcheck::getSelectedEntries()
+void ReportsWidgetHealthcheck::updateWidget()
{
- QList selectedEntries;
- for (auto index : m_ui->healthcheckTableView->selectionModel()->selectedRows()) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- selectedEntries << entry;
- }
- }
- return selectedEntries;
-}
-
-void ReportsWidgetHealthcheck::expireSelectedEntries()
-{
- for (auto entry : getSelectedEntries()) {
- entry->expireNow();
- }
-
calculateHealth();
}
-void ReportsWidgetHealthcheck::deleteSelectedEntries()
+QTableView* ReportsWidgetHealthcheck::getTableView() const
{
- QList selectedEntries = getSelectedEntries();
- bool permanent = !m_db->metadata()->recycleBinEnabled();
- if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
- GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
- }
-
- calculateHealth();
+ return m_ui->healthcheckTableView;
}
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
index 9a46b36b1f..7dfe0cdfc0 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.h
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -19,6 +19,7 @@
#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
#include "gui/entry/EntryModel.h"
+#include "gui/reports/ReportsWidgetBase.h"
#include
class Database;
@@ -27,46 +28,38 @@ class Group;
class PasswordHealth;
class QSortFilterProxyModel;
class QStandardItemModel;
+class QTableView;
namespace Ui
{
class ReportsWidgetHealthcheck;
}
-class ReportsWidgetHealthcheck : public QWidget
+class ReportsWidgetHealthcheck : public ReportsWidgetBase
{
Q_OBJECT
public:
explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
~ReportsWidgetHealthcheck() override;
- void loadSettings(QSharedPointer db);
- void saveSettings();
+ void loadSettings(QSharedPointer db) override;
protected:
void showEvent(QShowEvent* event) override;
+ void updateWidget() override;
+ QTableView* getTableView() const override;
signals:
- void entryActivated(Entry*);
+ void tablePopulated();
public slots:
void calculateHealth();
- void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
- QList getSelectedEntries();
- void expireSelectedEntries();
- void deleteSelectedEntries();
private:
void addHealthRow(QSharedPointer, Group*, Entry*, bool excluded);
QScopedPointer m_ui;
-
- bool m_healthCalculated = false;
- QScopedPointer m_referencesModel;
- QScopedPointer m_modelProxy;
- QSharedPointer m_db;
- QList> m_rowToEntry;
};
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
index a559208aaa..b97b9986c2 100644
--- a/src/gui/reports/ReportsWidgetHibp.cpp
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -23,42 +23,17 @@
#include "core/Metadata.h"
#include "gui/GuiTools.h"
#include "gui/Icons.h"
+#include "gui/reports/ProxyModels.h"
#include
#include
-#include
#include
#include
-namespace
-{
- class ReportSortProxyModel : public QSortFilterProxyModel
- {
- public:
- ReportSortProxyModel(QObject* parent)
- : QSortFilterProxyModel(parent){};
- ~ReportSortProxyModel() override = default;
-
- protected:
- bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
- {
- // Sort count column by user data
- if (left.column() == 2) {
- return sourceModel()->data(left, Qt::UserRole).toInt()
- < sourceModel()->data(right, Qt::UserRole).toInt();
- }
- // Otherwise use default sorting
- return QSortFilterProxyModel::lessThan(left, right);
- }
- };
-} // namespace
-
ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
- : QWidget(parent)
+ : ReportsWidgetBase(parent, SortProxyModelKind::Hibp)
, m_ui(new Ui::ReportsWidgetHibp())
- , m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new ReportSortProxyModel(this))
{
m_ui->setupUi(this);
@@ -85,13 +60,6 @@ ReportsWidgetHibp::~ReportsWidgetHibp() = default;
void ReportsWidgetHibp::loadSettings(QSharedPointer db)
{
- // Re-initialize
- m_db = std::move(db);
- m_referencesModel->clear();
- m_pwndPasswords.clear();
- m_error.clear();
- m_rowToEntry.clear();
- m_editedEntry = nullptr;
#ifdef WITH_XC_NETWORKING
m_ui->stackedWidget->setCurrentIndex(0);
m_ui->validationButton->setEnabled(true);
@@ -100,6 +68,9 @@ void ReportsWidgetHibp::loadSettings(QSharedPointer db)
// Compiled without networking, can't do anything
m_ui->stackedWidget->setCurrentIndex(2);
#endif
+
+ ReportsWidgetBase::loadSettings(db);
+ m_referencesModel->clear();
}
/*
@@ -153,13 +124,17 @@ void ReportsWidgetHibp::makeHibpTable()
auto title = entry->title();
// Hide entry if excluded unless explicitly requested
- if (entry->excludeFromReports()) {
+ if (entry->excludeFromReports() || entry->group()->excludeFromReports()) {
anyExcluded = true;
if (!showExcluded) {
continue;
}
- title.append(tr(" (Excluded)"));
+ if (group->excludeFromReports()) {
+ title.append(tr(" (Group Excluded)"));
+ } else {
+ title.append(tr(" (Excluded)"));
+ }
}
auto row = QList();
@@ -169,6 +144,8 @@ void ReportsWidgetHibp::makeHibpTable()
if (entry->excludeFromReports()) {
row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ } else if (entry->group()->excludeFromReports()) {
+ row[1]->setToolTip(tr("The group for this entry is being excluded from reports"));
}
row[2]->setForeground(red);
@@ -176,7 +153,7 @@ void ReportsWidgetHibp::makeHibpTable()
m_referencesModel->appendRow(row);
// Store entry pointer per table row (used in double click handler)
- m_rowToEntry.append(entry);
+ m_rowToEntry.append({group, entry});
}
// If there was an error, append the error message to the table
@@ -311,12 +288,12 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
// Find which database entry was double-clicked
auto mappedIndex = m_modelProxy->mapToSource(index);
const auto entry = m_rowToEntry[mappedIndex.row()];
- if (entry) {
+ if (entry.second) {
// Found it, invoke entry editor
- m_editedEntry = entry;
- m_editedPassword = entry->password();
- m_editedExcluded = entry->excludeFromReports();
- emit entryActivated(const_cast(entry));
+ m_editedEntry = entry.second;
+ m_editedPassword = entry.second->password();
+ m_editedExcluded = entry.second->excludeFromReports();
+ emit entryActivated(const_cast(entry.second));
}
}
@@ -355,101 +332,23 @@ void ReportsWidgetHibp::refreshAfterEdit()
void ReportsWidgetHibp::customMenuRequested(QPoint pos)
{
- auto selected = m_ui->hibpTableView->selectionModel()->selectedRows();
- if (selected.isEmpty()) {
- return;
- }
-
// Create the context menu
- const auto menu = new QMenu(this);
-
- // Create the "edit entry" menu item if 1 row is selected
- if (selected.size() == 1) {
- const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
- menu->addAction(edit);
- connect(edit, &QAction::triggered, edit, [this, selected] {
- auto row = m_modelProxy->mapToSource(selected[0]).row();
- auto entry = m_rowToEntry[row];
- emit entryActivated(entry);
- });
- }
+ const auto menu = customMenuRequestedBase();
- // Create the "Expire entry" menu item
- const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
- menu->addAction(expEntry);
- connect(expEntry, &QAction::triggered, this, &ReportsWidgetHibp::expireSelectedEntries);
-
- // Create the "delete entry" menu item
- const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
- menu->addAction(delEntry);
- connect(delEntry, &QAction::triggered, this, &ReportsWidgetHibp::deleteSelectedEntries);
-
- // Create the "exclude from reports" menu item
- const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
-
- bool isExcluded = false;
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row];
- if (entry && entry->excludeFromReports()) {
- // If at least one entry is excluded switch to inclusion
- isExcluded = true;
- break;
- }
+ if (!menu) {
+ return;
}
- exclude->setCheckable(true);
- exclude->setChecked(isExcluded);
-
- menu->addAction(exclude);
- connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) {
- for (auto index : selected) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row];
- if (entry) {
- entry->setExcludeFromReports(state);
- }
- }
- makeHibpTable();
- });
// Show the context menu
menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
}
-QList ReportsWidgetHibp::getSelectedEntries()
-{
- QList selectedEntries;
- for (auto index : m_ui->hibpTableView->selectionModel()->selectedRows()) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row];
- if (entry) {
- selectedEntries << entry;
- }
- }
- return selectedEntries;
-}
-
-void ReportsWidgetHibp::expireSelectedEntries()
+void ReportsWidgetHibp::updateWidget()
{
- for (auto entry : getSelectedEntries()) {
- entry->expireNow();
- }
-
- makeHibpTable();
-}
-
-void ReportsWidgetHibp::deleteSelectedEntries()
-{
- QList selectedEntries = getSelectedEntries();
- bool permanent = !m_db->metadata()->recycleBinEnabled();
- if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
- GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
- }
-
makeHibpTable();
}
-void ReportsWidgetHibp::saveSettings()
+QTableView* ReportsWidgetHibp::getTableView() const
{
- // nothing to do - the tab is passive
+ return m_ui->hibpTableView;
}
diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h
index 8e0d5e47bc..2d59a090a5 100644
--- a/src/gui/reports/ReportsWidgetHibp.h
+++ b/src/gui/reports/ReportsWidgetHibp.h
@@ -20,6 +20,7 @@
#include "config-keepassx.h"
#include "gui/entry/EntryModel.h"
+#include "gui/reports/ReportsWidgetBase.h"
#include
@@ -32,25 +33,26 @@ class Entry;
class Group;
class QSortFilterProxyModel;
class QStandardItemModel;
+class QTableView;
namespace Ui
{
class ReportsWidgetHibp;
}
-class ReportsWidgetHibp : public QWidget
+class ReportsWidgetHibp : public ReportsWidgetBase
{
Q_OBJECT
public:
explicit ReportsWidgetHibp(QWidget* parent = nullptr);
~ReportsWidgetHibp() override;
- void loadSettings(QSharedPointer db);
- void saveSettings();
+ void loadSettings(QSharedPointer db) override;
void refreshAfterEdit();
-signals:
- void entryActivated(Entry*);
+protected:
+ void updateWidget() override;
+ QTableView* getTableView() const override;
public slots:
void emitEntryActivated(const QModelIndex&);
@@ -58,22 +60,15 @@ public slots:
void fetchFailed(const QString& error);
void makeHibpTable();
void customMenuRequested(QPoint);
- QList getSelectedEntries();
- void expireSelectedEntries();
- void deleteSelectedEntries();
private:
void startValidation();
static QString countToText(int count);
QScopedPointer m_ui;
- QScopedPointer m_referencesModel;
- QScopedPointer m_modelProxy;
- QSharedPointer m_db;
QMap m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
QString m_error; // Error message if download failed, else empty
- QList m_rowToEntry; // List index is table row
QPointer m_editedEntry; // The entry we're currently editing
QString m_editedPassword; // The old password of the entry we're editing
bool m_editedExcluded; // The old "known bad" flag of the entry we're editing
diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp
index 831f4c7211..737c72f687 100644
--- a/src/gui/reports/ReportsWidgetPasskeys.cpp
+++ b/src/gui/reports/ReportsWidgetPasskeys.cpp
@@ -29,6 +29,7 @@
#include "gui/MessageBox.h"
#include "gui/passkeys/PasskeyExporter.h"
#include "gui/passkeys/PasskeyImporter.h"
+#include "gui/reports/ProxyModels.h"
#include "gui/styles/StateColorPalette.h"
#include
@@ -87,10 +88,8 @@ PasskeyList::PasskeyList(const QSharedPointer& db)
}
ReportsWidgetPasskeys::ReportsWidgetPasskeys(QWidget* parent)
- : QWidget(parent)
+ : ReportsWidgetBase(parent, SortProxyModelKind::Default)
, m_ui(new Ui::ReportsWidgetPasskeys())
- , m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new QSortFilterProxyModel(this))
{
m_ui->setupUi(this);
@@ -146,25 +145,13 @@ void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry)
m_rowToEntry.append({group, entry});
}
-void ReportsWidgetPasskeys::loadSettings(QSharedPointer db)
-{
- m_db = std::move(db);
- m_entriesUpdated = false;
- m_referencesModel->clear();
- m_rowToEntry.clear();
-
- auto row = QList();
- row << new QStandardItem(tr("Please wait, list of entries with passkeys is being updated…"));
- m_referencesModel->appendRow(row);
-}
-
void ReportsWidgetPasskeys::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
- if (!m_entriesUpdated) {
+ if (!m_widgetDataCalculated) {
// Perform stats calculation on next event loop to allow widget to appear
- m_entriesUpdated = true;
+ m_widgetDataCalculated = true;
QTimer::singleShot(0, this, SLOT(updateEntries()));
}
}
@@ -200,22 +187,6 @@ void ReportsWidgetPasskeys::updateEntries()
m_ui->passkeysTableView->resizeColumnsToContents();
}
-void ReportsWidgetPasskeys::emitEntryActivated(const QModelIndex& index)
-{
- if (!index.isValid()) {
- return;
- }
-
- auto mappedIndex = m_modelProxy->mapToSource(index);
- const auto row = m_rowToEntry[mappedIndex.row()];
- const auto group = row.first;
- const auto entry = row.second;
-
- if (group && entry) {
- emit entryActivated(entry);
- }
-}
-
void ReportsWidgetPasskeys::customMenuRequested(QPoint pos)
{
auto selected = m_ui->passkeysTableView->selectionModel()->selectedRows();
@@ -246,37 +217,6 @@ void ReportsWidgetPasskeys::customMenuRequested(QPoint pos)
menu->popup(m_ui->passkeysTableView->viewport()->mapToGlobal(pos));
}
-void ReportsWidgetPasskeys::saveSettings()
-{
- // Nothing to do - the tab is passive
-}
-
-void ReportsWidgetPasskeys::deleteSelectedEntries()
-{
- auto selectedEntries = getSelectedEntries();
- bool permanent = !m_db->metadata()->recycleBinEnabled();
-
- if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
- GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
- }
-
- updateEntries();
-}
-
-QList ReportsWidgetPasskeys::getSelectedEntries()
-{
- QList selectedEntries;
- for (auto index : m_ui->passkeysTableView->selectionModel()->selectedRows()) {
- auto row = m_modelProxy->mapToSource(index).row();
- auto entry = m_rowToEntry[row].second;
- if (entry) {
- selectedEntries << entry;
- }
- }
-
- return selectedEntries;
-}
-
void ReportsWidgetPasskeys::selectionChanged()
{
m_ui->exportButton->setEnabled(!m_ui->passkeysTableView->selectionModel()->selectedIndexes().isEmpty());
@@ -305,3 +245,13 @@ void ReportsWidgetPasskeys::exportPasskey()
PasskeyExporter passkeyExporter(this);
passkeyExporter.showExportDialog(getSelectedEntries());
}
+
+QTableView* ReportsWidgetPasskeys::getTableView() const
+{
+ return m_ui->passkeysTableView;
+}
+
+void ReportsWidgetPasskeys::updateWidget()
+{
+ updateEntries();
+}
diff --git a/src/gui/reports/ReportsWidgetPasskeys.h b/src/gui/reports/ReportsWidgetPasskeys.h
index 3d0593350c..30945bba60 100644
--- a/src/gui/reports/ReportsWidgetPasskeys.h
+++ b/src/gui/reports/ReportsWidgetPasskeys.h
@@ -19,6 +19,7 @@
#define KEEPASSXC_REPORTSWIDGETPASSKEYS_H
#include "gui/entry/EntryModel.h"
+#include "gui/reports/ReportsWidgetBase.h"
#include
class Database;
@@ -33,27 +34,21 @@ namespace Ui
class ReportsWidgetPasskeys;
}
-class ReportsWidgetPasskeys : public QWidget
+class ReportsWidgetPasskeys : public ReportsWidgetBase
{
Q_OBJECT
public:
explicit ReportsWidgetPasskeys(QWidget* parent = nullptr);
~ReportsWidgetPasskeys() override;
- void loadSettings(QSharedPointer db);
- void saveSettings();
-
protected:
void showEvent(QShowEvent* event) override;
-
-signals:
- void entryActivated(Entry*);
+ void updateWidget() override;
+ QTableView* getTableView() const override;
public slots:
void updateEntries();
- void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
- void deleteSelectedEntries();
private slots:
void selectionChanged();
@@ -62,15 +57,8 @@ private slots:
private:
void addPasskeyRow(Group*, Entry*);
- QList getSelectedEntries();
QScopedPointer m_ui;
-
- bool m_entriesUpdated = false;
- QScopedPointer m_referencesModel;
- QScopedPointer m_modelProxy;
- QSharedPointer m_db;
- QList> m_rowToEntry;
};
#endif // KEEPASSXC_REPORTSWIDGETPASSKEYS_H
diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp
index 0aced6ae5d..ff0b305ad0 100644
--- a/src/gui/reports/ReportsWidgetStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetStatistics.cpp
@@ -121,6 +121,11 @@ void ReportsWidgetStatistics::calculateStats()
stats->excludedEntries > 0,
tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't "
"necessarily a problem but you should keep an eye on them."));
+ addStatsRow(tr("Groups excluded from reports"),
+ QString::number(stats->excludedGroups),
+ stats->excludedGroups > 0,
+ tr("Excluding entire groups from reports isn't necessarily a problem but please exercise caution "
+ "when excluding entire groups."));
addStatsRow(tr("Average password length"),
tr("%1 character(s)", "", stats->averagePwdLength()).arg(stats->averagePwdLength()),
stats->isAvgPwdTooShort(),
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index adf3593247..fe051fab7d 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -62,6 +62,8 @@
#include "gui/group/GroupModel.h"
#include "gui/group/GroupView.h"
#include "gui/remote/RemoteHandler.h"
+#include "gui/reports/ReportsDialog.h"
+#include "gui/reports/ReportsWidgetHealthcheck.h"
#include "gui/tag/TagsEdit.h"
#include "gui/wizard/NewDatabaseWizard.h"
#include "keys/FileKey.h"
@@ -2483,6 +2485,349 @@ void TestGui::testMenuActionStates()
QVERIFY(isActionEnabled("actionPasswordGenerator"));
}
+void TestGui::testDatabaseReports()
+{
+ addGroup("Finance");
+ addGroup("Entertainment");
+
+ addEntry("Finance", "Chase", "user1", "password");
+ addEntry("Finance", "Amex", "user1", "password123");
+ addEntry("Finance", "Capital One", "user1", "password456");
+
+ addEntry("Entertainment", "Netflix", "user1", "password");
+ addEntry("Entertainment", "Hulu", "user1", "password321");
+ addEntry("Entertainment", "Apple TV", "user1", "password123");
+
+ Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment");
+ QCOMPARE(entertainmentGroup->entries().size(), 3);
+
+ Group* financeGroup = m_dbWidget->currentGroup()->findChildByName("Finance");
+ QCOMPARE(financeGroup->entries().size(), 3);
+
+ auto* actionReports = m_mainWindow->findChild("actionReports");
+ QVERIFY(actionReports->isEnabled());
+
+ auto* toolBar = m_mainWindow->findChild("toolBar");
+ QVERIFY(toolBar);
+
+ QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports);
+ QVERIFY(actionReportsWidget);
+ QVERIFY(actionReportsWidget->isVisible());
+ QVERIFY(actionReportsWidget->isEnabled());
+
+ QTest::mouseClick(actionReportsWidget, Qt::LeftButton);
+
+ auto* reportsDialog = m_dbWidget->findChild("reportsDialog");
+ QVERIFY(reportsDialog);
+
+ CategoryListWidget* categoryList = reportsDialog->findChild("categoryList");
+ categoryList->setCurrentCategory(1);
+
+ QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget");
+ QVERIFY(stackedWidget);
+ stackedWidget->setCurrentIndex(1);
+
+ ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild();
+ QVERIFY(healthCheckWidget);
+
+ QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
+ QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView");
+ QVERIFY(healthTable);
+
+ QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated);
+
+ QAbstractItemModel* healthModel = healthTable->model();
+ QVERIFY(healthModel);
+
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 1);
+ QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case
+
+ auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox");
+ QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+}
+
+void TestGui::testExcludedGroupEntryInReports()
+{
+ addGroup("Finance");
+ addGroup("Entertainment");
+
+ // Use bad passwords to make sure they all show up in health report
+ addEntry("Finance", "Chase", "user1", "password");
+ addEntry("Finance", "Amex", "user1", "password123");
+ addEntry("Finance", "Capital One", "user1", "password456");
+
+ addEntry("Entertainment", "Netflix", "user1", "password");
+ addEntry("Entertainment", "Hulu", "user1", "password321");
+ addEntry("Entertainment", "Apple TV", "user1", "password123");
+
+ Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment");
+ m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup);
+
+ auto* toolBar = m_mainWindow->findChild("toolBar");
+ QVERIFY(toolBar);
+
+ auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit");
+ QVERIFY(editGroupAction->isEnabled());
+ triggerAction("actionGroupEdit");
+
+ auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget");
+ QVERIFY(editGroupWidget);
+
+ // Bring up group edit page
+ QTest::mouseClick(editGroupWidget, Qt::LeftButton);
+
+ QLineEdit* nameEdit = editGroupWidget->findChild("editName");
+ QCOMPARE(nameEdit->text(), QString("Entertainment"));
+
+ // Find database report exclusion checkbox and check it
+ QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox");
+ QVERIFY(excludeGroupFromReportsCheckbox);
+
+ excludeGroupFromReportsCheckbox->setChecked(true);
+
+ auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox");
+ QVERIFY(editGroupWidgetButtonBox);
+
+ // Apply and go back to main view
+ QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton);
+ QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+
+ QVERIFY(entertainmentGroup->excludeFromReports());
+
+ // Verify they don't show up in the report
+ auto* actionReports = m_mainWindow->findChild("actionReports");
+ QVERIFY(actionReports->isEnabled());
+
+ QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports);
+ QVERIFY(actionReportsWidget);
+ QVERIFY(actionReportsWidget->isVisible());
+ QVERIFY(actionReportsWidget->isEnabled());
+
+ QTest::mouseClick(actionReportsWidget, Qt::LeftButton);
+
+ auto* reportsDialog = m_dbWidget->findChild("reportsDialog");
+ QVERIFY(reportsDialog);
+
+ CategoryListWidget* categoryList = reportsDialog->findChild("categoryList");
+ categoryList->setCurrentCategory(1);
+
+ QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget");
+ QVERIFY(stackedWidget);
+ stackedWidget->setCurrentIndex(1);
+
+ ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild();
+ QVERIFY(healthCheckWidget);
+
+ QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
+ QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView");
+ QVERIFY(healthTable);
+
+ QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated);
+
+ QAbstractItemModel* healthModel = healthTable->model();
+ QVERIFY(healthModel);
+
+ // There should be 3 showing
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 1);
+ QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case
+
+ QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded");
+ QVERIFY(showExcludedCheckBox);
+ QCOMPARE(showExcludedCheckBox->isChecked(), false);
+
+ showExcludedCheckBox->click();
+ QVERIFY(showExcludedCheckBox->isChecked());
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 2);
+
+ healthModel = healthTable->model();
+ QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case
+
+ for (int i = 0; i < healthModel->rowCount(); ++i) {
+ QModelIndex index = healthModel->index(i, 1);
+ QVariant data = healthModel->data(index);
+
+ if (data.toString().contains("Netflix")) {
+ auto rect = healthTable->visualRect(index);
+ auto centerPoint = rect.center();
+ QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint);
+ healthCheckWidget->customMenuRequested(centerPoint);
+
+ QMenu* menu = healthCheckWidget->findChild("customMenu");
+ QVERIFY(menu);
+ QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction");
+ QVERIFY(excludeEntryAction);
+ MessageBox::setNextAnswer(MessageBox::No);
+
+ excludeEntryAction->trigger();
+ QApplication::processEvents();
+ break;
+ }
+ }
+
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 3);
+
+ for (int i = 0; i < healthModel->rowCount(); ++i) {
+ QModelIndex index = healthModel->index(i, 1);
+ QVariant data = healthModel->data(index);
+
+ if (data.toString().contains("Netflix")) {
+ QVERIFY(!data.toString().contains("(Group Excluded)"));
+ break;
+ }
+ }
+
+ showExcludedCheckBox->click();
+ QVERIFY(!showExcludedCheckBox->isChecked());
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 4);
+
+ QCOMPARE(healthModel->rowCount(), 6); // 2 existing passwords from start, 3 from Finance, 1 from Entertainment
+
+ auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox");
+ QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+}
+
+void TestGui::testExcludedGroupReincludeAllEntries()
+{
+ addGroup("Finance");
+ addGroup("Entertainment");
+
+ // Use bad passwords to make sure they all show up in health report
+ addEntry("Finance", "Chase", "user1", "password");
+ addEntry("Finance", "Amex", "user1", "password123");
+ addEntry("Finance", "Capital One", "user1", "password456");
+
+ addEntry("Entertainment", "Netflix", "user1", "password");
+ addEntry("Entertainment", "Hulu", "user1", "password321");
+ addEntry("Entertainment", "Apple TV", "user1", "password123");
+
+ Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment");
+ m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup);
+
+ auto* toolBar = m_mainWindow->findChild("toolBar");
+ QVERIFY(toolBar);
+
+ auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit");
+ QVERIFY(editGroupAction->isEnabled());
+ triggerAction("actionGroupEdit");
+
+ auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget");
+ QVERIFY(editGroupWidget);
+
+ // Bring up group edit page
+ QTest::mouseClick(editGroupWidget, Qt::LeftButton);
+
+ QLineEdit* nameEdit = editGroupWidget->findChild("editName");
+ QCOMPARE(nameEdit->text(), QString("Entertainment"));
+
+ // Find database report exclusion checkbox and check it
+ QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox");
+ QVERIFY(excludeGroupFromReportsCheckbox);
+
+ excludeGroupFromReportsCheckbox->setChecked(true);
+
+ auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox");
+ QVERIFY(editGroupWidgetButtonBox);
+
+ // Apply and go back to main view
+ QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton);
+ QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+
+ QVERIFY(entertainmentGroup->excludeFromReports());
+
+ // Verify they don't show up in the report
+ auto* actionReports = m_mainWindow->findChild("actionReports");
+ QVERIFY(actionReports->isEnabled());
+
+ QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports);
+ QVERIFY(actionReportsWidget);
+ QVERIFY(actionReportsWidget->isVisible());
+ QVERIFY(actionReportsWidget->isEnabled());
+
+ QTest::mouseClick(actionReportsWidget, Qt::LeftButton);
+
+ auto* reportsDialog = m_dbWidget->findChild("reportsDialog");
+ QVERIFY(reportsDialog);
+
+ CategoryListWidget* categoryList = reportsDialog->findChild("categoryList");
+ categoryList->setCurrentCategory(1);
+
+ QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget");
+ QVERIFY(stackedWidget);
+ stackedWidget->setCurrentIndex(1);
+
+ ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild();
+ QVERIFY(healthCheckWidget);
+
+ QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
+ QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView");
+ QVERIFY(healthTable);
+
+ QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated);
+
+ QAbstractItemModel* healthModel = healthTable->model();
+ QVERIFY(healthModel);
+
+ // There should be 3 showing
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 1);
+ QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case
+
+ QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded");
+ QVERIFY(showExcludedCheckBox);
+ QCOMPARE(showExcludedCheckBox->isChecked(), false);
+
+ showExcludedCheckBox->click();
+ QVERIFY(showExcludedCheckBox->isChecked());
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 2);
+
+ healthModel = healthTable->model();
+ QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case
+
+ for (int i = 0; i < healthModel->rowCount(); ++i) {
+ QModelIndex index = healthModel->index(i, 1);
+ QVariant data = healthModel->data(index);
+
+ if (data.toString().contains("Netflix")) {
+ auto rect = healthTable->visualRect(index);
+ auto centerPoint = rect.center();
+ QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint);
+ healthCheckWidget->customMenuRequested(centerPoint);
+
+ QMenu* menu = healthCheckWidget->findChild("customMenu");
+ QVERIFY(menu);
+ QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction");
+ QVERIFY(excludeEntryAction);
+ MessageBox::setNextAnswer(MessageBox::Yes);
+
+ excludeEntryAction->trigger();
+ QApplication::processEvents();
+ break;
+ }
+ }
+
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 3);
+
+ for (int i = 0; i < healthModel->rowCount(); ++i) {
+ QModelIndex index = healthModel->index(i, 1);
+ QVariant data = healthModel->data(index);
+
+ QVERIFY(!data.toString().contains("(Group Excluded)"));
+ }
+
+ showExcludedCheckBox->click();
+ QVERIFY(!showExcludedCheckBox->isChecked());
+ QTRY_COMPARE(healthCheckWidgetSpy.count(), 4);
+
+ QCOMPARE(healthModel->rowCount(), 8); // 2 existing passwords from start, 3 from Finance, 3 from Entertainment
+
+ auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox");
+ QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+}
+
void TestGui::addCannedEntries()
{
// Find buttons
@@ -2513,6 +2858,51 @@ void TestGui::addCannedEntries()
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
}
+void TestGui::addGroup(const QString& name)
+{
+ // Find buttons for group creation
+ auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget");
+ auto* nameEdit = editGroupWidget->findChild("editName");
+ auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox");
+
+ // Add group with specified name
+ Group* rootGroup = m_db->rootGroup();
+ m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Add group on root level
+ m_dbWidget->createGroup();
+ QTest::keyClicks(nameEdit, name);
+ QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Reset to root level
+}
+
+void TestGui::addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password)
+{
+ // Find buttons
+ auto* toolBar = m_mainWindow->findChild("toolBar");
+ QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew"));
+ auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget");
+ auto* titleEdit = editEntryWidget->findChild("titleEdit");
+ auto* usernameComboBox = editEntryWidget->findChild("usernameComboBox");
+ auto* passwordEdit =
+ editEntryWidget->findChild("passwordEdit")->findChild("passwordEdit");
+ auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox");
+
+ // Add entry to specified group
+ QVERIFY(m_dbWidget->currentGroup());
+ Group* group = m_dbWidget->currentGroup()->findChildByName(groupName);
+ m_dbWidget->groupView()->setCurrentGroup(group);
+
+ QTest::mouseClick(entryNewWidget, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
+
+ QTest::keyClicks(titleEdit, title);
+ QTest::keyClicks(usernameComboBox, username);
+ QTest::keyClicks(passwordEdit, password);
+ QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+ m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup());
+}
+
void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName)
{
auto key = QSharedPointer::create();
diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h
index 514f7ce95b..ef6c3d61bd 100644
--- a/tests/gui/TestGui.h
+++ b/tests/gui/TestGui.h
@@ -71,9 +71,14 @@ private slots:
void testTrayRestoreHide();
void testShortcutConfig();
void testMenuActionStates();
+ void testDatabaseReports();
+ void testExcludedGroupEntryInReports();
+ void testExcludedGroupReincludeAllEntries();
private:
void addCannedEntries();
+ void addGroup(const QString& name);
+ void addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password);
void checkDatabase(const QString& filePath, const QString& expectedDbName);
void checkDatabase(const QString& filePath = {});
void triggerAction(const QString& name);