diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 9bf5ec12b..fd7ddfad3 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -58,6 +58,7 @@ #include "BatchExportDialog.h" #include "StravaDialog.h" #include "RideWithGPSDialog.h" +#include "TtbDialog.h" #include "TwitterDialog.h" #include "WithingsDownload.h" #include "CalendarDownload.h" @@ -681,6 +682,10 @@ MainWindow::MainWindow(const QDir &home) : connect(rideWithGPSAction, SIGNAL(triggered(bool)), this, SLOT(uploadRideWithGPSAction())); rideMenu->addAction(rideWithGPSAction); + ttbAction = new QAction("Upload to Trainingstagebuch...", this); + connect(ttbAction, SIGNAL(triggered(bool)), this, SLOT(uploadTtb())); + rideMenu->addAction(ttbAction); + rideMenu->addSeparator (); rideMenu->addAction(tr("&Save activity"), this, SLOT(saveRide()), tr("Ctrl+S")); rideMenu->addAction(tr("D&elete activity..."), this, SLOT(deleteRide())); @@ -928,9 +933,14 @@ MainWindow::setActivityMenu() if (tripid == "") rideWithGPSAction->setEnabled(true); else rideWithGPSAction->setEnabled(false); + activityId = ride->ride()->getTag("TtbExercise", ""); + if (activityId == "") ttbAction->setEnabled(true); + else ttbAction->setEnabled(false); + } else { stravaAction->setEnabled(false); rideWithGPSAction->setEnabled(false); + ttbAction->setEnabled(false); } } @@ -1772,6 +1782,24 @@ MainWindow::uploadRideWithGPSAction() } } +/*---------------------------------------------------------------------- +* trainingstagebuch.org +*--------------------------------------------------------------------*/ + +void +MainWindow::uploadTtb() +{ + QTreeWidgetItem *_item = treeWidget->currentItem(); + if (_item==NULL || _item->type() != RIDE_TYPE) return; + + RideItem *item = dynamic_cast(_item); + + if (item) { // menu is disabled anyway, but belt and braces + TtbDialog d(this, item); + d.exec(); + } +} + /*---------------------------------------------------------------------- * ErgDB *--------------------------------------------------------------------*/ diff --git a/src/MainWindow.h b/src/MainWindow.h index c84b753a4..c5ec8916c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -236,6 +236,7 @@ class MainWindow : public QMainWindow void exportMetrics(); void uploadStrava(); void uploadRideWithGPSAction(); + void uploadTtb(); void downloadErgDB(); void manualProcess(QString); #ifdef GC_HAVE_SOAP @@ -346,6 +347,7 @@ class MainWindow : public QMainWindow QAction *sideView; QAction *toolView; QAction *stravaAction, *rideWithGPSAction; + QAction *ttbAction; QMenu *windowMenu; GcBubble *bubble; GcCalendar *gcCalendar; diff --git a/src/Pages.cpp b/src/Pages.cpp index 0e41abf07..55c27e5b4 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -492,6 +492,11 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr QLabel *rwgpsuserLabel = new QLabel(tr("Username")); QLabel *rwgpspassLabel = new QLabel(tr("Password")); + QLabel *ttb = new QLabel(tr("Trainingstagebuch")); + ttb->setFont(current); + + QLabel *ttbuserLabel = new QLabel(tr("Username")); + QLabel *ttbpassLabel = new QLabel(tr("Password")); QLabel *wip = new QLabel(tr("Withings Wifi Scales")); wip->setFont(current); @@ -560,6 +565,13 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr rideWithGPSPass->setEchoMode(QLineEdit::Password); rideWithGPSPass->setText(appsettings->cvalue(mainWindow->cyclist, GC_RWGPSPASS, "").toString()); + ttbUser = new QLineEdit(this); + ttbUser->setText(appsettings->cvalue(mainWindow->cyclist, GC_TTBUSER, "").toString()); + + ttbPass = new QLineEdit(this); + ttbPass->setEchoMode(QLineEdit::Password); + ttbPass->setText(appsettings->cvalue(mainWindow->cyclist, GC_TTBPASS, "").toString()); + wiURL = new QLineEdit(this); wiURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_WIURL, "http://wbsapi.withings.net/").toString()); @@ -613,6 +625,9 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr grid->addWidget(dvurlLabel, 27,0); grid->addWidget(dvuserLabel, 28,0); grid->addWidget(dvpassLabel, 29,0); + grid->addWidget(ttb, 30,0); + grid->addWidget(ttbuserLabel, 31,0); + grid->addWidget(ttbpassLabel, 32,0); grid->addWidget(tpURL, 1, 1, 0); grid->addWidget(tpUser, 2, 1, Qt::AlignLeft | Qt::AlignVCenter); @@ -643,6 +658,9 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr grid->addWidget(dvUser, 27, 1, Qt::AlignLeft | Qt::AlignVCenter); grid->addWidget(dvPass, 28, 1, Qt::AlignLeft | Qt::AlignVCenter); + grid->addWidget(ttbUser, 31, 1, Qt::AlignLeft | Qt::AlignVCenter); + grid->addWidget(ttbPass, 32, 1, Qt::AlignLeft | Qt::AlignVCenter); + grid->setColumnStretch(0,0); grid->setColumnStretch(1,3); @@ -669,6 +687,8 @@ CredentialsPage::saveClicked() appsettings->setCValue(mainWindow->cyclist, GC_STRPASS, stravaPass->text()); appsettings->setCValue(mainWindow->cyclist, GC_RWGPSUSER, rideWithGPSUser->text()); appsettings->setCValue(mainWindow->cyclist, GC_RWGPSPASS, rideWithGPSPass->text()); + appsettings->setCValue(mainWindow->cyclist, GC_TTBUSER, ttbUser->text()); + appsettings->setCValue(mainWindow->cyclist, GC_TTBPASS, ttbPass->text()); appsettings->setCValue(mainWindow->cyclist, GC_TPTYPE, tpType->currentIndex()); appsettings->setCValue(mainWindow->cyclist, GC_TWURL, twitterURL->text()); appsettings->setCValue(mainWindow->cyclist, GC_WIURL, wiURL->text()); diff --git a/src/Pages.h b/src/Pages.h index 106cdb2a2..572c65c1e 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -163,6 +163,9 @@ class CredentialsPage : public QScrollArea QLineEdit *rideWithGPSUser; QLineEdit *rideWithGPSPass; + QLineEdit *ttbUser; + QLineEdit *ttbPass; + QLineEdit *wiURL; // url for withings QLineEdit *wiUser; QLineEdit *wiPass; diff --git a/src/Settings.h b/src/Settings.h index b76a88ebe..2b971be74 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -94,6 +94,8 @@ #define GC_STRPASS "str/pass" #define GC_RWGPSUSER "rwgps/user" #define GC_RWGPSPASS "rwgps/pass" +#define GC_TTBUSER "ttb/user" +#define GC_TTBPASS "ttb/pass" #define GC_WIURL "wi/url" #define GC_WIUSER "wi/user" #define GC_WIKEY "wi/key" diff --git a/src/TtbDialog.cpp b/src/TtbDialog.cpp new file mode 100644 index 000000000..aca649fd6 --- /dev/null +++ b/src/TtbDialog.cpp @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2012 Rainer Clasen + * + * 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 "TtbDialog.h" +#include "Settings.h" +#include +#include +#include +#include +#include +#include +#include "TimeUtils.h" +#include "Units.h" + +#include + +// acccess to metrics +#include "TcxRideFile.h" + +/* + * - reuse single QNetworkAccessManager for all requests to allow + * connection re-use. + * + * - replies must be handled differently - based on request, but there's + * just a single mgr.finish() handler. + * + * - can't connect() to QNetworkReply::finish, as there's a + * slight chance for a race condition: request finished between call to + * mgr.post() and connect(). Therefore have to connect(QNetworkAccessManager::finish) + * + * - can't subclass QNetworkRequest and add attribute, as + * QNetworkAccessManager only takes a reference and clones a plain + * QNetworkRequest from it. + * + * - so... hack around this with currentRequest + dispatchRequest TODO: fix + */ + +const QString TTB_URL( "http://trainingstagebuch.org" ); + +class TtbParser : public QXmlDefaultHandler +{ +public: + friend class TtbDialog; + + bool startElement( const QString&, const QString&, const QString&, + const QXmlAttributes& ) + { + cdata = ""; + return true; + }; + + bool endElement( const QString&, const QString&, const QString& qName ) + { + if( qName == "error" ){ + error = cdata; + return true; + } + return true; + } + + bool characters( const QString& str) + { + cdata += str; + return true; + }; + +protected: + QString cdata; + QString error; +}; + + +TtbDialog::TtbDialog(MainWindow *mainWindow, RideItem *item) : + mainWindow(mainWindow), + ride( item ), + proMember( false ) +{ + assert(mainWindow); + assert(ride); + + exerciseId = ride->ride()->getTag("TtbExercise", ""); + + setWindowTitle("Trainingstagebuch"); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + QVBoxLayout *progressLayout = new QVBoxLayout; + progressLabel = new QLabel("", this); + progressBar = new QProgressBar(this); + progressLayout->addWidget(progressBar); + progressLayout->addWidget(progressLabel); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + + closeButton = new QPushButton(tr("&Cancel"), this); + buttonLayout->addStretch(); + buttonLayout->addWidget(closeButton); + + mainLayout->addLayout(progressLayout); + mainLayout->addLayout(buttonLayout); + + connect(closeButton, SIGNAL(clicked()), this, SLOT(closeClicked())); + connect(&networkMgr, SIGNAL(finished(QNetworkReply*)), this, + SLOT(dispatchReply(QNetworkReply*))); + + show(); + + uploadToTtb(); +} + +void +TtbDialog::uploadToTtb() +{ + // TODO: check for user/pw + + // TODO: ask for overwrite + + progressBar->setValue(0); + requestSettings(); +} + +void +TtbDialog::closeClicked() +{ + reject(); +} + +void +TtbDialog::requestSettings() +{ + progressLabel->setText(tr("getting Settings...")); + + currentRequest = reqSettings; + + QString username = appsettings->cvalue(mainWindow->cyclist, GC_TTBUSER).toString(); + QString password = appsettings->cvalue(mainWindow->cyclist, GC_TTBPASS).toString(); + + QUrl url( TTB_URL + "/settings/list" ); + url.addQueryItem( "view", "xml" ); + url.addQueryItem( "user", username ); + url.addQueryItem( "pass", password ); + + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader( "Accept-Encoding", "identity" ); + request.setRawHeader( "Accept", "application/xml" ); + request.setRawHeader( "Accept-Charset", "utf-8" ); + + networkMgr.get( request ); + progressBar->setValue(2); +} + +void +TtbDialog::requestSession() +{ + progressLabel->setText(tr("getting new Session...")); + + currentRequest = reqSession; + + QString username = appsettings->cvalue(mainWindow->cyclist, GC_TTBUSER).toString(); + QString password = appsettings->cvalue(mainWindow->cyclist, GC_TTBPASS).toString(); + + QUrl url( TTB_URL + "/login/sso" ); + url.addQueryItem( "view", "xml" ); + url.addQueryItem( "user", username ); + url.addQueryItem( "pass", password ); + + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader( "Accept-Encoding", "identity" ); + request.setRawHeader( "Accept", "application/xml" ); + request.setRawHeader( "Accept-Charset", "utf-8" ); + + networkMgr.get( request ); + progressBar->setValue(6); +} + +void +TtbDialog::requestUpload() +{ + assert(sessionId.length() > 0 ); + + progressLabel->setText(tr("preparing upload ...")); + + QHttpMultiPart *body = new QHttpMultiPart( QHttpMultiPart::FormDataType ); + + QHttpPart textPart; + textPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"upload_submit\"")); + textPart.setBody("hrm"); + body->append( textPart ); + + + QString fname = mainWindow->home.absoluteFilePath(".ttbupload.tcx" ); + QFile *uploadFile = new QFile( fname ); + uploadFile->setParent(body); + + TcxFileReader reader; + reader.writeRideFile( mainWindow, ride->ride(), *uploadFile ); + progressBar->setValue(12); + + int limit = proMember + ? 8 * 1024 * 1024 + : 4 * 1024 * 1024; + if( uploadFile->size() >= limit ){ + progressLabel->setText(tr("temporary file too large for upload: %1 > %1 bytes") + .arg(uploadFile->size()) + .arg(limit) ); + closeButton->setText(tr("&Close")); + return; + } + + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, + QVariant("application/occtet-stream")); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"file\"; filename=\"gc-upload-ttb.tcx\"")); + uploadFile->open(QIODevice::ReadOnly); + filePart.setBodyDevice(uploadFile); + body->append( filePart ); + + + progressLabel->setText(tr("uploading ...")); + + currentRequest = reqUpload; + + QUrl url( TTB_URL + "/file/upload" ); + url.addQueryItem( "view", "xml" ); + url.addQueryItem( "sso", sessionId ); + + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader( "Accept-Encoding", "identity" ); + request.setRawHeader( "Accept", "application/xml" ); + request.setRawHeader( "Accept-Charset", "utf-8" ); + + QNetworkReply *reply = networkMgr.post( request, body ); + body->setParent( reply ); + + connect(reply, SIGNAL(uploadProgress(qint64,qint64)), this, + SLOT(uploadProgress(qint64,qint64))); +} + +void +TtbDialog::uploadProgress(qint64 sent, qint64 total) +{ + if( total < 0 ) + return; + + progressBar->setValue( 12.0 + sent / total * 88 ); +} + +void +TtbDialog::dispatchReply( QNetworkReply *reply ) +{ + if( reply->error() != QNetworkReply::NoError ){ + progressLabel->setText( tr("request failed: ") + + reply->errorString() ); + closeButton->setText(tr("&Close")); + return; + } + + QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ); + if( ! status.isValid() || status.toInt() != 200 ){ + QVariant msg( reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ) ); + + progressLabel->setText( tr("request failed, Server response: %1 %2") + .arg(status.toInt()) + .arg(msg.toString()) ); + closeButton->setText(tr("&Close")); + + return; + } + + switch( currentRequest ){ + case reqSettings: + finishSettings( reply ); + break; + + case reqSession: + finishSession( reply ); + break; + + case reqUpload: + finishUpload( reply ); + break; + } +} + +class TtbSettingsParser : public TtbParser +{ +public: + friend class TtbDialog; + + TtbSettingsParser() : + pro(false), + reFalse("\\s*(0|false|no|)\\s*",Qt::CaseInsensitive ) + {}; + + bool endElement( const QString& a, const QString&b, const QString& qName ) + { + if( qName == "pro" ){ + pro = ! reFalse.exactMatch(cdata); + return true; + + } else if( qName == "session" ){ + session = cdata; + return true; + + } + + return TtbParser::endElement( a, b, qName ); + }; + +protected: + bool pro; + QString session; + + QRegExp reFalse; +}; + +void +TtbDialog::finishSettings(QNetworkReply *reply) +{ + progressBar->setValue(4); + + TtbSettingsParser handler; + QXmlInputSource source( reply ); + + QXmlSimpleReader reader; + reader.setContentHandler( &handler ); + + if( ! reader.parse( source ) ){ + progressLabel->setText(tr("failed to parse Settings response: ") + +handler.errorString()); + closeButton->setText(tr("&Close")); + return; + } + + if( handler.error.length() > 0 ){ + progressLabel->setText(tr("failed to get settings: ") + +handler.error ); + closeButton->setText(tr("&Close")); + return; + } + + sessionId = handler.session; + proMember = handler.pro; + + if( sessionId.length() == 0 ){ + requestSession(); + } else { + requestUpload(); + } +} + +class TtbSessionParser : public TtbParser +{ +public: + friend class TtbDialog; + + bool endElement( const QString& a, const QString&b, const QString& qName ) + { + if( qName == "session" ){ + session = cdata; + return true; + + } + + return TtbParser::endElement( a, b, qName ); + }; + +protected: + QString session; + +}; + +void +TtbDialog::finishSession(QNetworkReply *reply) +{ + progressBar->setValue(8); + + TtbSessionParser handler; + QXmlInputSource source( reply ); + + QXmlSimpleReader reader; + reader.setContentHandler( &handler ); + + if( ! reader.parse( source ) ){ + progressLabel->setText(tr("failed to parse Session response: ") + +handler.errorString()); + closeButton->setText(tr("&Close")); + return; + } + + if( handler.error.length() > 0 ){ + progressLabel->setText(tr("failed to get new session: ") + +handler.error ); + closeButton->setText(tr("&Close")); + return; + } + + sessionId = handler.session; + + if( sessionId.length() == 0 ){ + progressLabel->setText(tr("got empty session")); + closeButton->setText(tr("&Close")); + return; + } + + requestUpload(); +} + +class TtbUploadParser : public TtbParser +{ +public: + friend class TtbDialog; + + bool endElement( const QString& a, const QString&b, const QString& qName ) + { + if( qName == "id" ){ + id = cdata; + return true; + + } + + return TtbParser::endElement( a, b, qName ); + }; + +protected: + QString id; + +}; + +void +TtbDialog::finishUpload(QNetworkReply *reply) +{ + progressBar->setValue(100); + closeButton->setText(tr("&Close")); + + TtbUploadParser handler; + QXmlInputSource source( reply ); + + QXmlSimpleReader reader; + reader.setContentHandler( &handler ); + + if( ! reader.parse( source ) ){ + progressLabel->setText(tr("failed to parse upload response: ") + +handler.errorString()); + return; + } + + if( handler.error.length() > 0 ){ + progressLabel->setText(tr("failed to upload file: ") + +handler.error ); + return; + } + + exerciseId = handler.id; + + if( exerciseId.length() == 0 ){ + progressLabel->setText(tr("got empty exercise")); + return; + } + + progressLabel->setText(tr("successfully uploaded as %1") + .arg(exerciseId)); + + ride->ride()->setTag("TtbExercise", exerciseId ); + ride->setDirty(true); + + accept(); +} + + + diff --git a/src/TtbDialog.h b/src/TtbDialog.h new file mode 100644 index 000000000..62977812d --- /dev/null +++ b/src/TtbDialog.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2011 Damien.Grauser (damien.grauser@pev-geneve.ch) + * + * 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 + */ + +#ifndef TTBDIALOG_H +#define TTBDIALOG_H +#include "GoldenCheetah.h" + +#include +#include +#include "MainWindow.h" +#include "RideItem.h" + +class TtbDialog : public QDialog +{ + Q_OBJECT + G_OBJECT + +public: + TtbDialog(MainWindow *mainWindow, RideItem *item); + +signals: + +public slots: + void uploadToTtb(); + +private slots: + void uploadProgress(qint64 sent, qint64 total); + + void dispatchReply( QNetworkReply *reply ); + + void requestSettings(); + void finishSettings(QNetworkReply *reply); + + void requestSession(); + void finishSession(QNetworkReply *reply); + + void requestUpload(); + void finishUpload(QNetworkReply *reply); + + void closeClicked(); + +private: + enum requestType { + reqSettings, + reqSession, + reqUpload, + }; + + MainWindow *mainWindow; + RideItem *ride; + + QProgressBar *progressBar; + QLabel *progressLabel; + QPushButton *closeButton; + + QNetworkAccessManager networkMgr; + requestType currentRequest; + + bool proMember; + QString sessionId; + QString exerciseId; + +}; + +#endif // TTBDIALOG_H diff --git a/src/src.pro b/src/src.pro index fe4603531..9d1ed3b9d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -345,6 +345,7 @@ HEADERS += \ TrainTool.h \ TreeMapWindow.h \ TreeMapPlot.h \ + TtbDialog.h \ Units.h \ WeeklySummaryWindow.h \ WeeklyViewItemDelegate.h \ @@ -529,6 +530,7 @@ SOURCES += \ TrainTool.cpp \ TreeMapWindow.cpp \ TreeMapPlot.cpp \ + TtbDialog.cpp \ TRIMPPoints.cpp \ WattsPerKilogram.cpp \ WithingsDownload.cpp \