data/RelationSuggestions.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RelationSuggestions.h" | ||
| 2 | |||
| 3 | #include "ContactIndex.h" | ||
| 4 | #include "ContactQueries.h" | ||
| 5 | #include "MailAddressList.h" | ||
| 6 | #include "RelationIndex.h" | ||
| 7 | |||
| 8 | #include <QDateTime> | ||
| 9 | #include <QLoggingCategory> | ||
| 10 | #include <QSqlError> | ||
| 11 | #include <QSqlQuery> | ||
| 12 | #include <QVariant> | ||
| 13 | #include <algorithm> | ||
| 14 | |||
| 15 | Q_DECLARE_LOGGING_CATEGORY(lcContactPeople) | ||
| 16 | |||
| 17 | namespace RelationSuggestions { | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 |
2/2✓ Branch 2 → 3 taken 51 times.
✓ Branch 2 → 4 taken 49 times.
|
100 | Pair pairOf(qint64 a, qint64 b) { return a < b ? Pair{a, b} : Pair{b, a}; } |
| 22 | |||
| 23 | // Every pair of the cards in one mail or appointment counts once. | ||
| 24 | 77 | void countTogether(const QSet<qint64> &cards, bool mail, QHash<Pair, Counts> *counts) { | |
| 25 |
5/6✓ Branch 3 → 4 taken 69 times.
✓ Branch 3 → 6 taken 8 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 69 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 69 times.
|
77 | if (cards.size() < 2 || cards.size() > 30) // a mailing list or a large meeting says little |
| 26 | 8 | return; | |
| 27 |
1/1✓ Branch 12 → 13 taken 69 times.
|
69 | QList<qint64> list(cards.cbegin(), cards.cend()); |
| 28 |
3/3✓ Branch 13 → 14 taken 69 times.
✓ Branch 14 → 15 taken 69 times.
✓ Branch 15 → 16 taken 69 times.
|
69 | std::sort(list.begin(), list.end()); |
| 29 |
2/2✓ Branch 30 → 17 taken 138 times.
✓ Branch 30 → 31 taken 69 times.
|
207 | for (int i = 0; i < list.size(); ++i) |
| 30 |
2/2✓ Branch 27 → 18 taken 69 times.
✓ Branch 27 → 28 taken 138 times.
|
207 | for (int j = i + 1; j < list.size(); ++j) { |
| 31 |
1/1✓ Branch 21 → 22 taken 69 times.
|
69 | Counts &c = (*counts)[Pair{list.at(i), list.at(j)}]; |
| 32 |
2/2✓ Branch 22 → 23 taken 59 times.
✓ Branch 22 → 24 taken 10 times.
|
69 | (mail ? c.mails : c.events) += 1; |
| 33 | } | ||
| 34 | 69 | } | |
| 35 | |||
| 36 | 183 | void addAddresses(const QString &header, const Cards &cards, QSet<qint64> *found) { | |
| 37 |
5/5✓ Branch 2 → 3 taken 183 times.
✓ Branch 3 → 4 taken 183 times.
✓ Branch 4 → 5 taken 183 times.
✓ Branch 15 → 6 taken 152 times.
✓ Branch 15 → 16 taken 183 times.
|
335 | for (const MailAddressList::Mailbox &mailbox : MailAddressList::parse(header)) |
| 38 |
3/3✓ Branch 7 → 8 taken 152 times.
✓ Branch 10 → 11 taken 120 times.
✓ Branch 10 → 13 taken 32 times.
|
152 | if (const qint64 card = cards.byAddress.value(ContactIndex::addressKey(mailbox.address)); card > 0) |
| 39 |
1/1✓ Branch 11 → 12 taken 120 times.
|
303 | found->insert(card); |
| 40 | 183 | } | |
| 41 | |||
| 42 | // S106-173: UIDs compare without "urn:uuid:" and case, as every other UID | ||
| 43 | // resolution of the people cluster does. | ||
| 44 | 378 | QString uidKey(const QString &uid) { | |
| 45 |
1/1✓ Branch 2 → 3 taken 378 times.
|
378 | QString key = uid.trimmed(); |
| 46 |
3/3✓ Branch 4 → 5 taken 378 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 10 taken 377 times.
|
378 | if (key.startsWith(QLatin1String("urn:uuid:"), Qt::CaseInsensitive)) |
| 47 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | key = key.mid(9); |
| 48 |
1/1✓ Branch 10 → 11 taken 378 times.
|
756 | return key.toLower(); |
| 49 | 378 | } | |
| 50 | |||
| 51 | } // namespace | ||
| 52 | |||
| 53 | 6 | Cards readCards(const QSqlDatabase &contacts, const QString &meKey) { | |
| 54 | 6 | Cards cards; | |
| 55 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | QSqlQuery q(contacts); |
| 56 |
1/1✓ Branch 3 → 4 taken 6 times.
|
6 | q.setForwardOnly(true); |
| 57 |
2/3✓ Branch 7 → 8 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
|
6 | if (!q.exec(QStringLiteral("SELECT c.id, c.book_id, c.uid FROM cards c JOIN address_books b ON b.id = c.book_id" |
| 58 | " WHERE b.show_in_all = 1 AND c.kind <> 'group'"))) | ||
| 59 | ✗ | return cards; | |
| 60 |
3/3✓ Branch 53 → 54 taken 188 times.
✓ Branch 54 → 13 taken 182 times.
✓ Branch 54 → 55 taken 6 times.
|
188 | while (q.next()) { |
| 61 |
5/5✓ Branch 13 → 14 taken 182 times.
✓ Branch 14 → 15 taken 182 times.
✓ Branch 15 → 16 taken 182 times.
✓ Branch 16 → 17 taken 182 times.
✓ Branch 17 → 18 taken 182 times.
|
182 | const QString key = ContactQueries::cardKey(q.value(1).toLongLong(), q.value(2).toString()); |
| 62 |
2/2✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 179 times.
|
182 | if (key == meKey) |
| 63 | 3 | continue; | |
| 64 |
3/3✓ Branch 24 → 25 taken 179 times.
✓ Branch 25 → 26 taken 179 times.
✓ Branch 26 → 27 taken 179 times.
|
179 | cards.keyOf.insert(q.value(0).toLongLong(), key); |
| 65 |
4/5✓ Branch 28 → 29 taken 179 times.
✓ Branch 29 → 30 taken 179 times.
✓ Branch 30 → 31 taken 179 times.
✓ Branch 35 → 36 taken 179 times.
✗ Branch 35 → 47 not taken.
|
179 | if (!cards.byUid.contains(uidKey(q.value(2).toString()))) |
| 66 |
6/6✓ Branch 36 → 37 taken 179 times.
✓ Branch 37 → 38 taken 179 times.
✓ Branch 38 → 39 taken 179 times.
✓ Branch 39 → 40 taken 179 times.
✓ Branch 40 → 41 taken 179 times.
✓ Branch 41 → 42 taken 179 times.
|
179 | cards.byUid.insert(uidKey(q.value(2).toString()), q.value(0).toLongLong()); |
| 67 | 182 | } | |
| 68 |
1/1✓ Branch 55 → 56 taken 6 times.
|
6 | QSqlQuery addresses(contacts); |
| 69 |
1/1✓ Branch 56 → 57 taken 6 times.
|
6 | addresses.setForwardOnly(true); |
| 70 |
2/3✓ Branch 60 → 61 taken 6 times.
✓ Branch 63 → 64 taken 6 times.
✗ Branch 63 → 93 not taken.
|
6 | if (addresses.exec(QStringLiteral("SELECT card_id, address_key FROM card_addresses"))) |
| 71 |
3/3✓ Branch 91 → 92 taken 174 times.
✓ Branch 92 → 65 taken 168 times.
✓ Branch 92 → 93 taken 6 times.
|
174 | while (addresses.next()) |
| 72 |
14/22✓ Branch 65 → 66 taken 168 times.
✓ Branch 66 → 67 taken 168 times.
✓ Branch 68 → 69 taken 165 times.
✓ Branch 68 → 74 taken 3 times.
✓ Branch 69 → 70 taken 165 times.
✓ Branch 70 → 71 taken 165 times.
✓ Branch 72 → 73 taken 165 times.
✗ Branch 72 → 74 not taken.
✓ Branch 75 → 76 taken 165 times.
✓ Branch 75 → 77 taken 3 times.
✓ Branch 77 → 78 taken 165 times.
✓ Branch 77 → 79 taken 3 times.
✓ Branch 79 → 80 taken 168 times.
✗ Branch 79 → 81 not taken.
✓ Branch 81 → 82 taken 165 times.
✓ Branch 81 → 91 taken 3 times.
✗ Branch 145 → 146 not taken.
✗ Branch 145 → 147 not taken.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
|
168 | if (cards.keyOf.contains(addresses.value(0).toLongLong()) && !cards.byAddress.contains(addresses.value(1).toString())) |
| 73 |
5/5✓ Branch 82 → 83 taken 165 times.
✓ Branch 83 → 84 taken 165 times.
✓ Branch 84 → 85 taken 165 times.
✓ Branch 85 → 86 taken 165 times.
✓ Branch 86 → 87 taken 165 times.
|
165 | cards.byAddress.insert(addresses.value(1).toString(), addresses.value(0).toLongLong()); |
| 74 | 6 | return cards; | |
| 75 | 6 | } | |
| 76 | |||
| 77 | 5 | void countMails(const QSqlDatabase &mailCache, const Cards &cards, qint64 sinceSecs, QHash<Pair, Counts> *counts) { | |
| 78 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | QSqlQuery q(mailCache); |
| 79 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | q.setForwardOnly(true); |
| 80 |
1/1✓ Branch 7 → 8 taken 5 times.
|
5 | q.prepare(QStringLiteral("SELECT message_id, from_addr, to_addr, cc_addr FROM headers WHERE date >= ?" |
| 81 | " ORDER BY date DESC LIMIT ?")); | ||
| 82 |
1/1✓ Branch 12 → 13 taken 5 times.
|
5 | q.addBindValue(sinceSecs); |
| 83 |
1/1✓ Branch 16 → 17 taken 5 times.
|
5 | q.addBindValue(MaxMails); |
| 84 |
2/3✓ Branch 18 → 19 taken 5 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 36 taken 5 times.
|
5 | if (!q.exec()) { |
| 85 | ✗ | qCWarning(lcContactPeople) << "Suggestions: mails could not be read:" << q.lastError().text(); | |
| 86 | ✗ | return; | |
| 87 | } | ||
| 88 | 5 | QSet<QString> seen; // a mail in Inbox and Sent counts once | |
| 89 | 5 | int mails = 0; | |
| 90 |
3/3✓ Branch 67 → 68 taken 69 times.
✓ Branch 68 → 38 taken 64 times.
✓ Branch 68 → 69 taken 5 times.
|
69 | while (q.next()) { |
| 91 |
2/2✓ Branch 38 → 39 taken 64 times.
✓ Branch 39 → 40 taken 64 times.
|
64 | const QString id = q.value(0).toString(); |
| 92 |
5/6✓ Branch 42 → 43 taken 64 times.
✗ Branch 42 → 46 not taken.
✓ Branch 44 → 45 taken 3 times.
✓ Branch 44 → 46 taken 61 times.
✓ Branch 47 → 48 taken 3 times.
✓ Branch 47 → 49 taken 61 times.
|
64 | if (!id.isEmpty() && seen.contains(id)) |
| 93 | 3 | continue; | |
| 94 |
1/1✓ Branch 49 → 50 taken 61 times.
|
61 | seen.insert(id); |
| 95 | 61 | QSet<qint64> found; | |
| 96 |
2/2✓ Branch 58 → 52 taken 183 times.
✓ Branch 58 → 59 taken 61 times.
|
244 | for (int column = 1; column <= 3; ++column) |
| 97 |
3/3✓ Branch 52 → 53 taken 183 times.
✓ Branch 53 → 54 taken 183 times.
✓ Branch 54 → 55 taken 183 times.
|
183 | addAddresses(q.value(column).toString(), cards, &found); |
| 98 |
1/1✓ Branch 59 → 60 taken 61 times.
|
61 | countTogether(found, true, counts); |
| 99 | 61 | ++mails; | |
| 100 | 64 | } | |
| 101 |
6/6✓ Branch 69 → 70 taken 5 times.
✓ Branch 73 → 74 taken 5 times.
✓ Branch 74 → 75 taken 5 times.
✓ Branch 75 → 76 taken 5 times.
✓ Branch 79 → 71 taken 5 times.
✓ Branch 79 → 80 taken 5 times.
|
10 | qCDebug(lcContactPeople) << "Suggestions: mails looked at:" << mails; |
| 102 | 5 | } | |
| 103 | |||
| 104 | 6 | void countEvents(const QSqlDatabase &calendar, const Cards &cards, qint64 sinceSecs, QHash<Pair, Counts> *counts) { | |
| 105 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | QSqlQuery q(calendar); |
| 106 |
1/1✓ Branch 3 → 4 taken 6 times.
|
6 | q.setForwardOnly(true); |
| 107 |
1/1✓ Branch 7 → 8 taken 6 times.
|
6 | q.prepare(QStringLiteral("SELECT p.event_id, p.card_uid, p.address FROM event_people p JOIN events e" |
| 108 | " ON e.id = p.event_id WHERE e.dt_start >= ? ORDER BY p.event_id")); | ||
| 109 |
1/1✓ Branch 12 → 13 taken 6 times.
|
6 | q.addBindValue(sinceSecs); |
| 110 |
2/3✓ Branch 14 → 15 taken 6 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 32 taken 6 times.
|
6 | if (!q.exec()) { |
| 111 | ✗ | qCWarning(lcContactPeople) << "Suggestions: appointments could not be read:" << q.lastError().text(); | |
| 112 | ✗ | return; | |
| 113 | } | ||
| 114 | 6 | qint64 current = -1; | |
| 115 | 6 | QSet<qint64> found; | |
| 116 |
3/3✓ Branch 72 → 73 taken 26 times.
✓ Branch 73 → 34 taken 20 times.
✓ Branch 73 → 74 taken 6 times.
|
26 | while (q.next()) { |
| 117 |
2/2✓ Branch 34 → 35 taken 20 times.
✓ Branch 35 → 36 taken 20 times.
|
20 | const qint64 event = q.value(0).toLongLong(); |
| 118 |
2/2✓ Branch 37 → 38 taken 10 times.
✓ Branch 37 → 41 taken 10 times.
|
20 | if (event != current) { |
| 119 |
1/1✓ Branch 38 → 39 taken 10 times.
|
10 | countTogether(found, false, counts); |
| 120 | 10 | found.clear(); | |
| 121 | 10 | current = event; | |
| 122 | } | ||
| 123 |
3/3✓ Branch 41 → 42 taken 20 times.
✓ Branch 42 → 43 taken 20 times.
✓ Branch 43 → 44 taken 20 times.
|
20 | qint64 card = cards.byUid.value(uidKey(q.value(1).toString()), 0); |
| 124 |
11/16✓ Branch 48 → 49 taken 3 times.
✓ Branch 48 → 54 taken 17 times.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 50 → 51 taken 3 times.
✓ Branch 52 → 53 taken 3 times.
✗ Branch 52 → 54 not taken.
✓ Branch 55 → 56 taken 3 times.
✓ Branch 55 → 57 taken 17 times.
✓ Branch 57 → 58 taken 3 times.
✓ Branch 57 → 59 taken 17 times.
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 68 taken 17 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
|
20 | if (card == 0 && !q.value(2).toString().isEmpty()) |
| 125 |
3/3✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
|
3 | card = cards.byAddress.value(ContactIndex::addressKey(q.value(2).toString()), 0); |
| 126 |
1/2✓ Branch 68 → 69 taken 20 times.
✗ Branch 68 → 71 not taken.
|
20 | if (card > 0) |
| 127 |
1/1✓ Branch 69 → 70 taken 20 times.
|
20 | found.insert(card); |
| 128 | } | ||
| 129 |
1/1✓ Branch 74 → 75 taken 6 times.
|
6 | countTogether(found, false, counts); |
| 130 | 6 | } | |
| 131 | |||
| 132 | 5 | QSet<Pair> relatedPairs(const QSqlDatabase &contacts) { | |
| 133 | 5 | QSet<Pair> related; | |
| 134 |
5/5✓ Branch 3 → 4 taken 5 times.
✓ Branch 4 → 5 taken 5 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 15 → 7 taken 101 times.
✓ Branch 15 → 16 taken 5 times.
|
106 | for (const RelationIndex::Edge &edge : RelationIndex::allEdges(contacts)) |
| 135 |
3/4✓ Branch 8 → 9 taken 99 times.
✓ Branch 8 → 13 taken 2 times.
✓ Branch 9 → 10 taken 99 times.
✗ Branch 9 → 13 not taken.
|
101 | if (edge.toCardId > 0 && edge.fromCardId != edge.toCardId) |
| 136 |
1/1✓ Branch 11 → 12 taken 99 times.
|
104 | related.insert(pairOf(edge.fromCardId, edge.toCardId)); |
| 137 | 5 | return related; | |
| 138 | ✗ | } | |
| 139 | |||
| 140 | 5 | QSet<Pair> dismissedPairs(const QSqlDatabase &contacts, const Cards &cards) { | |
| 141 | 5 | QHash<QString, qint64> idOf; | |
| 142 |
2/2✓ Branch 9 → 3 taken 177 times.
✓ Branch 9 → 10 taken 5 times.
|
182 | for (auto it = cards.keyOf.cbegin(); it != cards.keyOf.cend(); ++it) |
| 143 |
1/1✓ Branch 5 → 6 taken 177 times.
|
177 | idOf.insert(it.value(), it.key()); |
| 144 | 5 | QSet<Pair> dismissed; | |
| 145 |
1/1✓ Branch 11 → 12 taken 5 times.
|
5 | QSqlQuery q(contacts); |
| 146 |
2/3✓ Branch 15 → 16 taken 5 times.
✓ Branch 18 → 19 taken 5 times.
✗ Branch 18 → 37 not taken.
|
5 | if (q.exec(QStringLiteral("SELECT first_key, second_key FROM graph_dismissed"))) |
| 147 |
3/3✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 20 taken 1 time.
✓ Branch 36 → 37 taken 5 times.
|
6 | while (q.next()) |
| 148 |
6/8✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 35 not taken.
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 35 not taken.
|
1 | if (const qint64 a = idOf.value(q.value(0).toString()), b = idOf.value(q.value(1).toString()); a > 0 && b > 0) |
| 149 |
1/1✓ Branch 33 → 34 taken 1 time.
|
1 | dismissed.insert(pairOf(a, b)); |
| 150 | 5 | return dismissed; | |
| 151 | 5 | } | |
| 152 | |||
| 153 | 5 | QList<Suggestion> select(const QHash<Pair, Counts> &counts, const QSet<Pair> &related, const QSet<Pair> &dismissed) { | |
| 154 | 5 | QList<Suggestion> out; | |
| 155 |
2/2✓ Branch 26 → 3 taken 14 times.
✓ Branch 26 → 27 taken 5 times.
|
19 | for (auto it = counts.cbegin(); it != counts.cend(); ++it) { |
| 156 |
10/10✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 7 taken 6 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 13 taken 5 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 13 taken 3 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 5 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 17 taken 5 times.
|
14 | if ((it->mails < MinMails && it->events < MinEvents) || related.contains(it.key()) || dismissed.contains(it.key())) |
| 157 | 9 | continue; | |
| 158 |
1/1✓ Branch 21 → 22 taken 5 times.
|
5 | out.append({it.key().first, it.key().second, it->mails, it->events}); |
| 159 | } | ||
| 160 |
3/3✓ Branch 27 → 28 taken 5 times.
✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
|
5 | std::sort(out.begin(), out.end(), [](const Suggestion &a, const Suggestion &b) { |
| 161 | 4 | const int wa = a.mails + 3 * a.events, wb = b.mails + 3 * b.events; | |
| 162 |
1/4✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
|
4 | return wa != wb ? wa > wb : (a.first != b.first ? a.first < b.first : a.second < b.second); |
| 163 | }); | ||
| 164 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 5 times.
|
5 | if (out.size() > MaxSuggestions) |
| 165 | ✗ | out.resize(MaxSuggestions); | |
| 166 | 5 | return out; | |
| 167 | ✗ | } | |
| 168 | |||
| 169 | 1 | bool dismiss(QSqlDatabase &contacts, const QString &firstKey, const QString &secondKey) { | |
| 170 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | QSqlQuery q(contacts); |
| 171 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | q.prepare(QStringLiteral("INSERT OR REPLACE INTO graph_dismissed (first_key, second_key, dismissed_at) VALUES (?, ?, ?)")); |
| 172 |
1/1✓ Branch 12 → 13 taken 1 time.
|
1 | q.addBindValue(std::min(firstKey, secondKey)); |
| 173 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | q.addBindValue(std::max(firstKey, secondKey)); |
| 174 |
3/3✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
|
1 | q.addBindValue(QDateTime::currentDateTimeUtc().toString(Qt::ISODate)); |
| 175 |
1/1✓ Branch 27 → 28 taken 1 time.
|
2 | return q.exec(); |
| 176 | 1 | } | |
| 177 | |||
| 178 | 1 | bool forgetDismissed(QSqlDatabase &contacts) { | |
| 179 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | QSqlQuery q(contacts); |
| 180 |
1/1✓ Branch 6 → 7 taken 1 time.
|
2 | return q.exec(QStringLiteral("DELETE FROM graph_dismissed")); |
| 181 | 1 | } | |
| 182 | |||
| 183 | } // namespace RelationSuggestions | ||
| 184 |