File indexing completed on 2024-05-19 05:13:19

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Daniel Vrátil <dvratil@redhat.com>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-or-later
0005  *
0006  */
0007 
0008 #include "tagpropertiesdialog.h"
0009 #include <Akonadi/DbAccess>
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <Akonadi/AttributeFactory>
0014 #include <QDialogButtonBox>
0015 #include <QSqlError>
0016 #include <QSqlQuery>
0017 
0018 using namespace Akonadi;
0019 
0020 TagPropertiesDialog::TagPropertiesDialog(QWidget *parent)
0021     : QDialog(parent)
0022 {
0023     setupUi();
0024 }
0025 
0026 TagPropertiesDialog::TagPropertiesDialog(const Akonadi::Tag &tag, QWidget *parent)
0027     : QDialog(parent)
0028     , mTag(tag)
0029     , mChanged(false)
0030 {
0031     setupUi();
0032 }
0033 
0034 TagPropertiesDialog::~TagPropertiesDialog() = default;
0035 
0036 Tag TagPropertiesDialog::tag() const
0037 {
0038     return mTag;
0039 }
0040 
0041 bool TagPropertiesDialog::changed() const
0042 {
0043     return mChanged;
0044 }
0045 
0046 void TagPropertiesDialog::setupUi()
0047 {
0048     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
0049     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0050     okButton->setDefault(true);
0051     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0052     connect(buttonBox, &QDialogButtonBox::accepted, this, &TagPropertiesDialog::slotAccept);
0053     connect(buttonBox, &QDialogButtonBox::rejected, this, &TagPropertiesDialog::reject);
0054 
0055     auto widget = new QWidget(this);
0056     ui.setupUi(widget);
0057     auto mainLayout = new QVBoxLayout(this);
0058     mainLayout->addWidget(widget);
0059     mainLayout->addWidget(buttonBox);
0060 
0061     connect(ui.addAttrButton, &QPushButton::clicked, this, &TagPropertiesDialog::addAttributeClicked);
0062     connect(ui.deleteAttrButton, &QPushButton::clicked, this, &TagPropertiesDialog::deleteAttributeClicked);
0063 
0064     connect(ui.addRIDButton, &QPushButton::clicked, this, &TagPropertiesDialog::addRIDClicked);
0065     connect(ui.deleteRIDButton, &QPushButton::clicked, this, &TagPropertiesDialog::deleteRIDClicked);
0066 
0067     Attribute::List attributes = mTag.attributes();
0068     mAttributesModel = new QStandardItemModel(attributes.size(), 2, this);
0069     connect(mAttributesModel, &QStandardItemModel::itemChanged, this, &TagPropertiesDialog::attributeChanged);
0070     QStringList labels;
0071     labels << i18n("Attribute") << i18n("Value");
0072     mAttributesModel->setHorizontalHeaderLabels(labels);
0073 
0074     mRemoteIdsModel = new QStandardItemModel(this);
0075     connect(mRemoteIdsModel, &QStandardItemModel::itemChanged, this, &TagPropertiesDialog::remoteIdChanged);
0076     mRemoteIdsModel->setColumnCount(2);
0077     labels.clear();
0078     labels << i18n("Resource") << i18n("Remote ID");
0079     mRemoteIdsModel->setHorizontalHeaderLabels(labels);
0080     ui.ridsView->setModel(mRemoteIdsModel);
0081 
0082     if (mTag.isValid()) {
0083         ui.idLabel->setText(QString::number(mTag.id()));
0084         ui.typeEdit->setText(QLatin1StringView(mTag.type()));
0085         ui.gidEdit->setText(QLatin1StringView(mTag.gid()));
0086         ui.parentIdLabel->setText(QString::number(mTag.parent().id()));
0087 
0088         for (int i = 0; i < attributes.count(); ++i) {
0089             QModelIndex index = mAttributesModel->index(i, 0);
0090             Q_ASSERT(index.isValid());
0091             mAttributesModel->setData(index, QLatin1StringView(attributes[i]->type()));
0092             mAttributesModel->item(i, 0)->setEditable(false);
0093             index = mAttributesModel->index(i, 1);
0094             Q_ASSERT(index.isValid());
0095             mAttributesModel->setData(index, QLatin1StringView(attributes[i]->serialized()));
0096             mAttributesModel->item(i, 1)->setEditable(true);
0097         }
0098 
0099         {
0100             // There is (intentionally) no way to retrieve Tag RID for another
0101             // resource than in the current context. Since Akonadi Console has
0102             // not resource context at all, we need to retrieve the IDs the hard way
0103             QSqlQuery query(DbAccess::database());
0104             query.prepare(
0105                 QStringLiteral("SELECT ResourceTable.name, TagRemoteIdResourceRelationTable.remoteId "
0106                                "FROM TagRemoteIdResourceRelationTable "
0107                                "LEFT JOIN ResourceTable ON ResourceTable.id = TagRemoteIdResourceRelationTable.resourceId "
0108                                "WHERE TagRemoteIdResourceRelationTable.tagid = ?"));
0109             query.addBindValue(mTag.id());
0110             if (query.exec()) {
0111                 while (query.next()) {
0112                     QList<QStandardItem *> items;
0113                     auto item = new QStandardItem(query.value(0).toString());
0114                     item->setEditable(false);
0115                     items << item;
0116                     item = new QStandardItem(query.value(1).toString());
0117                     item->setEditable(true);
0118                     items << item;
0119                     mRemoteIdsModel->appendRow(items);
0120                 }
0121             } else {
0122                 qCritical() << query.executedQuery();
0123                 qCritical() << query.lastError().text();
0124             }
0125         }
0126     } else {
0127         ui.idLabel->setVisible(false);
0128         ui.idLabelBuddy->setVisible(false);
0129         if (mTag.parent().isValid()) {
0130             ui.parentIdLabel->setText(QString::number(mTag.parent().id()));
0131         } else {
0132             ui.parentIdLabel->setVisible(false);
0133             ui.parentIdLabelBuddy->setVisible(false);
0134         }
0135         // Since we are using direct SQL to update RIDs, we cannot do this
0136         // when creating a new Tag, because the tag is created by caller after
0137         // this dialog is closed
0138         ui.tabWidget->setTabEnabled(2, false);
0139     }
0140 
0141     ui.attrsView->setModel(mAttributesModel);
0142 }
0143 
0144 void TagPropertiesDialog::addAttributeClicked()
0145 {
0146     const QString newType = ui.newAttrEdit->text();
0147     if (newType.isEmpty()) {
0148         return;
0149     }
0150     ui.newAttrEdit->clear();
0151 
0152     mChangedAttrs.insert(newType);
0153     mRemovedAttrs.remove(newType);
0154     mChanged = true;
0155 
0156     const int row = mAttributesModel->rowCount();
0157     mAttributesModel->insertRow(row);
0158     const QModelIndex index = mAttributesModel->index(row, 0);
0159     Q_ASSERT(index.isValid());
0160     mAttributesModel->setData(index, newType);
0161     mAttributesModel->item(row, 0)->setEditable(false);
0162     mAttributesModel->setItem(row, 1, new QStandardItem);
0163     mAttributesModel->item(row, 1)->setEditable(true);
0164 }
0165 
0166 void TagPropertiesDialog::deleteAttributeClicked()
0167 {
0168     const QModelIndexList selection = ui.attrsView->selectionModel()->selectedRows();
0169     if (selection.count() != 1) {
0170         return;
0171     }
0172     const QString attr = selection.first().data().toString();
0173     mChangedAttrs.remove(attr);
0174     mRemovedAttrs.insert(attr);
0175     mChanged = true;
0176     mAttributesModel->removeRow(selection.first().row());
0177 }
0178 
0179 void TagPropertiesDialog::attributeChanged(QStandardItem *item)
0180 {
0181     const QString attr = mAttributesModel->data(mAttributesModel->index(item->row(), 0)).toString();
0182     mRemovedAttrs.remove(attr);
0183     mChangedAttrs.insert(attr);
0184     mChanged = true;
0185 }
0186 
0187 void TagPropertiesDialog::addRIDClicked()
0188 {
0189     const QString newResource = ui.newRIDEdit->text();
0190     if (newResource.isEmpty()) {
0191         return;
0192     }
0193     ui.newRIDEdit->clear();
0194 
0195     mChangedRIDs.insert(newResource);
0196     mRemovedRIDs.remove(newResource);
0197     // Don't change mChanged here, we will handle this internally
0198 
0199     const int row = mRemoteIdsModel->rowCount();
0200     mRemoteIdsModel->insertRow(row);
0201     const QModelIndex index = mRemoteIdsModel->index(row, 0);
0202     Q_ASSERT(index.isValid());
0203     mRemoteIdsModel->setData(index, newResource);
0204     mRemoteIdsModel->item(row, 0)->setEditable(false);
0205     mRemoteIdsModel->setItem(row, 1, new QStandardItem);
0206     mRemoteIdsModel->item(row, 1)->setEditable(true);
0207 }
0208 
0209 void TagPropertiesDialog::deleteRIDClicked()
0210 {
0211     const QModelIndexList selection = ui.ridsView->selectionModel()->selectedRows();
0212     if (selection.count() != 1) {
0213         return;
0214     }
0215     const QString res = selection.first().data().toString();
0216     mChangedRIDs.remove(res);
0217     mRemovedRIDs.insert(res);
0218     // Don't change mChanged here, we will handle this internally
0219     mRemoteIdsModel->removeRow(selection.first().row());
0220 }
0221 
0222 void TagPropertiesDialog::remoteIdChanged(QStandardItem *item)
0223 {
0224     const QString res = mRemoteIdsModel->data(mRemoteIdsModel->index(item->row(), 0)).toString();
0225     mRemovedRIDs.remove(res);
0226     mChangedRIDs.insert(res);
0227     // Don't change mChanged here, we will handle this internally
0228 }
0229 
0230 void TagPropertiesDialog::slotAccept()
0231 {
0232     mChanged |= (mTag.type() != ui.typeEdit->text().toLatin1());
0233     mChanged |= (mTag.gid() != ui.gidEdit->text().toLatin1());
0234 
0235     if (!mChanged && mChangedRIDs.isEmpty() && mRemovedRIDs.isEmpty()) {
0236         QDialog::accept();
0237         return;
0238     }
0239 
0240     mTag.setType(ui.typeEdit->text().toLatin1());
0241     mTag.setGid(ui.gidEdit->text().toLatin1());
0242 
0243     for (const QString &removedAttr : std::as_const(mRemovedAttrs)) {
0244         mTag.removeAttribute(removedAttr.toLatin1());
0245     }
0246     for (int i = 0; i < mAttributesModel->rowCount(); ++i) {
0247         const QModelIndex typeIndex = mAttributesModel->index(i, 0);
0248         Q_ASSERT(typeIndex.isValid());
0249         if (!mChangedAttrs.contains(typeIndex.data().toString())) {
0250             continue;
0251         }
0252         const QModelIndex valueIndex = mAttributesModel->index(i, 1);
0253         Attribute *attr = AttributeFactory::createAttribute(mAttributesModel->data(typeIndex).toString().toLatin1());
0254         if (!attr) {
0255             continue;
0256         }
0257         attr->deserialize(mAttributesModel->data(valueIndex).toString().toLatin1());
0258         mTag.addAttribute(attr);
0259     }
0260 
0261     bool queryOK = true;
0262     if (mTag.isValid() && (!mRemovedRIDs.isEmpty() || !mChangedRIDs.isEmpty())) {
0263         DbAccess::database().transaction();
0264     }
0265 
0266     if (mTag.isValid() && !mRemovedRIDs.isEmpty()) {
0267         QSqlQuery query(DbAccess::database());
0268         QString queryStr = QStringLiteral(
0269             "DELETE FROM TagRemoteIdResourceRelationTable "
0270             "WHERE tagId = ? AND "
0271             "resourceId IN (SELECT id "
0272             "FROM ResourceTable "
0273             "WHERE ");
0274         QStringList conds;
0275         for (int i = 0; i < mRemovedRIDs.count(); ++i) {
0276             conds << QStringLiteral("name = ?");
0277         }
0278         queryStr += conds.join(QLatin1StringView(" OR ")) + QLatin1Char(')');
0279         query.prepare(queryStr);
0280         query.addBindValue(mTag.id());
0281         for (const QString &removedRid : std::as_const(mRemovedRIDs)) {
0282             query.addBindValue(removedRid);
0283         }
0284         if (!query.exec()) {
0285             qCritical() << query.executedQuery();
0286             qCritical() << query.lastError().text();
0287             queryOK = false;
0288         }
0289     }
0290     if (queryOK && mTag.isValid() && !mChangedRIDs.isEmpty()) {
0291         QMap<QString, qint64> resourceNameToIdMap;
0292         QList<qint64> existingResourceRecords;
0293         {
0294             QSqlQuery query(DbAccess::database());
0295             QString queryStr = QStringLiteral("SELECT id, name FROM ResourceTable WHERE ");
0296             QStringList conds;
0297             for (int i = 0; i < mChangedRIDs.count(); ++i) {
0298                 conds << QStringLiteral("name = ?");
0299             }
0300             queryStr += conds.join(QLatin1StringView(" OR "));
0301             query.prepare(queryStr);
0302             for (const QString &res : std::as_const(mChangedRIDs)) {
0303                 query.addBindValue(res);
0304             }
0305             if (!query.exec()) {
0306                 qCritical() << query.executedQuery();
0307                 qCritical() << query.lastError().text();
0308                 queryOK = false;
0309             }
0310 
0311             while (query.next()) {
0312                 resourceNameToIdMap[query.value(1).toString()] = query.value(0).toLongLong();
0313             }
0314         }
0315 
0316         // This is a workaround for PSQL not supporting UPSERTs
0317         {
0318             QSqlQuery query(DbAccess::database());
0319             query.prepare(QStringLiteral("SELECT resourceId FROM TagRemoteIdResourceRelationTable WHERE tagId = ?"));
0320             query.addBindValue(mTag.id());
0321             if (!query.exec()) {
0322                 qCritical() << query.executedQuery();
0323                 qCritical() << query.lastError().text();
0324                 queryOK = false;
0325             }
0326 
0327             while (query.next()) {
0328                 existingResourceRecords << query.value(0).toLongLong();
0329             }
0330         }
0331 
0332         for (int i = 0; i < mRemoteIdsModel->rowCount() && queryOK; ++i) {
0333             const QModelIndex resIndex = mRemoteIdsModel->index(i, 0);
0334             Q_ASSERT(resIndex.isValid());
0335             if (!mChangedRIDs.contains(resIndex.data().toString())) {
0336                 continue;
0337             }
0338             const QModelIndex valueIndex = mRemoteIdsModel->index(i, 1);
0339 
0340             QSqlQuery query(DbAccess::database());
0341             const qlonglong resourceId = resourceNameToIdMap[resIndex.data().toString()];
0342             if (existingResourceRecords.contains(resourceId)) {
0343                 query.prepare(QStringLiteral("UPDATE TagRemoteIdResourceRelationTable SET remoteId = ? WHERE tagId = ? AND resourceId = ?"));
0344                 query.addBindValue(valueIndex.data().toString());
0345                 query.addBindValue(mTag.id());
0346                 query.addBindValue(resourceId);
0347             } else {
0348                 query.prepare(QStringLiteral("INSERT INTO TagRemoteIdResourceRelationTable (tagId, resourceId, remoteId) VALUES (?, ?, ?)"));
0349                 query.addBindValue(mTag.id());
0350                 query.addBindValue(resourceId);
0351                 query.addBindValue(valueIndex.data().toString());
0352             }
0353             if (!query.exec()) {
0354                 qCritical() << query.executedQuery();
0355                 qCritical() << query.lastError().text();
0356                 queryOK = false;
0357                 break;
0358             }
0359         }
0360     }
0361 
0362     if (mTag.isValid() && (!mRemovedRIDs.isEmpty() || !mChangedRIDs.isEmpty())) {
0363         if (queryOK) {
0364             DbAccess::database().commit();
0365         } else {
0366             DbAccess::database().rollback();
0367         }
0368     }
0369 
0370     QDialog::accept();
0371 }
0372 
0373 #include "moc_tagpropertiesdialog.cpp"