diff --git a/src/CalDAV.cpp b/src/CalDAV.cpp
index 32eba481c..a6c33cbbd 100644
--- a/src/CalDAV.cpp
+++ b/src/CalDAV.cpp
@@ -39,10 +39,104 @@ CalDAV::download()
if (url == "") return false; // not configured
QNetworkRequest request = QNetworkRequest(QUrl(url));
+
+ QByteArray *queryText = new QByteArray( ""
+ ""
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ "\r\n");
+
+ request.setRawHeader("Depth", "0");
+ request.setRawHeader("Content-Type", "application/xml; charset=\"utf-8\"");
+ request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());
+
+ QBuffer *query = new QBuffer(queryText);
+
mode = Events;
- QNetworkReply *reply = nam->get(request);
+ QNetworkReply *reply = nam->sendCustomRequest(request, "REPORT", query);
if (reply->error() != QNetworkReply::NoError) {
- QMessageBox::warning(main, tr("CalDAV Calendar url error"), reply->errorString());
+ QMessageBox::warning(main, tr("CalDAV REPORT url error"), reply->errorString());
+ mode = None;
+ return false;
+ }
+ return true;
+}
+
+//
+// Get OPTIONS available
+//
+bool
+CalDAV::options()
+{
+ QString url = appsettings->cvalue(main->cyclist, GC_DVURL, "").toString();
+ if (url == "") return false; // not configured
+
+ QNetworkRequest request = QNetworkRequest(QUrl(url));
+
+
+ QByteArray *queryText = new QByteArray(""
+ ""
+ " "
+ "");
+
+ request.setRawHeader("Depth", "0");
+ request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\"");
+ request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());
+
+ QBuffer *query = new QBuffer(queryText);
+
+ mode = Options;
+ QNetworkReply *reply = nam->sendCustomRequest(request, "OPTIONS", query);
+ if (reply->error() != QNetworkReply::NoError) {
+ QMessageBox::warning(main, tr("CalDAV OPTIONS url error"), reply->errorString());
+ mode = None;
+ return false;
+ }
+ return true;
+}
+
+//
+// Get URI Properties via PROPFIND
+//
+bool
+CalDAV::propfind()
+{
+ QString url = appsettings->cvalue(main->cyclist, GC_DVURL, "").toString();
+ if (url == "") return false; // not configured
+
+ QNetworkRequest request = QNetworkRequest(QUrl(url));
+
+
+ QByteArray *queryText = new QByteArray( ""
+ ""
+ " "
+ " "
+ " "
+ " "
+ " "
+ "\r\n");
+
+ request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\"");
+ request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1());
+ request.setRawHeader("Depth", "0");
+
+ QBuffer *query = new QBuffer(queryText);
+
+ mode = PropFind;
+ QNetworkReply *reply = nam->sendCustomRequest(request, "PROPFIND" , query);
+ if (reply->error() != QNetworkReply::NoError) {
+ QMessageBox::warning(main, tr("CalDAV OPTIONS url error"), reply->errorString());
mode = None;
return false;
}
@@ -90,9 +184,29 @@ CalDAV::report()
static
icalcomponent *createEvent(RideItem *rideItem)
{
+ // calendar
icalcomponent *root = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
+
+ // calendar version
+ icalproperty *version = icalproperty_new_version("2.0");
+ icalcomponent_add_property(root, version);
+
+
icalcomponent *event = icalcomponent_new(ICAL_VEVENT_COMPONENT);
+ //
+ // Unique ID
+ //
+ QString id = rideItem->ride()->id();
+ if (id == "") {
+ id = QUuid::createUuid().toString() + "@" + "goldencheetah.org";
+ rideItem->ride()->setId(id);
+ rideItem->notifyRideMetadataChanged();
+ rideItem->setDirty(true); // need to save this!
+ }
+ icalproperty *uid = icalproperty_new_uid(id.toLatin1());
+ icalcomponent_add_property(event, uid);
+
//
// START DATE
//
@@ -153,6 +267,50 @@ icalcomponent *createEvent(RideItem *rideItem)
return root;
}
+// extract entries and concatenate
+// into a single string. This is from a query response
+// where the VEVENTS are embedded within an XML document
+static QString extractComponents(QString document)
+{
+ QString returning = "";
+
+ // parse the document and extract the multistatus node (there is only one of those)
+ QDomDocument doc;
+ if (document == "" || doc.setContent(document) == false) return "";
+ QDomNode multistatus = doc.documentElement();
+ if (multistatus.isNull()) return "";
+
+ // Google Calendar retains the namespace prefix in the results
+ // Apple MobileMe doesn't. This means the element names will
+ // possibly need a prefix...
+ QString Dprefix = "";
+ QString Cprefix = "";
+ if (multistatus.nodeName().startsWith("D:")) {
+ Dprefix = "D:";
+ Cprefix = "C:";
+ }
+
+ // read all the responses within the multistatus
+ for (QDomNode response = multistatus.firstChildElement(Dprefix + "response");
+ response.nodeName() == (Dprefix + "response"); response = response.nextSiblingElement(Dprefix + "response")) {
+
+ // skate over the nest of crap to get at the calendar-data
+ QDomNode propstat = response.firstChildElement(Dprefix + "propstat");
+ QDomNode prop = propstat.firstChildElement(Dprefix + "prop");
+ QDomNode calendardata = prop.firstChildElement(Cprefix + "calendar-data");
+
+ // extract the calendar entry - top and tail the other crap
+ QString text = calendardata.toElement().text();
+ int start = text.indexOf("BEGIN:VEVENT");
+ int stop = text.indexOf("END:VEVENT");
+
+ if (start == -1 || stop == -1) continue;
+
+ returning += text.mid(start, stop-start+10) + "\n";
+ }
+ return returning;
+}
+
//
// PUT a ride item
//
@@ -196,13 +354,17 @@ void
CalDAV::requestReply(QNetworkReply *reply)
{
QString response = reply->readAll();
+
switch (mode) {
case Report:
case Events:
- main->rideCalendar->refreshRemote(response);
+ main->rideCalendar->refreshRemote(extractComponents(response));
break;
default:
+ case Options:
+ case PropFind:
case Put:
+ //nothing at the moment XXX FIXME need some diags / error checking
break;
}
mode = None;
diff --git a/src/CalDAV.h b/src/CalDAV.h
index ed06339cb..8520520de 100644
--- a/src/CalDAV.h
+++ b/src/CalDAV.h
@@ -46,12 +46,15 @@
#include "RideFile.h"
#include "JsonRideFile.h"
+// create a UUID
+#include
+
class CalDAV : public QObject
{
Q_OBJECT
G_OBJECT
- enum action { Put, Get, Events, Report, None };
+ enum action { Options, PropFind, Put, Get, Events, Report, None };
typedef enum action ActionType;
public:
@@ -60,6 +63,12 @@ public:
public slots:
+ // Query CalDAV server Options
+ bool options();
+
+ // Query CalDAV server Options
+ bool propfind();
+
// authentication (and refresh all events)
bool download();
diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp
index 964610a53..98f5188e1 100644
--- a/src/DBAccess.cpp
+++ b/src/DBAccess.cpp
@@ -40,7 +40,7 @@
// DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!!
// Schema version will change if a) the default metadata.xml is updated
// or b) new metrics are added / old changed
-static int DBSchemaVersion = 23;
+static int DBSchemaVersion = 24;
DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home)
{
@@ -145,6 +145,7 @@ bool DBAccess::createMetricsTable()
// we need to create it!
if (rc && createTables) {
QString createMetricTable = "create table metrics (filename varchar primary key,"
+ "identifier varchar,"
"timestamp integer,"
"ride_date date,"
"fingerprint integer";
@@ -374,7 +375,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsign
}
// construct an insert statement
- QString insertStatement = "insert into metrics ( filename, timestamp, ride_date, fingerprint ";
+ QString insertStatement = "insert into metrics ( filename, identifier, timestamp, ride_date, fingerprint ";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; irideMetadata()->getFields()) {
@@ -406,6 +407,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsign
// filename, timestamp, ride date
query.addBindValue(summaryMetrics->getFileName());
+ query.addBindValue(summaryMetrics->getId());
query.addBindValue(timestamp.toTime_t());
query.addBindValue(summaryMetrics->getRideDate());
query.addBindValue((int)fingerprint);
@@ -476,7 +478,7 @@ QList DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10);
// construct the select statement
- QString selectStatement = "SELECT filename, ride_date";
+ QString selectStatement = "SELECT filename, identifier, ride_date";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i DBAccess::getAllMetricsFor(QDateTime start, QDateTime end)
// filename and date
summaryMetrics.setFileName(query.value(0).toString());
- summaryMetrics.setRideDate(query.value(1).toDateTime());
+ summaryMetrics.setId(query.value(1).toString());
+ summaryMetrics.setRideDate(query.value(2).toDateTime());
// the values
int i=0;
for (; irideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
- summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+2).toDouble());
+ summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+3).toDouble());
i++;
} else if (!sp.isMetric(field.name) && field.type < 3) {
QString underscored = field.name;
// ignore texts for now XXX todo if want metadata from Summary Metrics
- summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString());
+ summaryMetrics.setText(underscored.replace("_"," "), query.value(i+3).toString());
i++;
}
}
@@ -527,7 +530,7 @@ SummaryMetrics DBAccess::getRideMetrics(QString filename)
SummaryMetrics summaryMetrics;
// construct the select statement
- QString selectStatement = "SELECT filename";
+ QString selectStatement = "SELECT filename, identifier";
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; irideMetadata()->getFields()) {
if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) {
QString underscored = field.name;
- summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+1).toDouble());
+ summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+2).toDouble());
i++;
} else if (!sp.isMetric(field.name) && field.type < 3) {
// ignore texts for now XXX todo if want metadata from Summary Metrics
QString underscored = field.name;
- summaryMetrics.setText(underscored.replace("_"," "), query.value(i+1).toString());
+ summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString());
i++;
}
}
diff --git a/src/GcRideFile.cpp b/src/GcRideFile.cpp
index e230b84a3..8ce4dd949 100644
--- a/src/GcRideFile.cpp
+++ b/src/GcRideFile.cpp
@@ -64,6 +64,9 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const
// now set in localtime
rideFile->setStartTime(asUTC.toLocalTime());
}
+ if (key == "Identifier") {
+ rideFile->setId(value);
+ }
}
// read in metric overrides:
@@ -186,6 +189,10 @@ GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const
attributes.appendChild(attribute);
attribute.setAttribute("key", "Device type");
attribute.setAttribute("value", ride->deviceType());
+ attribute = doc.createElement("attribute");
+ attributes.appendChild(attribute);
+ attribute.setAttribute("key", "Identifier");
+ attribute.setAttribute("value", ride->id());
// write out in metric overrides:
//
diff --git a/src/JsonRideFile.l b/src/JsonRideFile.l
index 8edbb5ddd..60912e900 100644
--- a/src/JsonRideFile.l
+++ b/src/JsonRideFile.l
@@ -43,6 +43,7 @@
\"STARTTIME\" return STARTTIME;
\"RECINTSECS\" return RECINTSECS;
\"DEVICETYPE\" return DEVICETYPE;
+\"IDENTIFIER\" return IDENTIFIER;
\"OVERRIDES\" return OVERRIDES;
\"TAGS\" return TAGS;
\"INTERVALS\" return INTERVALS;
diff --git a/src/JsonRideFile.y b/src/JsonRideFile.y
index 6da304857..292b08c31 100644
--- a/src/JsonRideFile.y
+++ b/src/JsonRideFile.y
@@ -63,21 +63,22 @@ void JsonRideFileerror(const char *error) // used by parser aka yyerror()
//
// Escape special characters (JSON compliance)
-static QString protect(QString string)
+static QString protect(const QString string)
{
- string.replace("\\", "\\\\"); // backslash
- string.replace("\"", "\\\""); // quote
- string.replace("\t", "\\t"); // tab
- string.replace("\n", "\\n"); // newline
- string.replace("\r", "\\r"); // carriage-return
- string.replace("\b", "\\b"); // backspace
- string.replace("\f", "\\f"); // formfeed
- string.replace("/", "\\/"); // solidus
- return string;
+ QString s = string;
+ s.replace("\\", "\\\\"); // backslash
+ s.replace("\"", "\\\""); // quote
+ s.replace("\t", "\\t"); // tab
+ s.replace("\n", "\\n"); // newline
+ s.replace("\r", "\\r"); // carriage-return
+ s.replace("\b", "\\b"); // backspace
+ s.replace("\f", "\\f"); // formfeed
+ s.replace("/", "\\/"); // solidus
+ return s;
}
// Un-Escape special characters (JSON compliance)
-static QString unprotect(QString string)
+static QString unprotect(const QString string)
{
// this is a lexer string so it will be enclosed
// in quotes. Lets strip those first
@@ -98,7 +99,7 @@ static QString unprotect(QString string)
%}
%token STRING INTEGER FLOAT
-%token RIDE STARTTIME RECINTSECS DEVICETYPE
+%token RIDE STARTTIME RECINTSECS DEVICETYPE IDENTIFIER
%token OVERRIDES
%token TAGS INTERVALS NAME START STOP
%token SAMPLES SECS KM WATTS NM CAD KPH HR ALTITUDE LAT LON HEADWIND
@@ -124,6 +125,7 @@ rideelement_list: rideelement_list ',' rideelement
rideelement: starttime
| recordint
| devicetype
+ | identifier
| overrides
| tags
| intervals
@@ -140,6 +142,7 @@ starttime: STARTTIME ':' string {
}
recordint: RECINTSECS ':' number { JsonRide->setRecIntSecs(JsonNumber); }
devicetype: DEVICETYPE ':' string { JsonRide->setDeviceType(JsonString); }
+identifier: IDENTIFIER ':' string { JsonRide->setId(JsonString); }
/*
* Metric Overrides
@@ -279,7 +282,8 @@ JsonFileReader::writeRideFile(const RideFile *ride, QFile &file) const
// first class variables
out << "\t\t\"STARTTIME\":\"" << protect(ride->startTime().toUTC().toString(DATETIME_FORMAT)) << "\",\n";
out << "\t\t\"RECINTSECS\":" << ride->recIntSecs() << ",\n";
- out << "\t\t\"DEVICETYPE\":\"" << protect(ride->deviceType()) << "\"";
+ out << "\t\t\"DEVICETYPE\":\"" << protect(ride->deviceType()) << "\",\n";
+ out << "\t\t\"IDENTIFIER\":\"" << protect(ride->id()) << "\"";
//
// OVERRIDES
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 2340240ba..568f86e98 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -320,7 +320,7 @@ MainWindow::MainWindow(const QDir &home) :
#ifdef GC_HAVE_ICAL
rideCalendar = new ICalendar(this); // my local/remote calendar entries
davCalendar = new CalDAV(this); // remote caldav
- davCalendar->download(); // login
+ davCalendar->download(); // refresh the diary window
#endif
QTreeWidgetItem *last = NULL;
diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp
index 39b0ac788..c009bdb10 100644
--- a/src/MetricAggregator.cpp
+++ b/src/MetricAggregator.cpp
@@ -175,6 +175,7 @@ bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, u
QDateTime dateTime(date, time);
summaryMetric->setRideDate(dateTime);
+ summaryMetric->setId(ride->id());
const RideMetricFactory &factory = RideMetricFactory::instance();
QStringList metrics;
diff --git a/src/Pages.cpp b/src/Pages.cpp
index 7ee798620..d8443d429 100644
--- a/src/Pages.cpp
+++ b/src/Pages.cpp
@@ -517,7 +517,9 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
webcalURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_WEBCAL_URL, "").toString());
dvURL = new QLineEdit(this);
- dvURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_DVURL, "https://www.google.com/calendar/dav//events/").toString());
+ QString url = appsettings->cvalue(mainWindow->cyclist, GC_DVURL, "").toString();
+ url.replace("%40", "@"); // remove escape of @ character
+ dvURL->setText(url);
dvUser = new QLineEdit(this);
dvUser->setText(appsettings->cvalue(mainWindow->cyclist, GC_DVUSER, "").toString());
@@ -601,7 +603,11 @@ CredentialsPage::saveClicked()
appsettings->setCValue(mainWindow->cyclist, GC_WIUSER, wiUser->text());
appsettings->setCValue(mainWindow->cyclist, GC_WIKEY, wiPass->text());
appsettings->setCValue(mainWindow->cyclist, GC_WEBCAL_URL, webcalURL->text());
- appsettings->setCValue(mainWindow->cyclist, GC_DVURL, dvURL->text());
+
+ // escape the at character
+ QString url = dvURL->text();
+ url.replace("@", "%40");
+ appsettings->setCValue(mainWindow->cyclist, GC_DVURL, url);
appsettings->setCValue(mainWindow->cyclist, GC_DVUSER, dvUser->text());
appsettings->setCValue(mainWindow->cyclist, GC_DVPASS, dvPass->text());
saveTwitter(); // get secret key if PIN set
diff --git a/src/RideFile.h b/src/RideFile.h
index 31305a667..939a21b7f 100644
--- a/src/RideFile.h
+++ b/src/RideFile.h
@@ -122,6 +122,8 @@ class RideFile : public QObject // QObject to emit signals
void setRecIntSecs(double value) { recIntSecs_ = value; }
const QString &deviceType() const { return deviceType_; }
void setDeviceType(const QString &value) { deviceType_ = value; }
+ const QString id() const { return id_; }
+ void setId(const QString &value) { id_ = value; }
// Working with INTERVALS
const QList &intervals() const { return intervals_; }
@@ -179,6 +181,7 @@ class RideFile : public QObject // QObject to emit signals
private:
+ QString id_; // global uuid@goldencheetah.org
QDateTime startTime_; // time of day that the ride started
double recIntSecs_; // recording interval in seconds
QVector dataPoints_;
diff --git a/src/RideMetadata.cpp b/src/RideMetadata.cpp
index 3886563ce..92f393662 100644
--- a/src/RideMetadata.cpp
+++ b/src/RideMetadata.cpp
@@ -407,6 +407,9 @@ FormField::editFinished()
if (definition.name == "Device") {
ourRideItem->ride()->setDeviceType(text);
ourRideItem->notifyRideMetadataChanged();
+ } else if (definition.name == "Identifier") {
+ ourRideItem->ride()->setId(text);
+ ourRideItem->notifyRideMetadataChanged();
} else if (definition.name == "Recording Interval") {
ourRideItem->ride()->setRecIntSecs(text.toDouble());
ourRideItem->notifyRideMetadataChanged();
@@ -516,6 +519,7 @@ FormField::metadataChanged()
else if (definition.name == "Recording Interval") value = QString("%1").arg(ourRideItem->ride()->recIntSecs());
else if (definition.name == "Start Date") value = ourRideItem->ride()->startTime().date().toString("dd.MM.yyyy");
else if (definition.name == "Start Time") value = ourRideItem->ride()->startTime().time().toString("hh:mm:ss.zzz");
+ else if (definition.name == "Identifier") value = ourRideItem->ride()->id();
else {
if (sp.isMetric(definition.name)) {
// get from metric overrides
diff --git a/src/SpecialFields.cpp b/src/SpecialFields.cpp
index a8df615c5..39edf0234 100644
--- a/src/SpecialFields.cpp
+++ b/src/SpecialFields.cpp
@@ -25,6 +25,7 @@ SpecialFields::SpecialFields()
{
names_ << "Start Date" // linked to RideFile::starttime
<< "Start Time" // linked to RideFile::starttime
+ << "Identifier" // linkned to RideFile::id
<< "Workout Code" // in WKO and possibly others
<< "Sport" // in WKO and possible others
<< "Objective" // in WKO as "goal" nad possibly others
diff --git a/src/SummaryMetrics.h b/src/SummaryMetrics.h
index c239dae44..7c556a023 100644
--- a/src/SummaryMetrics.h
+++ b/src/SummaryMetrics.h
@@ -31,6 +31,10 @@ class SummaryMetrics
QString getFileName() { return fileName; }
void setFileName(QString fileName) { this->fileName = fileName; }
+ // Identifier
+ QString getId() { return id; }
+ void setId(QString id) { this->id = id; }
+
// ride date
QDateTime getRideDate() { return rideDate; }
void setRideDate(QDateTime rideDate) { this->rideDate = rideDate; }
@@ -61,6 +65,7 @@ class SummaryMetrics
private:
QString fileName;
+ QString id;
QDateTime rideDate;
QMap value;
QMap texts;