mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
Merge 249352a227 into d9be1b0682
This commit is contained in:
commit
b51c75e7c3
22 changed files with 1091 additions and 658 deletions
|
|
@ -3806,6 +3806,10 @@ Supported extensions are: %1.</source>
|
|||
<source>Search toggle for this and sub groups</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude from database reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditWidgetIcons</name>
|
||||
|
|
@ -9499,6 +9503,47 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetBase</name>
|
||||
<message>
|
||||
<source>Please wait, report is being calculated…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit Entry…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Expire Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Delete Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude Entry(s) from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude Group(s) from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The Group for "%1" is excluded. Would you like to include all Entries from there as well?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Include Group?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetBrowserStatistics</name>
|
||||
<message>
|
||||
|
|
@ -9533,10 +9578,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>This entry is being excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please wait, browser statistics is being calculated…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No entries with a URL, or none has browser extension settings saved.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
@ -9553,28 +9594,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>URLs</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit Entry…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Delete Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Expire Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Only show entries that have a URL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
@ -9598,6 +9617,14 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source> (Group Excluded)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The group for this entry is being excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetHealthcheck</name>
|
||||
|
|
@ -9633,10 +9660,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>This entry is being excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please wait, health data is being calculated…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Congratulations, everything is healthy!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
@ -9657,32 +9680,18 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>Reason</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit Entry…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Delete Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Expire Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show entries that have been excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source> (Group Excluded)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The group for this entry is being excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetHibp</name>
|
||||
|
|
@ -9767,27 +9776,13 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit Entry…</source>
|
||||
<source> (Group Excluded)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Delete Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Exclude from reports</source>
|
||||
<source>The group for this entry is being excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Expire Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetPasskeys</name>
|
||||
|
|
@ -9850,10 +9845,6 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>The passkey file will be vulnerable to theft and unauthorized use, if left unsecured. Are you sure you want to continue?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please wait, list of entries with passkeys is being updated…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No entries with passkeys.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
@ -9988,6 +9979,14 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Groups excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Excluding entire groups from reports isn't necessarily a problem but please exercise caution when excluding entire groups.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SSHAgent</name>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ void DatabaseStats::gatherStats(const QList<Group*>& 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<Group*>& groups)
|
|||
++weakPasswords;
|
||||
}
|
||||
|
||||
if (entry->excludeFromReports()) {
|
||||
if (entry->excludeFromReports() || group->excludeFromReports()) {
|
||||
++excludedEntries;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ public:
|
|||
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 @@ public:
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<height>523</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,1" rowminimumheight="0,0,0,0,0,0,0,0,0,1">
|
||||
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,1,0" rowminimumheight="0,0,0,0,0,0,0,0,0,1,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="3" column="0">
|
||||
<item row="3" column="0" alignment="Qt::AlignRight">
|
||||
<widget class="QCheckBox" name="expireCheck">
|
||||
<property name="accessibleName">
|
||||
<string>Toggle expiration</string>
|
||||
|
|
@ -66,10 +66,10 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="editName">
|
||||
<property name="accessibleName">
|
||||
<string>Name field</string>
|
||||
<item row="7" column="1">
|
||||
<widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
|
||||
<property name="text">
|
||||
<string>Set default Auto-Type sequence</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
@ -86,30 +86,10 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QRadioButton" name="autoTypeSequenceInherit">
|
||||
<property name="text">
|
||||
<string>Use default Auto-Type sequence of parent group</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="autotypeLabel">
|
||||
<property name="text">
|
||||
<string>Auto-Type:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="searchLabel">
|
||||
<property name="text">
|
||||
<string>Search:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="editName">
|
||||
<property name="accessibleName">
|
||||
<string>Name field</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
@ -120,6 +100,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="searchComboBox">
|
||||
<property name="accessibleName">
|
||||
<string>Search toggle for this and sub groups</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
|
|
@ -199,31 +186,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelName">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
|
||||
<property name="text">
|
||||
<string>Set default Auto-Type sequence</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="searchComboBox">
|
||||
<property name="accessibleName">
|
||||
<string>Search toggle for this and sub groups</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<item row="10" column="0">
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
|
@ -236,6 +199,50 @@
|
|||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="searchLabel">
|
||||
<property name="text">
|
||||
<string>Search:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="autotypeLabel">
|
||||
<property name="text">
|
||||
<string>Auto-Type:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QRadioButton" name="autoTypeSequenceInherit">
|
||||
<property name="text">
|
||||
<string>Use default Auto-Type sequence of parent group</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelName">
|
||||
<property name="text">
|
||||
<string>Name:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QCheckBox" name="excludeReportsCheckBox">
|
||||
<property name="text">
|
||||
<string>Exclude from database reports</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
|
|
|||
75
src/gui/reports/ProxyModels.h
Normal file
75
src/gui/reports/ProxyModels.h
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PROXYMODELS_H
|
||||
#define KEEPASSXC_PROXYMODELS_H
|
||||
|
||||
#include <QModelIndex>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
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
|
||||
259
src/gui/reports/ReportsWidgetBase.cpp
Normal file
259
src/gui/reports/ReportsWidgetBase.cpp
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ReportsWidgetBase.h"
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "gui/GuiTools.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/MessageBox.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QIcon>
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTableView>
|
||||
|
||||
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<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
m_widgetDataCalculated = false;
|
||||
m_referencesModel->clear();
|
||||
m_rowToEntry.clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
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<Group*> 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<Entry*> ReportsWidgetBase::getSelectedEntries() const
|
||||
{
|
||||
QList<Entry*> 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*>(entry));
|
||||
}
|
||||
}
|
||||
72
src/gui/reports/ReportsWidgetBase.h
Normal file
72
src/gui/reports/ReportsWidgetBase.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_REPORTSWIDGETBASE_H
|
||||
#define KEEPASSXC_REPORTSWIDGETBASE_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#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<Database> db);
|
||||
virtual void saveSettings();
|
||||
|
||||
protected:
|
||||
virtual QTableView* getTableView() const = 0;
|
||||
virtual void updateWidget() = 0;
|
||||
|
||||
QMenu* customMenuRequestedBase();
|
||||
|
||||
public slots:
|
||||
QList<Entry*> getSelectedEntries() const;
|
||||
void expireSelectedEntries();
|
||||
void deleteSelectedEntries();
|
||||
void emitEntryActivated(const QModelIndex& index);
|
||||
|
||||
signals:
|
||||
void entryActivated(Entry*);
|
||||
|
||||
protected:
|
||||
bool m_widgetDataCalculated = false;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGETBASE_H
|
||||
|
|
@ -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<Database> 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<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
m_statisticsCalculated = false;
|
||||
m_referencesModel->clear();
|
||||
m_rowToEntry.clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
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*>(entry));
|
||||
}
|
||||
}
|
||||
|
||||
void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos)
|
||||
{
|
||||
auto selected = m_ui->browserStatisticsTableView->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty()) {
|
||||
auto menu = customMenuRequestedBase();
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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<Entry*> ReportsWidgetBrowserStatistics::getSelectedEntries()
|
||||
{
|
||||
QList<Entry*> 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<QString, QStringList> ReportsWidgetBrowserStatistics::getBrowserConfigFromE
|
|||
return configList;
|
||||
}
|
||||
|
||||
QList<Entry*> ReportsWidgetBrowserStatistics::getSelectedEntries() const
|
||||
QTableView* ReportsWidgetBrowserStatistics::getTableView() const
|
||||
{
|
||||
QList<Entry*> 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;
|
||||
return m_ui->browserStatisticsTableView;
|
||||
}
|
||||
|
||||
void ReportsWidgetBrowserStatistics::updateWidget()
|
||||
{
|
||||
calculateBrowserStatistics();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#define KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
|
||||
|
||||
#include "gui/entry/EntryModel.h"
|
||||
#include "gui/reports/ReportsWidgetBase.h"
|
||||
#include <QWidget>
|
||||
|
||||
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<Database> 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<Entry*> getSelectedEntries();
|
||||
void expireSelectedEntries();
|
||||
void deleteSelectedEntries();
|
||||
void deletePluginDataFromSelectedEntries();
|
||||
|
||||
private:
|
||||
void addStatisticsRow(bool hasUrls, bool hasSettings, Group*, Entry*, bool);
|
||||
QList<Entry*> getSelectedEntries() const;
|
||||
QMap<QString, QStringList> getBrowserConfigFromEntry(Entry* entry) const;
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetBrowserStatistics> m_ui;
|
||||
|
||||
bool m_statisticsCalculated = false;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
|
||||
|
|
|
|||
|
|
@ -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 <QMenu>
|
||||
#include <QShortcut>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
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<QSharedPointer<Item>> 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<Database> db)
|
||||
|
|
@ -137,10 +116,8 @@ Health::Health(QSharedPointer<Database> 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<PasswordHealth> 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<PasswordHealth> 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<PasswordHealth> healt
|
|||
|
||||
void ReportsWidgetHealthcheck::loadSettings(QSharedPointer<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
m_healthCalculated = false;
|
||||
m_referencesModel->clear();
|
||||
m_rowToEntry.clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
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*>(entry));
|
||||
}
|
||||
emit tablePopulated();
|
||||
}
|
||||
|
||||
void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
|
||||
{
|
||||
auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty()) {
|
||||
auto menu = customMenuRequestedBase();
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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()
|
||||
void ReportsWidgetHealthcheck::updateWidget()
|
||||
{
|
||||
// nothing to do - the tab is passive
|
||||
}
|
||||
|
||||
QList<Entry*> ReportsWidgetHealthcheck::getSelectedEntries()
|
||||
{
|
||||
QList<Entry*> 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<Entry*> selectedEntries = getSelectedEntries();
|
||||
bool permanent = !m_db->metadata()->recycleBinEnabled();
|
||||
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||
}
|
||||
|
||||
calculateHealth();
|
||||
return m_ui->healthcheckTableView;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
|
||||
|
||||
#include "gui/entry/EntryModel.h"
|
||||
#include "gui/reports/ReportsWidgetBase.h"
|
||||
#include <QWidget>
|
||||
|
||||
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<Database> db);
|
||||
void saveSettings();
|
||||
void loadSettings(QSharedPointer<Database> 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<Entry*> getSelectedEntries();
|
||||
void expireSelectedEntries();
|
||||
void deleteSelectedEntries();
|
||||
|
||||
private:
|
||||
void addHealthRow(QSharedPointer<PasswordHealth>, Group*, Entry*, bool excluded);
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
||||
|
||||
bool m_healthCalculated = false;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
|
||||
|
|
|
|||
|
|
@ -23,42 +23,17 @@
|
|||
#include "core/Metadata.h"
|
||||
#include "gui/GuiTools.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/reports/ProxyModels.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QShortcut>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<Database> 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<Database> 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<QStandardItem*>();
|
||||
|
|
@ -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*>(entry));
|
||||
m_editedEntry = entry.second;
|
||||
m_editedPassword = entry.second->password();
|
||||
m_editedExcluded = entry.second->excludeFromReports();
|
||||
emit entryActivated(const_cast<Entry*>(entry.second));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,101 +332,23 @@ void ReportsWidgetHibp::refreshAfterEdit()
|
|||
|
||||
void ReportsWidgetHibp::customMenuRequested(QPoint pos)
|
||||
{
|
||||
auto selected = m_ui->hibpTableView->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty()) {
|
||||
// Create the context menu
|
||||
const auto menu = customMenuRequestedBase();
|
||||
|
||||
if (!menu) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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<Entry*> ReportsWidgetHibp::getSelectedEntries()
|
||||
void ReportsWidgetHibp::updateWidget()
|
||||
{
|
||||
QList<Entry*> 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()
|
||||
{
|
||||
for (auto entry : getSelectedEntries()) {
|
||||
entry->expireNow();
|
||||
}
|
||||
|
||||
makeHibpTable();
|
||||
}
|
||||
|
||||
void ReportsWidgetHibp::deleteSelectedEntries()
|
||||
QTableView* ReportsWidgetHibp::getTableView() const
|
||||
{
|
||||
QList<Entry*> selectedEntries = getSelectedEntries();
|
||||
bool permanent = !m_db->metadata()->recycleBinEnabled();
|
||||
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||
}
|
||||
|
||||
makeHibpTable();
|
||||
}
|
||||
|
||||
void ReportsWidgetHibp::saveSettings()
|
||||
{
|
||||
// nothing to do - the tab is passive
|
||||
return m_ui->hibpTableView;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include "config-keepassx.h"
|
||||
#include "gui/entry/EntryModel.h"
|
||||
#include "gui/reports/ReportsWidgetBase.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -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<Database> db);
|
||||
void saveSettings();
|
||||
void loadSettings(QSharedPointer<Database> 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<Entry*> getSelectedEntries();
|
||||
void expireSelectedEntries();
|
||||
void deleteSelectedEntries();
|
||||
|
||||
private:
|
||||
void startValidation();
|
||||
static QString countToText(int count);
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetHibp> m_ui;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
|
||||
QMap<QString, int> m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
|
||||
QString m_error; // Error message if download failed, else empty
|
||||
QList<Entry*> m_rowToEntry; // List index is table row
|
||||
QPointer<Entry> 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
|
||||
|
|
|
|||
|
|
@ -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 <QMenu>
|
||||
|
|
@ -87,10 +88,8 @@ PasskeyList::PasskeyList(const QSharedPointer<Database>& 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<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
m_entriesUpdated = false;
|
||||
m_referencesModel->clear();
|
||||
m_rowToEntry.clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
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<Entry*> ReportsWidgetPasskeys::getSelectedEntries()
|
||||
{
|
||||
QList<Entry*> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#define KEEPASSXC_REPORTSWIDGETPASSKEYS_H
|
||||
|
||||
#include "gui/entry/EntryModel.h"
|
||||
#include "gui/reports/ReportsWidgetBase.h"
|
||||
#include <QWidget>
|
||||
|
||||
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<Database> 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<Entry*> getSelectedEntries();
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetPasskeys> m_ui;
|
||||
|
||||
bool m_entriesUpdated = false;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGETPASSKEYS_H
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -2472,6 +2474,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<QAction*>("actionReports");
|
||||
QVERIFY(actionReports->isEnabled());
|
||||
|
||||
auto* toolBar = m_mainWindow->findChild<QToolBar*>("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*>("reportsDialog");
|
||||
QVERIFY(reportsDialog);
|
||||
|
||||
CategoryListWidget* categoryList = reportsDialog->findChild<CategoryListWidget*>("categoryList");
|
||||
categoryList->setCurrentCategory(1);
|
||||
|
||||
QStackedWidget* stackedWidget = reportsDialog->findChild<QStackedWidget*>("stackedWidget");
|
||||
QVERIFY(stackedWidget);
|
||||
stackedWidget->setCurrentIndex(1);
|
||||
|
||||
ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild<ReportsWidgetHealthcheck*>();
|
||||
QVERIFY(healthCheckWidget);
|
||||
|
||||
QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
|
||||
QTableView* healthTable = healthCheckWidget->findChild<QTableView*>("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<QDialogButtonBox*>("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<QToolBar*>("toolBar");
|
||||
QVERIFY(toolBar);
|
||||
|
||||
auto* editGroupAction = m_mainWindow->findChild<QAction*>("actionGroupEdit");
|
||||
QVERIFY(editGroupAction->isEnabled());
|
||||
triggerAction("actionGroupEdit");
|
||||
|
||||
auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
|
||||
QVERIFY(editGroupWidget);
|
||||
|
||||
// Bring up group edit page
|
||||
QTest::mouseClick(editGroupWidget, Qt::LeftButton);
|
||||
|
||||
QLineEdit* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
|
||||
QCOMPARE(nameEdit->text(), QString("Entertainment"));
|
||||
|
||||
// Find database report exclusion checkbox and check it
|
||||
QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
|
||||
QVERIFY(excludeGroupFromReportsCheckbox);
|
||||
|
||||
excludeGroupFromReportsCheckbox->setChecked(true);
|
||||
|
||||
auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("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<QAction*>("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*>("reportsDialog");
|
||||
QVERIFY(reportsDialog);
|
||||
|
||||
CategoryListWidget* categoryList = reportsDialog->findChild<CategoryListWidget*>("categoryList");
|
||||
categoryList->setCurrentCategory(1);
|
||||
|
||||
QStackedWidget* stackedWidget = reportsDialog->findChild<QStackedWidget*>("stackedWidget");
|
||||
QVERIFY(stackedWidget);
|
||||
stackedWidget->setCurrentIndex(1);
|
||||
|
||||
ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild<ReportsWidgetHealthcheck*>();
|
||||
QVERIFY(healthCheckWidget);
|
||||
|
||||
QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
|
||||
QTableView* healthTable = healthCheckWidget->findChild<QTableView*>("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<QCheckBox*>("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<QMenu*>("customMenu");
|
||||
QVERIFY(menu);
|
||||
QAction* excludeEntryAction = healthCheckWidget->findChild<QAction*>("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<QDialogButtonBox*>("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<QToolBar*>("toolBar");
|
||||
QVERIFY(toolBar);
|
||||
|
||||
auto* editGroupAction = m_mainWindow->findChild<QAction*>("actionGroupEdit");
|
||||
QVERIFY(editGroupAction->isEnabled());
|
||||
triggerAction("actionGroupEdit");
|
||||
|
||||
auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
|
||||
QVERIFY(editGroupWidget);
|
||||
|
||||
// Bring up group edit page
|
||||
QTest::mouseClick(editGroupWidget, Qt::LeftButton);
|
||||
|
||||
QLineEdit* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
|
||||
QCOMPARE(nameEdit->text(), QString("Entertainment"));
|
||||
|
||||
// Find database report exclusion checkbox and check it
|
||||
QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
|
||||
QVERIFY(excludeGroupFromReportsCheckbox);
|
||||
|
||||
excludeGroupFromReportsCheckbox->setChecked(true);
|
||||
|
||||
auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("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<QAction*>("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*>("reportsDialog");
|
||||
QVERIFY(reportsDialog);
|
||||
|
||||
CategoryListWidget* categoryList = reportsDialog->findChild<CategoryListWidget*>("categoryList");
|
||||
categoryList->setCurrentCategory(1);
|
||||
|
||||
QStackedWidget* stackedWidget = reportsDialog->findChild<QStackedWidget*>("stackedWidget");
|
||||
QVERIFY(stackedWidget);
|
||||
stackedWidget->setCurrentIndex(1);
|
||||
|
||||
ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild<ReportsWidgetHealthcheck*>();
|
||||
QVERIFY(healthCheckWidget);
|
||||
|
||||
QTest::mouseClick(healthCheckWidget, Qt::LeftButton);
|
||||
QTableView* healthTable = healthCheckWidget->findChild<QTableView*>("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<QCheckBox*>("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<QMenu*>("customMenu");
|
||||
QVERIFY(menu);
|
||||
QAction* excludeEntryAction = healthCheckWidget->findChild<QAction*>("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<QDialogButtonBox*>("buttonBox");
|
||||
QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton);
|
||||
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
|
||||
}
|
||||
|
||||
void TestGui::addCannedEntries()
|
||||
{
|
||||
// Find buttons
|
||||
|
|
@ -2502,6 +2847,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*>("editGroupWidget");
|
||||
auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
|
||||
auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("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<QToolBar*>("toolBar");
|
||||
QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild<QAction*>("actionEntryNew"));
|
||||
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
|
||||
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
|
||||
auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
|
||||
auto* passwordEdit =
|
||||
editEntryWidget->findChild<PasswordWidget*>("passwordEdit")->findChild<QLineEdit*>("passwordEdit");
|
||||
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("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<CompositeKey>::create();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue