/* * Copyright (c) 2015 Joern Rischmueller (joern.rm@gmail.com) * * 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 of the License, or (at your option) * any later version. * * 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, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "CloudDBChart.h" #include "CloudDBCommon.h" #include "CloudDBStatus.h" #include "CloudService.h" #include "LTMChartParser.h" #include "GcUpgrade.h" #include "Colors.h" #ifdef GC_WANT_R #include #endif #ifdef GC_WANT_PYTHON #include "PythonEmbed.h" #endif #include #include #include #include #include #include #include #include #include #include #include CloudDBChartClient::CloudDBChartClient() { g_nam = new QNetworkAccessManager(this); QDir cacheDir(QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation).at(0)); cacheDir.cdUp(); g_cacheDir = QString(cacheDir.absolutePath()+"/GoldenCheetahCloudDB"); QDir newCacheDir(g_cacheDir); if (!newCacheDir.exists()) { cacheDir.mkdir("GoldenCheetahCloudDB"); } // general handling for sslErrors connect(g_nam, SIGNAL(sslErrors(QNetworkReply*,QList)), this, SLOT(sslErrors(QNetworkReply*,QList))); // common definitions used g_chart_url_base = g_chart_url_header = g_chartcuration_url_base = g_chartdownloadincr_url_base = CloudDBCommon::cloudDBBaseURL; g_chart_url_base.append("gchart/"); g_chart_url_header.append("gchartheader"); g_chartcuration_url_base.append("gchartcuration/"); g_chartdownloadincr_url_base.append("gchartuse/"); } CloudDBChartClient::~CloudDBChartClient() { delete g_nam; } bool CloudDBChartClient::postChart(ChartAPIv1 chart) { // check if Athlete ID is filled if (chart.Header.CreatorId.isEmpty()) return CloudDBCommon::APIresponseOthers; // default where it may be necessary if (chart.Header.Language.isEmpty()) chart.Header.Language = "en"; // first create the JSON object // only a subset of fields is required for POST QJsonObject json_header; CloudDBCommon::marshallAPIHeaderV1Object(json_header, chart.Header); QJsonObject json; json["header"] = json_header; json["chartSport"] = chart.ChartSport; json["chartType"] = chart.ChartType; json["chartView"] = chart.ChartView; json["chartDef"] = chart.ChartDef; QString image; image.append(chart.Image.toBase64()); json["image"] = image; json["creatornick"] = chart.CreatorNick; json["creatoremail"] = chart.CreatorEmail; QJsonDocument document; document.setObject(json); QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chart_url_base); g_reply = g_nam->post(request, document.toJson()); // wait for reply (synchronously) and process error codes as necessary if (!CloudDBCommon::replyReceivedAndOk(g_reply)) return false; CloudDBHeader::setChartHeaderStale(true); return true; } bool CloudDBChartClient::putChart(ChartAPIv1 chart) { // we assume all field are filled properly / not further check or modification // first create the JSON object / all fields are required for PUT / only LastChanged i QJsonObject json_header; CloudDBCommon::marshallAPIHeaderV1Object(json_header, chart.Header); QJsonObject json; json["header"] = json_header; json["chartSport"] = chart.ChartSport; json["chartType"] = chart.ChartType; json["chartView"] = chart.ChartView; json["chartDef"] = chart.ChartDef; QString image; image.append(chart.Image.toBase64()); json["image"] = image; json["creatornick"] = chart.CreatorNick; json["creatoremail"] = chart.CreatorEmail; QJsonDocument document; document.setObject(json); QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chart_url_base); g_reply = g_nam->put(request, document.toJson()); // wait for reply (synchronously) and process error codes as necessary if (!CloudDBCommon::replyReceivedAndOk(g_reply)) return false; // cache is stale deleteChartCache(chart.Header.Id); CloudDBHeader::setChartHeaderStale(true); return true; } bool CloudDBChartClient::getChartByID(qint64 id, ChartAPIv1 *chart) { // read from Cache first if (readChartCache(id, chart)) return CloudDBCommon::APIresponseOk; // now from GAE QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chart_url_base+QString::number(id, 10)); g_reply = g_nam->get(request); // wait for reply (synchronously) and process error codes as necessary if (!CloudDBCommon::replyReceivedAndOk(g_reply)) return false; // call successfull QByteArray result = g_reply->readAll(); QList* charts = new QList; unmarshallAPIv1(result, charts); if (charts->size() > 0) { *chart = charts->value(0); writeChartCache(chart); charts->clear(); delete charts; return true; } delete charts; return false; } bool CloudDBChartClient::deleteChartByID(qint64 id) { QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chart_url_base+QString::number(id, 10)); g_reply = g_nam->deleteResource(request); // wait for reply (synchronously) and process error codes as necessary if (!CloudDBCommon::replyReceivedAndOk(g_reply)) return false; deleteChartCache(id); return true; } bool CloudDBChartClient::curateChartByID(qint64 id, bool newStatus) { QUrlQuery query; query.addQueryItem("newStatus", (newStatus ? "true": "false")); QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chartcuration_url_base+QString::number(id, 10), &query); // add a payload to "PUT" even though it's not processed g_reply = g_nam->put(request, "{ \"id\": \"dummy\" }"); // wait for reply (synchronously) and process error codes as necessary if (!CloudDBCommon::replyReceivedAndOk(g_reply)) return false; deleteChartCache(id); return true; } void CloudDBChartClient::incrementDownloadCounterByID(qint64 id) { QNetworkRequest request; CloudDBCommon::prepareRequest(request, g_chartdownloadincr_url_base+QString::number(id, 10)); g_reply = g_nam->put(request, "{ \"id\": \"dummy\" }"); // ignore any errors or reply - user does not need to be informed in case of problems } bool CloudDBChartClient::getAllChartHeader(QList* header) { bool request_ok = CloudDBHeader::getAllCachedHeader(header, CloudDBHeader::CloudDB_Chart, g_cacheDir, g_chart_url_header, g_nam, g_reply); if (request_ok && header->size()>0) { cleanChartCache(header); } return request_ok; } // // Trap SSL errors // void CloudDBChartClient::sslErrors(QNetworkReply* reply ,QList errors) { CloudService::sslErrors(nullptr, reply, errors); } // Internal Methods bool CloudDBChartClient::writeChartCache(ChartAPIv1 * chart) { // make sure the subdir exists QDir cacheDir(g_cacheDir); if (cacheDir.exists()) { cacheDir.mkdir("charts"); } else { return false; } QFile file(g_cacheDir+"/charts/"+QString::number(chart->Header.Id)+".dat"); if (file.exists()) { // overwrite data file.resize(0); } if (!file.open(QIODevice::WriteOnly)) return false; QDataStream out(&file); out.setVersion(QDataStream::Qt_4_6); // track a version to be able change data structure out << chart_magic_string; out << chart_cache_version; out << chart->Header.LastChanged; // start out << chart->Header.CreatorId; out << chart->Header.Curated; out << chart->Header.Deleted; out << chart->Header.Description; out << chart->Header.GcVersion; out << chart->Header.Id; out << chart->Header.Language; out << chart->Header.Name; out << chart->ChartSport; out << chart->ChartType; out << chart->ChartView; out << chart->ChartDef; out << chart->CreatorEmail; out << chart->CreatorNick; out << chart->Image; file.close(); return true; } bool CloudDBChartClient::readChartCache(qint64 id, ChartAPIv1 *chart) { QFile file(g_cacheDir+"/charts/"+QString::number(id)+".dat"); if (!file.open(QIODevice::ReadOnly)) return false; QDataStream in(&file); in.setVersion(QDataStream::Qt_4_6); // track a version to be able change data structure int magic_string; int version; in >> magic_string; if (magic_string != chart_magic_string) { // wrong file format / close and exit file.close(); return false; } in >> version; if (version != chart_cache_version) { // change of version, delete old cache entry file.remove(); return false; } in >> chart->Header.LastChanged; // start in >> chart->Header.CreatorId; in >> chart->Header.Curated; in >> chart->Header.Deleted; in >> chart->Header.Description; in >> chart->Header.GcVersion; in >> chart->Header.Id; in >> chart->Header.Language; in >> chart->Header.Name; in >> chart->ChartSport; in >> chart->ChartType; in >> chart->ChartView; in >> chart->ChartDef; in >> chart->CreatorEmail; in >> chart->CreatorNick; in >> chart->Image; file.close(); return true; } void CloudDBChartClient::deleteChartCache(qint64 id) { QFile file(g_cacheDir+"/charts/"+QString::number(id)+".dat"); if (file.exists()) { file.remove(); } } void CloudDBChartClient::cleanChartCache(QList *objectHeader) { // remove Deleted Entries from Cache int deleted = 0; QMutableListIterator it(*objectHeader); while (it.hasNext()) { CommonAPIHeaderV1 header = it.next(); if (header.Deleted) { deleteChartCache(header.Id); it.remove(); deleted ++; } } if (deleted > 0) { // store cache for next time CloudDBHeader::writeHeaderCache(objectHeader, CloudDBHeader::CloudDB_Chart, g_cacheDir); } } bool CloudDBChartClient::unmarshallAPIv1(QByteArray json, QList *charts) { QJsonParseError parseError; QJsonDocument document = QJsonDocument::fromJson(json, &parseError); // all these things should not happen and we have not valid object to return if (parseError.error != QJsonParseError::NoError || document.isEmpty() || document.isNull()) { return false; } // do we have a single object or an array ? if (document.isObject()) { ChartAPIv1 chart; QJsonObject object = document.object(); unmarshallAPIv1Object(&object, &chart); QJsonObject header = object["header"].toObject(); CloudDBCommon::unmarshallAPIHeaderV1Object(&header, &chart.Header); charts->append(chart); } else if (document.isArray()) { QJsonArray array(document.array()); for (int i = 0; i< array.size(); i++) { QJsonValue value = array.at(i); if (value.isObject()) { ChartAPIv1 chart; QJsonObject object = value.toObject(); unmarshallAPIv1Object(&object, &chart); QJsonObject header = object["header"].toObject(); CloudDBCommon::unmarshallAPIHeaderV1Object(&header, &chart.Header); charts->append(chart); } } } return true; } void CloudDBChartClient::unmarshallAPIv1Object(QJsonObject* object, ChartAPIv1* chart) { chart->ChartSport = object->value("chartSport").toString(); chart->ChartType = object->value("chartType").toString(); chart->ChartView = object->value("chartView").toString(); chart->ChartDef = object->value("chartDef").toString(); QString imageString = object->value("image").toString(); QByteArray ba; ba.append(imageString.toUtf8()); chart->Image = QByteArray::fromBase64(ba); chart->CreatorNick = object->value("creatorNick").toString(); chart->CreatorEmail = object->value("creatorEmail").toString(); } //------------------------------------------------------------------------------------------------------------ // Dialog to show and retrieve Shared Charts //------------------------------------------------------------------------------------------------------------ // define size if image preview at one place static const int chartImageWidth = 320; static const int chartImageHeight = 240; CloudDBChartListDialog::CloudDBChartListDialog() : const_stepSize(5) { g_client = new CloudDBChartClient(); g_currentHeaderList = new QList; g_fullHeaderList = new QList; CloudDBHeader::setChartHeaderStale(true); //Force Headers to be loaded g_currentPresets = new QList; g_textFilterActive = false; g_networkrequestactive = false; // don't allow Dialog to close while we are retrieving data showing = new QLabel; showingTextTemplate = tr("Showing %1 to %2 of %3 charts for %4 / Total uploaded %5"); resetToStart = new QPushButton(tr("First")); nextSet = new QPushButton(tr("Next %1").arg(QString::number(const_stepSize))); prevSet = new QPushButton(tr("Prev %1").arg(QString::number(const_stepSize))); resetToStart->setEnabled(true); nextSet->setDefault(true); nextSet->setEnabled(true); prevSet->setEnabled(true); connect(resetToStart, SIGNAL(clicked()), this, SLOT(resetToStartClicked())); connect(nextSet, SIGNAL(clicked()), this, SLOT(nextSetClicked())); connect(prevSet, SIGNAL(clicked()), this, SLOT(prevSetClicked())); showingLayout = new QHBoxLayout; showingLayout->addWidget(showing); showingLayout->addStretch(); showingLayout->addWidget(resetToStart); showingLayout->addWidget(prevSet); showingLayout->addWidget(nextSet); ownChartsOnly = new QCheckBox(tr("My Charts")); ownChartsOnly->setChecked(false); connect(ownChartsOnly, SIGNAL(toggled(bool)), this, SLOT(ownChartsToggled(bool))); curationStateCombo = new QComboBox(); curationStateCombo->addItem(tr("All")); curationStateCombo->addItem(tr("Curated Only")); curationStateCombo->addItem(tr("Uncurated Only")); // default to curated charts only curationStateCombo->setCurrentIndex(1); connect(curationStateCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(curationStateFilterChanged(int))); sportCombo = new QComboBox(); sportCombo->addItem(tr("Any Sport")); foreach (QString sport, CloudDBCommon::cloudDBSports) { sportCombo->addItem(sport); } connect(sportCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(sportComboFilterChanged(int))); langCombo = new QComboBox(); langCombo->addItem(tr("Any Language")); foreach (QString lang, CloudDBCommon::cloudDBLangs) { langCombo->addItem(lang); } textFilterApply = new QPushButton(tr("Search Keyword")); textFilterApply->setFixedWidth(180 *dpiXFactor); // To allow proper translation textFilter = new QLineEdit; g_textFilterActive = false; connect(textFilter, SIGNAL(editingFinished()), this, SLOT(textFilterEditingFinished())); connect(langCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(languageFilterChanged(int))); connect(textFilterApply, SIGNAL(clicked()), this, SLOT(toggleTextFilterApply())); filterLayout = new QHBoxLayout; filterLayout->addWidget(ownChartsOnly); filterLayout->addStretch(); filterLayout->addWidget(curationStateCombo); filterLayout->addStretch(); filterLayout->addWidget(sportCombo); filterLayout->addStretch(); filterLayout->addWidget(langCombo); filterLayout->addStretch(); filterLayout->addWidget(textFilterApply); filterLayout->addWidget(textFilter); textFilter->setVisible(true); tableWidget = new QTableWidget(0, 2); tableWidget->setContentsMargins(0,0,0,0); tableWidget->horizontalHeader()->setStretchLastSection(true); tableWidget->horizontalHeader()->setVisible(false); tableWidget->verticalHeader()->setVisible(false); tableWidget->setShowGrid(true); // the preview shall have a dedicated size tableWidget->setColumnWidth(0, chartImageWidth); connect(tableWidget, SIGNAL(cellDoubleClicked(int,int)), this, SLOT(cellDoubleClicked(int,int))); // UserGet Role addAndCloseUserGetButton = new QPushButton(tr("Download selected chart(s)")); closeUserGetButton = new QPushButton(tr("Close")); addAndCloseUserGetButton->setEnabled(true); closeUserGetButton->setEnabled(true); closeUserGetButton->setDefault(true); connect(addAndCloseUserGetButton, SIGNAL(clicked()), this, SLOT(addAndCloseClicked())); connect(closeUserGetButton, SIGNAL(clicked()), this, SLOT(closeClicked())); buttonUserGetLayout = new QHBoxLayout; buttonUserGetLayout->addStretch(); buttonUserGetLayout->addWidget(addAndCloseUserGetButton); buttonUserGetLayout->addWidget(closeUserGetButton); // UserEdit Role deleteUserEditButton = new QPushButton(tr("Delete selected chart")); editUserEditButton = new QPushButton(tr("Edit selected chart")); closeUserEditButton = new QPushButton(tr("Close")); connect(deleteUserEditButton, SIGNAL(clicked()), this, SLOT(deleteUserEdit())); connect(editUserEditButton, SIGNAL(clicked()), this, SLOT(editUserEdit())); connect(closeUserEditButton, SIGNAL(clicked()), this, SLOT(closeClicked())); buttonUserEditLayout = new QHBoxLayout; buttonUserEditLayout->addStretch(); buttonUserEditLayout->addWidget(deleteUserEditButton); buttonUserEditLayout->addWidget(editUserEditButton); buttonUserEditLayout->addWidget(closeUserEditButton); // CuratorEdit Role curateCuratorEditButton = new QPushButton(tr("Set selected chart 'Curated'")); editCuratorEditButton = new QPushButton(tr("Edit selected chart")); deleteCuratorEditButton = new QPushButton(tr("Delete selected chart")); closeCuratorButton = new QPushButton(tr("Close")); connect(curateCuratorEditButton, SIGNAL(clicked()), this, SLOT(curateCuratorEdit())); connect(editCuratorEditButton, SIGNAL(clicked()), this, SLOT(editCuratorEdit())); connect(deleteCuratorEditButton, SIGNAL(clicked()), this, SLOT(deleteCuratorEdit())); connect(closeCuratorButton, SIGNAL(clicked()), this, SLOT(closeClicked())); buttonCuratorEditLayout = new QHBoxLayout; buttonCuratorEditLayout->addStretch(); buttonCuratorEditLayout->addWidget(curateCuratorEditButton); buttonCuratorEditLayout->addWidget(editCuratorEditButton); buttonCuratorEditLayout->addWidget(deleteCuratorEditButton); buttonCuratorEditLayout->addWidget(closeCuratorButton); // prepare the main layouts - with different buttons layouts mainLayout = new QVBoxLayout; mainLayout->addLayout(showingLayout); mainLayout->addLayout(filterLayout); mainLayout->addWidget(tableWidget); mainLayout->addLayout(buttonUserGetLayout); mainLayout->addLayout(buttonUserEditLayout); mainLayout->addLayout(buttonCuratorEditLayout); setMinimumHeight(500 *dpiYFactor); setMinimumWidth(700 *dpiXFactor); setLayout(mainLayout); } CloudDBChartListDialog::~CloudDBChartListDialog() { delete g_client; delete g_currentHeaderList; delete g_fullHeaderList; delete g_currentPresets; } // block DialogWindow close while networkrequest is processed void CloudDBChartListDialog::closeEvent(QCloseEvent* event) { if (g_networkrequestactive) { event->ignore(); return; } QDialog::closeEvent(event); } bool CloudDBChartListDialog::prepareData(QString athlete, CloudDBCommon::UserRole role, int chartView) { g_role = role; g_chartView = chartView; // and now initialize the dialog setVisibleButtonsForRole(); if (g_role == CloudDBCommon::UserEdit) { ownChartsOnly->setChecked(true); ownChartsOnly->setEnabled(false); setWindowTitle(tr("Chart maintenance - Edit or Delete your Charts")); tableWidget->setSelectionMode(QAbstractItemView::SingleSelection); tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); } else if (role == CloudDBCommon::CuratorEdit) { ownChartsOnly->setChecked(false); curationStateCombo->setCurrentIndex(2); // start with uncurated setWindowTitle(tr("Curator chart maintenance - Curate, Edit or Delete Charts")); tableWidget->setSelectionMode(QAbstractItemView::SingleSelection); tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); } else { setWindowTitle(tr("Select charts to download")); tableWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); } if (CloudDBHeader::isStaleChartHeader()) { if (!refreshStaleChartHeader()) return false; CloudDBHeader::setChartHeaderStale(false); } g_currentAthleteId = appsettings->cvalue(athlete, GC_ATHLETE_ID, "").toString(); applyAllFilters(); return true; } void CloudDBChartListDialog::updateCurrentPresets(int index, int count) { // while getting the data (which may take of few seconds), disable the UI resetToStart->setEnabled(false); nextSet->setEnabled(false); prevSet->setEnabled(false); closeUserGetButton->setEnabled(false); addAndCloseUserGetButton->setEnabled(false); curationStateCombo->setEnabled(false); ownChartsOnly->setEnabled(false); textFilterApply->setEnabled(false); sportCombo->setEnabled(false); langCombo->setEnabled(false); deleteUserEditButton->setEnabled(false); editUserEditButton->setEnabled(false); closeUserEditButton->setEnabled(false); curateCuratorEditButton->setEnabled(false); editCuratorEditButton->setEnabled(false); deleteCuratorEditButton->setEnabled(false); closeCuratorButton->setEnabled(false); // now get the presets g_currentPresets->clear(); ChartAPIv1* chart = new ChartAPIv1; g_networkrequestactive = true; bool noError = true; for (int i = index; i< index+count && icount() && noError; i++) { if (g_client->getChartByID(g_currentHeaderList->at(i).Id, chart)) { ChartWorkingStructure preset; preset.id = chart->Header.Id; preset.name = chart->Header.Name; preset.description = chart->Header.Description; preset.language = chart->Header.Language; preset.createdAt = chart->Header.LastChanged; preset.image.loadFromData(chart->Image); preset.gchartType = chart->ChartType; preset.gchartView = chart->ChartView; preset.gchartDef = chart->ChartDef; preset.creatorNick = chart->CreatorNick; preset.gchartSport = chart->ChartSport; g_currentPresets->append(preset); } else { noError = false; } } g_networkrequestactive = false; delete chart; // update table with current list tableWidget->clearContents(); tableWidget->setRowCount(0); int chartCount = (g_currentHeaderList->size() == 0) ? 0 : g_currentIndex+1; int lastIndex = (g_currentIndex+const_stepSize > g_currentHeaderList->size()) ? g_currentHeaderList->size() : g_currentIndex+const_stepSize; QString view = tr("unknown"); if (g_role == CloudDBCommon::UserImport ) { switch(g_chartView) { case 0 : view = tr("Trends"); break; case 1 : view = tr("Activities"); break; case 2 : view = tr("Plan"); break; case 3 : view = tr("Train"); break; } } else { view = tr("All Views"); } showing->setText(QString(showingTextTemplate) .arg(QString::number(chartCount)) .arg(QString::number(lastIndex)) .arg(QString::number(g_currentHeaderList->size())) .arg(view) .arg(QString::number(g_fullHeaderList->size()))); for (int i = 0; i< g_currentPresets->size(); i++ ) { tableWidget->insertRow(i); ChartWorkingStructure preset = g_currentPresets->at(i); QTableWidgetItem *newPxItem = new QTableWidgetItem(""); newPxItem->setData(Qt::DecorationRole, QVariant(preset.image.scaled(chartImageWidth, chartImageHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation))); newPxItem->setSizeHint(QSize(chartImageWidth, chartImageHeight)); newPxItem->setFlags(newPxItem->flags() & ~Qt::ItemIsEditable); tableWidget->setItem(i, 0, newPxItem); tableWidget->item(i,0)->setBackground(Qt::darkGray); tableWidget->setRowHeight(i, chartImageHeight+20); QString cellText = QString(tr("

%1

Last Edited At: %2 - Creator: %3

%4")) .arg(CloudDBCommon::encodeHTML(preset.name)) .arg(preset.createdAt.date().toString(Qt::ISODate)) .arg(CloudDBCommon::encodeHTML(preset.creatorNick)) .arg(CloudDBCommon::encodeHTML(preset.description)); QTextEdit *formattedText = new QTextEdit; formattedText->setHtml(cellText); formattedText->setReadOnly(true); tableWidget->setCellWidget(i,1, formattedText); } // enable UI again resetToStart->setEnabled(true); nextSet->setEnabled(true); prevSet->setEnabled(true); closeUserGetButton->setEnabled(true); addAndCloseUserGetButton->setEnabled(true); curationStateCombo->setEnabled(true); ownChartsOnly->setEnabled(true); textFilterApply->setEnabled(true); sportCombo->setEnabled(true); langCombo->setEnabled(true); deleteUserEditButton->setEnabled(true); editUserEditButton->setEnabled(true); closeUserEditButton->setEnabled(true); curateCuratorEditButton->setEnabled(true); editCuratorEditButton->setEnabled(true); deleteCuratorEditButton->setEnabled(true); closeCuratorButton->setEnabled(true); // role dependent UI settings if (g_role == CloudDBCommon::UserEdit) { ownChartsOnly->setEnabled(false); } } void CloudDBChartListDialog::setVisibleButtonsForRole() { if (g_role == CloudDBCommon::UserEdit) { deleteUserEditButton->setVisible(true); editUserEditButton->setVisible(true); closeUserEditButton->setVisible(true); curateCuratorEditButton->setVisible(false); editCuratorEditButton->setVisible(false); deleteCuratorEditButton->setVisible(false); closeCuratorButton->setVisible(false); closeUserGetButton->setVisible(false); addAndCloseUserGetButton->setVisible(false); } else if (g_role == CloudDBCommon::CuratorEdit) { deleteUserEditButton->setVisible(false); editUserEditButton->setVisible(false); closeUserEditButton->setVisible(false); curateCuratorEditButton->setVisible(true); editCuratorEditButton->setVisible(true); deleteCuratorEditButton->setVisible(true); closeCuratorButton->setVisible(true); closeUserGetButton->setVisible(false); addAndCloseUserGetButton->setVisible(false); } else { deleteUserEditButton->setVisible(false); editUserEditButton->setVisible(false); closeUserEditButton->setVisible(false); curateCuratorEditButton->setVisible(false); editCuratorEditButton->setVisible(false); deleteCuratorEditButton->setVisible(false); closeCuratorButton->setVisible(false); closeUserGetButton->setVisible(true); addAndCloseUserGetButton->setVisible(true); } } bool CloudDBChartListDialog::refreshStaleChartHeader() { g_fullHeaderList->clear(); g_currentHeaderList->clear(); g_currentIndex = 0; g_networkrequestactive = true; if (!g_client->getAllChartHeader(g_fullHeaderList)) { g_networkrequestactive = false; return false; } g_networkrequestactive = false; return true; } void CloudDBChartListDialog::addAndCloseClicked() { // check if an item of the table is selected QList selected = tableWidget->selectedItems(); if (selected.count()>0) { g_selected.clear(); // the selectionMode allows multiple selection so get them all for (int i = 0; i< selected.count(); i++) { QTableWidgetItem* s = selected.at(i); if (s->row() >= 0 && s->row() <= g_currentPresets->count()) { #if defined(GC_WANT_R) || defined (GC_WANT_PYTHON) int chartType = g_currentPresets->at(s->row()).gchartType.toInt(); #ifdef GC_WANT_R if (chartType == GcWindowTypes::RConsole || chartType == GcWindowTypes::RConsoleSeason ) { if (rtool == NULL) { QMessageBox::information(0, tr("Chart requires 'R'"), tr("The chart your are downloading requires 'R' to be installed \ and activated for GoldenCheetah to show any graphics. Either 'R' is not activated \ in the preferences, or not even installed.

Please ensure 'R' \ is installed and activated to be able to use this chart.")); } } #endif #ifdef GC_WANT_PYTHON if (chartType == GcWindowTypes::Python || chartType == GcWindowTypes::PythonSeason ) { if (python == NULL) { QMessageBox::information(0, tr("Chart requires 'Python'"), tr("The chart your are downloading requires 'Python' to be installed \ and activated for GoldenCheetah to show any graphics. Either 'Python' is not activated \ in the preferences, or not even installed.

Please ensure 'Python' \ is installed and activated to be able to use this chart.")); } } #endif #endif g_selected << g_currentPresets->at(s->row()).gchartDef; // increment download counter g_client->incrementDownloadCounterByID(g_currentPresets->at(s->row()).id); } } accept(); } } void CloudDBChartListDialog::closeClicked() { reject(); } void CloudDBChartListDialog::resetToStartClicked() { if (g_currentIndex == 0) return; g_currentIndex = 0; updateCurrentPresets(g_currentIndex, const_stepSize); } void CloudDBChartListDialog::nextSetClicked() { g_currentIndex += const_stepSize; if (g_currentIndex >= g_currentHeaderList->size()) { g_currentIndex = g_currentHeaderList->size() - const_stepSize; } if (g_currentIndex < 0) g_currentIndex = 0; updateCurrentPresets(g_currentIndex, const_stepSize); } void CloudDBChartListDialog::prevSetClicked() { g_currentIndex -= const_stepSize; if (g_currentIndex < 0) g_currentIndex = 0; updateCurrentPresets(g_currentIndex, const_stepSize); } void CloudDBChartListDialog::curateCuratorEdit() { if (tableWidget->selectedItems().size()>0) { // the selectionMode allows only 1 item to be selected at a time QTableWidgetItem* s = tableWidget->selectedItems().at(0); if (s->row() >= 0 && s->row() <= g_currentPresets->count()) { qint64 id = g_currentPresets->at(s->row()).id; if (!g_client->curateChartByID(id, true)) { return; } // refresh header buffer refreshStaleChartHeader(); // curated chart appears on top of the list / and needs to be filtered applyAllFilters(); } } } void CloudDBChartListDialog::deleteCuratorEdit(){ // currently same like User deleteUserEdit(); } void CloudDBChartListDialog::editCuratorEdit(){ // currently same like User editUserEdit(); } void CloudDBChartListDialog::deleteUserEdit(){ if (tableWidget->selectedItems().size()>0) { // chart selected for deletion - but ask again to be sure if (QMessageBox::question(0, tr("Chart Maintenance"), QString(tr("Do you really want to delete this chart definition ?"))) != QMessageBox::Yes) return; // the selectionMode allows only 1 item to be selected at a time QTableWidgetItem* s = tableWidget->selectedItems().at(0); if (s->row() >= 0 && s->row() <= g_currentPresets->count()) { qint64 id = g_currentPresets->at(s->row()).id; if (!g_client->deleteChartByID(id)) { return; } // set stale for subsequent list dialog calls CloudDBHeader::setChartHeaderStale(true); // remove deleted chart from both lists QMutableListIterator it1(*g_currentHeaderList); while (it1.hasNext()) { if (it1.next().Id == id) { it1.remove(); break; // there is just one equal entry } } QMutableListIterator it2(*g_fullHeaderList); while (it2.hasNext()) { if (it2.next().Id == id) { it2.remove(); break; // there is just one equal entry } } if (g_currentIndex >= g_currentHeaderList->size()) { g_currentIndex = g_currentHeaderList->size() - const_stepSize; } if (g_currentIndex < 0) g_currentIndex = 0; updateCurrentPresets(g_currentIndex, const_stepSize); } } } void CloudDBChartListDialog::editUserEdit(){ if (tableWidget->selectedItems().size()>0) { // the selectionMode allows only 1 item to be selected at a time QTableWidgetItem* s = tableWidget->selectedItems().at(0); if (s->row() >= 0 && s->row() <= g_currentPresets->count()) { qint64 id = g_currentPresets->at(s->row()).id; ChartAPIv1 chart; if (!g_client->getChartByID(id, &chart)) { return; } // now complete the chart with for the user manually added fields CloudDBChartObjectDialog dialog(chart, "", true); if (dialog.exec() == QDialog::Accepted) { if (g_client->putChart(dialog.getChart())) { refreshStaleChartHeader(); } else { return; } } else { return; } } // updated chart appears on top of the list / and needs to be filtered applyAllFilters(); } } void CloudDBChartListDialog::curationStateFilterChanged(int) { applyAllFilters(); } void CloudDBChartListDialog::sportComboFilterChanged(int) { applyAllFilters(); } void CloudDBChartListDialog::ownChartsToggled(bool) { applyAllFilters(); } void CloudDBChartListDialog::toggleTextFilterApply() { if (g_textFilterActive) { g_textFilterActive = false; textFilterApply->setText(tr("Search Keyword")); applyAllFilters(); } else { g_textFilterActive = true; textFilterApply->setText(tr("Reset Search")); applyAllFilters(); } } void CloudDBChartListDialog::languageFilterChanged(int) { applyAllFilters(); } void CloudDBChartListDialog::applyAllFilters() { // setup to only show charts that are relevant to the current view unsigned int mask=0; switch(g_chartView) { case 0 : mask = VIEW_TRENDS; break; default: case 1 : mask = VIEW_ANALYSIS; break; case 2 : mask = VIEW_PLAN; break; case 3 : mask = VIEW_TRAIN; break; } QStringList searchList; g_currentHeaderList->clear(); if (!textFilter->text().isEmpty()) { // split by any whitespace searchList = textFilter->text().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); } foreach (CommonAPIHeaderV1 chart, *g_fullHeaderList) { // list does not contain any deleted chart id's // first filter based on the current view (Home, Analysis, Plan) - but only on Imp bool chartOkForView = false; if (g_role == CloudDBCommon::UserImport) { int winId = chart.ChartType.toInt(); for (int i = 0; GcWindows[i].relevance; i++) { if (GcWindows[i].id == winId && (GcWindows[i].relevance & mask)) { chartOkForView = true; break; } } } else { // for editing and curation all charts are shown chartOkForView = true; } int curationState = curationStateCombo->currentIndex(); if (chartOkForView) { // check curated first if (curationState == 0 || (curationState == 1 && chart.Curated) || (curationState == 2 && !chart.Curated ) ) { //check own chart only if (!ownChartsOnly->isChecked() || (ownChartsOnly->isChecked() && chart.CreatorId == g_currentAthleteId)) { // then check sport (observe that we have an additional entry "all" in the combo ! if (sportCombo->currentIndex() == 0 || (sportCombo->currentIndex() > 0 && CloudDBCommon::cloudDBSportIds[sportCombo->currentIndex()-1] == chart.ChartSport )) { // then check language (observe that we have an additional entry "all" in the combo ! if (langCombo->currentIndex() == 0 || (langCombo->currentIndex() > 0 && CloudDBCommon::cloudDBLangsIds[langCombo->currentIndex()-1] == chart.Language )) { // at last check text filter if (g_textFilterActive) { // search with filter Keywords QString chartInfo = chart.Name + " " + chart.Description; foreach (QString search, searchList) { if (chartInfo.contains(search, Qt::CaseInsensitive)) { g_currentHeaderList->append(chart); break; } } } else { g_currentHeaderList->append(chart); } } } } } } } g_currentIndex = 0; // now get the data updateCurrentPresets(g_currentIndex, const_stepSize); } void CloudDBChartListDialog::textFilterEditingFinished() { if (g_textFilterActive) { applyAllFilters(); } } void CloudDBChartListDialog::cellDoubleClicked(int row, int /*column */) { ChartAPIv1* chart = new ChartAPIv1; if (row >= 0 && row < g_currentHeaderList->size() ) { g_networkrequestactive = true; if (g_client->getChartByID(g_currentHeaderList->at(g_currentIndex+row).Id, chart)) { CloudDBChartShowPictureDialog showImage(chart->Image); showImage.exec(); } } g_networkrequestactive = false; delete chart; } CloudDBChartShowPictureDialog::CloudDBChartShowPictureDialog(QByteArray imageData) : imageData(imageData) { QRect rec = QApplication::primaryScreen()->geometry(); imageLabel = new QLabel(); imageLabel->setMinimumSize(150,100); // not scaled to hi-dpi screens (cloud db constraint not device) chartImage.loadFromData(imageData); QSize size = chartImage.size(); int w = rec.width()/2; int h = (qreal)size.width()*w/size.height(); imageLabel->setPixmap(chartImage.scaled(w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation)); okButton = new QPushButton(tr("Close")); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addStretch(); buttonLayout->addWidget(okButton); connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked())); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(imageLabel); layout->addLayout(buttonLayout); setLayout(layout); } CloudDBChartShowPictureDialog::~CloudDBChartShowPictureDialog() { delete imageLabel; delete okButton; } void CloudDBChartShowPictureDialog::okClicked() { accept(); } void CloudDBChartShowPictureDialog::resizeEvent(QResizeEvent *) { int w = imageLabel->width(); int h = imageLabel->height(); // set a scaled pixmap to a w x h window keeping its aspect ratio imageLabel->setPixmap(chartImage.scaled(w,h,Qt::KeepAspectRatio, Qt::SmoothTransformation)); } //------------------------------------------------------------------------------------------------------------ // Dialog for publishing Chart Details //------------------------------------------------------------------------------------------------------------ CloudDBChartObjectDialog::CloudDBChartObjectDialog(ChartAPIv1 data, QString athlete, bool update) : data(data), athlete(athlete), update(update) { QLabel *chartName = new QLabel(tr("Chart Name")); name = new QLineEdit(); nameDefault = tr(""); name->setMaxLength(50); if (update) { name->setText(data.Header.Name); nameOk = true; // no need to re-check } else { name->setText(nameDefault); nameOk = false; } QRegularExpression name_rx("^.{5,50}$"); QValidator *name_validator = new QRegularExpressionValidator(name_rx, this); name->setValidator(name_validator); QLabel* sportLabel = new QLabel(tr("Sport")); sportCombo = new QComboBox(); foreach (QString sport, CloudDBCommon::cloudDBSports) { sportCombo->addItem(sport); } if (update) { int index = CloudDBCommon::cloudDBSportIds.indexOf(data.ChartSport); sportCombo->setCurrentIndex( index<0 ? 0 : index); } QLabel* langLabel = new QLabel(tr("Language")); langCombo = new QComboBox(); foreach (QString lang, CloudDBCommon::cloudDBLangs) { langCombo->addItem(lang); } if (update) { int index = CloudDBCommon::cloudDBLangsIds.indexOf(data.Header.Language); langCombo->setCurrentIndex( index<0 ? 0 : index); } connect(name, SIGNAL(textChanged(QString)), this, SLOT(nameTextChanged(QString))); connect(name, SIGNAL(editingFinished()), this, SLOT(nameEditingFinished())); QLabel *nickLabel = new QLabel(tr("Nickname")); nickName = new QLineEdit(); if (update) { nickName->setText(data.CreatorNick); } else { nickName->setText(appsettings->cvalue(athlete, GC_NICKNAME, "").toString()); } QLabel *emailLabel = new QLabel(tr("E-Mail")); email = new QLineEdit(); email->setMaxLength(100); if (update) { email->setText(data.CreatorEmail); } else { email->setText(appsettings->cvalue(athlete, GC_CLOUDDB_EMAIL, "").toString()); } // regexp: simple e-mail validation / also allow long domain types & subdomains QRegularExpression email_rx("^.+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,10}$"); QValidator *email_validator = new QRegularExpressionValidator(email_rx, this); email->setValidator(email_validator); emailOk = !email->text().isEmpty(); // email from properties is ok when loaded connect(email, SIGNAL(textChanged(QString)), this, SLOT(emailTextChanged(QString))); connect(email, SIGNAL(editingFinished()), this, SLOT(emailEditingFinished())); QLabel* gcVersionLabel = new QLabel(tr("Version Details")); QString versionString = VERSION_STRING; versionString.append(" : " + data.Header.GcVersion); gcVersionString = new QLabel(versionString); QLabel* creatorIdLabel = new QLabel(tr("Creator UUid")); creatorId = new QLabel(data.Header.CreatorId); QGridLayout *detailsLayout = new QGridLayout; detailsLayout->addWidget(chartName, 0, 0, Qt::AlignLeft); detailsLayout->addWidget(name, 0, 1); detailsLayout->addWidget(sportLabel, 1, 0, Qt::AlignLeft); detailsLayout->addWidget(sportCombo, 1, 1, Qt::AlignLeft); detailsLayout->addWidget(langLabel, 1, 2, Qt::AlignLeft); detailsLayout->addWidget(langCombo, 1, 3, Qt::AlignLeft); detailsLayout->addWidget(nickLabel, 2, 0, Qt::AlignLeft); detailsLayout->addWidget(nickName, 2, 1); detailsLayout->addWidget(emailLabel, 2, 2, Qt::AlignLeft); detailsLayout->addWidget(email, 2, 3); detailsLayout->addWidget(gcVersionLabel, 3, 0, Qt::AlignLeft); detailsLayout->addWidget(gcVersionString, 3, 1); detailsLayout->addWidget(creatorIdLabel, 3, 2, Qt::AlignLeft); detailsLayout->addWidget(creatorId, 3, 3); description = new QTextEdit(); description->setAcceptRichText(false); descriptionDefault = tr(""); description->setText(descriptionDefault); if (update) { description->setText(data.Header.Description); } else { description->setText(descriptionDefault); } QPixmap *chartImage = new QPixmap(); chartImage->loadFromData(data.Image); image = new QLabel(); image->setPixmap(chartImage->scaled(chartImageWidth*2, chartImageHeight*2, Qt::KeepAspectRatio, Qt::SmoothTransformation)); publishButton = new QPushButton(tr("Upload"), this); cancelButton = new QPushButton(tr("Cancel"), this); publishButton->setEnabled(true); connect(publishButton, SIGNAL(clicked()), this, SLOT(publishClicked())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addStretch(); buttonLayout->addWidget(publishButton); buttonLayout->addWidget(cancelButton); QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->addLayout(detailsLayout); mainLayout->addWidget(description); mainLayout->addWidget(image); mainLayout->addLayout(buttonLayout); setLayout(mainLayout); } CloudDBChartObjectDialog::~CloudDBChartObjectDialog() { } void CloudDBChartObjectDialog::publishClicked() { // check data consistency if (name->text().isEmpty() || name->text() == nameDefault || !nameOk) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a valid chart name with min. 5 characters length."))); return; } if (nickName->text().isEmpty()) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a nickname for this athlete."))); return; } if (email->text().isEmpty() || !emailOk) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a valid e-mail address."))); return; } if (description->toPlainText().isEmpty() || description->toPlainText() == descriptionDefault) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a sensible chart description."))); return; } if (QMessageBox::question(0, tr("Cloud Upload"), QString(tr("Do you want to upload this chart definition ?"))) != QMessageBox::Yes) return; data.Header.Name = name->text(); data.Header.Description = description->toPlainText(); data.CreatorEmail = email->text(); data.CreatorNick = nickName->text(); data.Header.Language = CloudDBCommon::cloudDBLangsIds.at(langCombo->currentIndex()); data.ChartSport = CloudDBCommon::cloudDBSportIds.at(sportCombo->currentIndex()); if (!update) { appsettings->setCValue(athlete, GC_NICKNAME, data.CreatorNick); appsettings->setCValue(athlete, GC_CLOUDDB_EMAIL, data.CreatorEmail); } accept(); } void CloudDBChartObjectDialog::cancelClicked() { reject(); } void CloudDBChartObjectDialog::emailTextChanged(QString text) { if (text.isEmpty()) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a valid e-mail address."))); } else { emailOk = false; } } void CloudDBChartObjectDialog::emailEditingFinished() { // validator check passed emailOk = true; } void CloudDBChartObjectDialog::nameTextChanged(QString text) { if (text.isEmpty()) { QMessageBox::warning(0, tr("Upload Chart"), QString(tr("Please enter a valid chart name with min. 5 characters length."))); } else { nameOk = false; } } void CloudDBChartObjectDialog::nameEditingFinished() { // validator check passed nameOk = true; }