New upstream version 0.6.3 upstream/0.6.3
authorgregor herrmann <gregoa@debian.org>
Sun, 1 Oct 2017 15:22:52 +0000 (17:22 +0200)
committergregor herrmann <gregoa@debian.org>
Sun, 1 Oct 2017 15:22:52 +0000 (17:22 +0200)
27 files changed:
ChangeLog
NEWS
confclerk.pro
data/confclerk.1
data/confclerk.png
data/confclerk.pod
src/app/app.pro
src/app/application.cpp
src/global.pri
src/gui/eventdialog.cpp
src/gui/gui.pro
src/gui/mainwindow.cpp
src/gui/mainwindow.h
src/gui/searchhead.h
src/gui/urlinputdialog.cpp
src/gui/urlinputdialog.h
src/mvc/conferencemodel.h
src/mvc/delegate.cpp
src/mvc/delegate.h
src/mvc/eventmodel.cpp
src/mvc/mvc.pro
src/mvc/track.cpp
src/orm/ormrecord.h
src/sql/schedulexmlparser.cpp
src/sql/schedulexmlparser.h
src/sql/sqlengine.cpp
src/sql/sqlengine.h

index 536d70d322138c4a87108557e391f9c25a03bdef..83744ff097614a19e3158840a75766216658d34c 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,76 @@
+2017-10-01  gregor herrmann  <gregor@toastfreeware.priv.at>
+
+       update NEWS before release of version 0.6.3
+
+2017-09-27  Philipp Spitzer  <philipp@spitzer.priv.at>
+
+       Now the links in the description are clickable. Fixes #49.
+
+       The ID of an event is checked now when importing the XML file.
+
+       Now using exceptions to report errors in schedulexmlparser.cpp.
+
+       Create dedicated sub-function to parse XML data (to prepare exception error reporting).
+
+       Use TransactionRaii in schedulexmlparser.cpp.
+
+       Implement transaction RAII.
+
+       Implement rollbackTransaction().
+
+2017-09-13  Philipp Spitzer  <philipp@spitzer.priv.at>
+
+       If no day_change was given for a conference 4 AM is assumed.
+       Fixes #53.
+
+       Import schedules with dates attached to events correctly.
+
+       Avoid duplicate code when inserting/updating a conference.
+
+2017-09-13  gregor herrmann  <gregor@toastfreeware.priv.at>
+
+       URL input dialog: trim URL.
+
+2017-08-30  Philipp Spitzer  <philipp@spitzer.priv.at>
+
+       Minor typographic improvement.
+
+       "Open" button is disabled not when no URL was entered.
+
+2017-08-30  gregor herrmann  <gregor@toastfreeware.priv.at>
+
+       Mention new location of database file under Qt5 in docs.
+
+       ifdef qt4 and qt5
+
+       Merge branch 'master' into qt5
+
+2017-08-30  Philipp Spitzer  <philipp@spitzer.priv.at>
+
+       When no track is present, use the special name "No track" is used. This fixes issue #56.
+
+2017-08-30  Martín Ferrari  <tincho@tincho.org>
+
+       Fix possibility for SQL injection attack.
+
+       Demangling exception class name from error message of unknown exceptions.
+
+       Write debug message in case of silently catched exceptions.
+
+       More specific error message for "unknown" exceptions.
+
+2017-08-27  gregor herrmann  <gregor@toastfreeware.priv.at>
+
+       TrackInsertException: make error message useful.
+
+       TrackInsertException: correctly derive from OrmSqlException.
+
+       Derive OrmException from std::runtime_error.
+
 2017-01-24  gregor herrmann  <gregoa@debian.org>
 
+       bump version after release
+
        update NEWS before release.
 
 2017-01-23  Philipp Spitzer  <philipp@spitzer.priv.at>
 
        eventdialog: only convertFromPlainText description and abstract if they are not richtext.
 
+       Merge branch 'master' into qt5
+
 2015-01-20  gregor herrmann  <gregoa@debian.org>
 
        Bump copyright year.
        gitlog-to-changelog sums up the commits, whereas git2cl dumps them
        individually.
 
+2015-01-13  Philipp Spitzer  <philipp@spitzer.priv.at>
+
+       Merged changes from trunk. It still compiles successfully. :-)
+
 2014-09-11  gregor herrmann  <gregoa@debian.org>
 
        Make release target depend on distclean target to ensure we have no compiled objects or Makefiles in the release tarball.
 
 2013-09-24  Philipp Spitzer  <philipp@spitzer.priv.at>
 
+       Now the application compiles for QT5.
+       Note that the location of the database in Linux has changed from
+       ~/.local/share/data/Toastfreeware/ConfClerk
+       to
+       ~/.local/share/Toastfreeware/ConfClerk
+
        Fixed a yet unknown bug: The room name was not properly inserted in the room table.
 
 2013-09-10  Philipp Spitzer  <philipp@spitzer.priv.at>
diff --git a/NEWS b/NEWS
index 0b7c29bd1eb21164afd84c4682f5a2dc505100b4..c163847c2cfcaf8a38cb383a6ce9048895da3de2 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,27 @@
 This is the NEWS file for ConfClerk. ConfClerk is the successor of
 fosdem-schedule; cf. docs/fosdem-schedule for the historic documentation.
 
+version 0.6.3, 2017-10-01
+* Support Qt5 and Qt4.
+  (Fixes: #57)
+  Mention new location of database file under Qt5 in docs.
+* Fix display of events after midnight.
+  (Fixes: #53)
+* Fix import failures when the conference has no tracks in the XML file.
+  Thanks to Martín Ferrari for the bug report and the help with the fix.
+  (Fixes: #56)
+* Bail out on import when conference XML has empty or ill-formed
+  event IDs.
+  Thanks to Paul Wise for the bug report.
+  (Fixes: #58)
+* Make URLs in Abstract/Description of event detail view clickable.
+  (Fixes: #49)
+* Improve exception handling and error messages.
+  Thanks to Martín Ferrari for his help.
+* Improve import dialog:
+  - enable "Open" button only when a URL is entered
+  - trim URL
+
 version 0.6.2, 2017-01-24
 * Event dialog: don't unconditionally assume plain text for description and
   abstract, can be rich text as well (i.e. contain HTML tags).
index 22f8f79afc7d441ac529a28e581dd8b6468be044..8171399115a896841ef6b34dc6cb0fe3584a7008 100644 (file)
@@ -3,7 +3,7 @@
 QMAKEVERSION = $$[QMAKE_VERSION]
 ISQT4 = $$find(QMAKEVERSION, ^[2-9])
 isEmpty( ISQT4 ) {
-       error("Use the qmake include with Qt4.4 or greater, on Debian that is qmake-qt4");
+       error("Use the qmake include with Qt4.7 or greater, on Debian that is qmake-qt4");
 }
 
 TEMPLATE = subdirs
index c84e8e0c5431589e38a44ca1f57f66c9a8e0e4cc..44a63b773ca50a18f19be5bc75221f4100fad0d8 100644 (file)
@@ -1,4 +1,4 @@
-.\" Automatically generated by Pod::Man 4.07 (Pod::Simple 3.32)
+.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35)
 .\"
 .\" Standard preamble:
 .\" ========================================================================
@@ -67,7 +67,7 @@
 .\" ========================================================================
 .\"
 .IX Title "CONFCLERK 1"
-.TH CONFCLERK 1 "2017-01-22" "Version 0.6.2" "Offline conference scheduler"
+.TH CONFCLERK 1 "2017-08-30" "Version 0.6.3" "Offline conference scheduler"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -117,7 +117,8 @@ Directory specification <http://standards.freedesktop.org/basedir\-spec/basedir\
 .PP
 So the configuration (see \*(L"\s-1CONFIGURATION\*(R"\s0) is stored at
 \&\fI~/.config/Toastfreeware/ConfClerk.conf\fR and the database is kept at
-\&\fI~/.local/share/data/Toastfreeware/ConfClerk/ConfClerk.sqlite\fR.
+\&\fI~/.local/share/data/Toastfreeware/ConfClerk/ConfClerk.sqlite\fR (Qt4)
+\&\fI~/.local/share/Toastfreeware/ConfClerk/ConfClerk.sqlite\fR (Qt5).
 .SH "COPYRIGHT AND LICENSE"
 .IX Header "COPYRIGHT AND LICENSE"
 .SS "Main code"
index 25b8da15563b383e2e002b92e5c5c6f1081e6de9..41551c239e13f17de66ce145f74befdeb317b6db 100644 (file)
Binary files a/data/confclerk.png and b/data/confclerk.png differ
index 03e0a8e6882510f279cd3f589a47c4a982aac577..6c16b969a3e942f1625f2995b046668124ddefa9 100644 (file)
@@ -58,7 +58,8 @@ Directory specification L<http://standards.freedesktop.org/basedir-spec/basedir-
 
 So the configuration (see L</"CONFIGURATION">) is stored at
 F<~/.config/Toastfreeware/ConfClerk.conf> and the database is kept at
-F<~/.local/share/data/Toastfreeware/ConfClerk/ConfClerk.sqlite>.
+F<~/.local/share/data/Toastfreeware/ConfClerk/ConfClerk.sqlite> (Qt4)
+F<~/.local/share/Toastfreeware/ConfClerk/ConfClerk.sqlite> (Qt5).
 
 =head1 COPYRIGHT AND LICENSE
 
index 57cd46fc588e443378db573518f309cfdc7784e1..37f4f487137270c25e592a85d9e8118030690a99 100644 (file)
@@ -3,6 +3,7 @@ TEMPLATE = app
 TARGET = confclerk
 DESTDIR = ../bin
 QT += sql xml network
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
 CONFIG(maemo5) {
     QT += maemo5
 }
index c78ee9945647e069bb301af78deab467c4db1f62..386b681a0389d1b4d6a7e554af4eee4f903fa114 100644 (file)
  * You should have received a copy of the GNU General Public License along with
  * ConfClerk.  If not, see <http://www.gnu.org/licenses/>.
  */
+#if defined(__GNUC__) || defined(__llvm__) || defined(__clang__)
+#include <cxxabi.h>
+#endif
+#include <exception>
+
 #include "application.h"
 #include "errormessage.h"
 
@@ -32,8 +37,21 @@ bool Application::notify(QObject* receiver, QEvent* event)
     } catch (OrmException& e) {
         error_message("UNCAUGHT OrmException: " + e.text());
         return false;
+    } catch (std::exception& e) {
+        error_message("UNCAUGHT exception: " + QString(e.what()));
+        return false;
     } catch (...) {
-        error_message("UNCAUGHT EXCEPTION: unknown");
+#if defined(__GNUC__) || defined(__llvm__) || defined(__clang__)
+        int status = 0;
+        char *buff = __cxxabiv1::__cxa_demangle(
+                __cxxabiv1::__cxa_current_exception_type()->name(),
+                NULL, NULL, &status);
+        QString exception_name = QString(buff);
+        std::free(buff);
+#else
+        QString exception_name = QString("unknown");
+#endif
+        error_message("UNCAUGHT exception: " + exception_name);
         return false;
     }
 }
index 86126e6c290eab193fae09f46ce995d52e1cc312..fc4a0e6561e0f7f30e9f5d629bc3bb1ece8cd50d 100644 (file)
@@ -4,7 +4,7 @@
 # USAGE: include(./global.pri)
 
 # VERSION
-VERSION = 0.6.2
+VERSION = 0.6.3
 DEFINES += VERSION=\\\"$$VERSION\\\"
 
 # Define 'MAEMO' specific CONFIG/DEFINE
index 458a7018bfa79e53a7d9cffd793239a1d687f6c8..6f3014e63a5bdf210a550474f3549fa694c1e559 100644 (file)
 #include "appsettings.h"
 #endif
 
+QString toHtmlEscaped(const QString& string) {
+#if QT_VERSION >= 0x050000
+    return string.toHtmlEscaped();
+#else
+    return Qt::escape(string);
+#endif
+}
+
 EventDialog::EventDialog(int conferenceId, int eventId, QWidget *parent): QDialog(parent), mConferenceId(conferenceId), mEventId(eventId) {
     setupUi(this);
 
@@ -38,12 +46,12 @@ EventDialog::EventDialog(int conferenceId, int eventId, QWidget *parent): QDialo
 
     QString info;
     // title
-    info.append(QString("<h1>%1</h1>\n").arg(Qt::escape(event.title())));
+    info.append(QString("<h1>%1</h1>\n").arg(toHtmlEscaped(event.title())));
 
     // persons
     info += QString("<h2>%1</h2>\n").arg(tr("Persons"));
     QStringList persons = event.persons();
-    for (int i = 0; i != persons.size(); ++i) persons[i] = Qt::escape(persons[i]);
+    for (int i = 0; i != persons.size(); ++i) persons[i] = toHtmlEscaped(persons[i]);
     info += QString("<p>%1</p>\n").arg(persons.join(", "));
 
     // abstract
@@ -59,7 +67,10 @@ EventDialog::EventDialog(int conferenceId, int eventId, QWidget *parent): QDialo
     if (Qt::mightBeRichText(event.description())) {
         info += event.description();
     } else {
-        info += Qt::convertFromPlainText(event.description(), Qt::WhiteSpaceNormal);
+        QString description = Qt::convertFromPlainText(event.description(), Qt::WhiteSpaceNormal);
+        // make links clickable
+        QRegExp rx("<?(((s?ftp|https?|svn|svn\\+ssh|git|git\\+ssh)://|(file|news):|www\\.)[-a-z0-9_.:%]*[a-z0-9](/[^][{}\\s\"<>()]*[^][{}\\s\"<>().,:!])?/?)>?");
+        info += description.replace(rx, "<a href=\"\\1\">\\1</a>");
     }
 
     // links
@@ -71,7 +82,7 @@ EventDialog::EventDialog(int conferenceId, int eventId, QWidget *parent): QDialo
         QString name(i.key());
         if (url.isEmpty() || url == "http://") continue;
         if (name.isEmpty()) name = url;
-        info += QString("<li><a href=\"%1\">%2</a></li>\n").arg(Qt::escape(url), Qt::escape(name));
+        info += QString("<li><a href=\"%1\">%2</a></li>\n").arg(toHtmlEscaped(url), toHtmlEscaped(name));
     }
     info += QString("</ul>\n");
     eventInfoTextBrowser->setHtml(info);
index f9f7028999d64ac2293e3622e97c50f554c754df..cac0bc20b8e8493f20a64940b262ff1bccf123df 100644 (file)
@@ -6,6 +6,7 @@ CONFIG += static
 QT += sql \
     xml \
     network
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
 QMAKE_CLEAN += ../bin/libgui.a
 
 # module dependencies
@@ -86,3 +87,6 @@ CONFIG(maemo5) {
     QT += maemo5
 }
 
+OTHER_FILES += \
+    test.qml
+
index 98a45c8e47a39b352ef554434cd1a502a531dc76..3b2582a213eab1f72b0a87d5ac4e5c4283005957 100644 (file)
@@ -310,7 +310,8 @@ void MainWindow::useConference(int conferenceId)
 
         // end of optimization
         // initTabs();
-    } catch (OrmException& e) {
+    } catch (const OrmException& e) {
+        qDebug() << "OrmException:" << e.text();
         // cannon set an active conference
         unsetConference();   // TODO: as no active conference is now correctly managed this should be handled as a fatal error
         return;
@@ -405,7 +406,8 @@ void MainWindow::on_conferencesAction_triggered()
     // optimization, see useConference() code
     try {
         initTabs();
-    } catch (OrmException) {
+    } catch (const OrmException& e) {
+        qDebug() << "OrmException:" << e.text();
         clearTabs();
     }
 }
index 537d81e793f9cd3961ea1b25c858154951d44122..91b3c114e5b1490474e8a08a0ae43853205c6f7d 100644 (file)
 #ifndef MAINWINDOW_H
 #define MAINWINDOW_H
 
+#include "qglobal.h"
+#if QT_VERSION >= 0x050000
+#include <QtWidgets>
+#else
 #include <QtGui/QMainWindow>
+#endif
 #include <QSslError>
 
 #include "ui_mainwindow.h"
index ac212bc3d233d573377f8e9abe770bbe77e0317d..39303f00d77064a7be7093342ea23d43b2c1e4c3 100644 (file)
 #ifndef SEARCHHEAD_H
 #define SEARCHHEAD_H
 
+#include "qglobal.h"
+#if QT_VERSION >= 0x050000
+#include <QtWidgets>
+#else
 #include <QtGui/QWidget>
+#endif
 #include <QDebug>
 #include "ui_searchhead.h"
 
index 20f138e2b5c0ae7b36c60c3ffcc610be8c2240a2..23a36b988c90d08ee3dc8d01162cc124d7118a94 100644 (file)
@@ -27,11 +27,13 @@ UrlInputDialog::UrlInputDialog(QWidget* parent)
 {
     setupUi(this);
 
-    QPushButton* openFile = buttons->addButton("Open File...", QDialogButtonBox::ActionRole);
+    QPushButton* openFile = buttons->addButton(tr("Open file ..."), QDialogButtonBox::ActionRole);
+    textChanged(urlEntry->text());
 
     connect(openFile, SIGNAL(clicked()), SLOT(openFileClicked()));
     connect(buttons, SIGNAL(accepted()), SLOT(acceptClicked()));
     connect(buttons, SIGNAL(rejected()), SLOT(rejectClicked()));
+    connect(urlEntry, SIGNAL(textChanged(QString)), SLOT(textChanged(QString)));
 }
 
 void UrlInputDialog::openFileClicked()
@@ -46,9 +48,14 @@ void UrlInputDialog::openFileClicked()
     }
 }
 
+void UrlInputDialog::textChanged(const QString& text)
+{
+    buttons->button(QDialogButtonBox::StandardButton::Open)->setEnabled(!text.isEmpty());
+}
+
 void UrlInputDialog::acceptClicked()
 {
-    saved_result = urlEntry->text();
+    saved_result = urlEntry->text().trimmed();
     setResult(HaveUrl);
 }
 
index c754e746b7546d01d1c320af306afb76f4a691e8..9b4b9885c2be6d9c4c237c8f3330c50f43431ef2 100644 (file)
@@ -38,6 +38,7 @@ private slots:
     void acceptClicked();
     void rejectClicked();
     void openFileClicked();
+    void textChanged(const QString& text);
 private:
     QString saved_result;
 };
index db646fa72d2e8de76abf31a202dec8bf78135906..b7297ba2c83218a2f2447df376f9596ce572d2cc 100644 (file)
@@ -52,8 +52,9 @@ private:
     // reinitialize list from database
     void reinit()
     {
+        beginResetModel();
         conferences = Conference::getAll();
-        reset();
+        endResetModel();
     }
 
     QList<Conference> conferences;
index ebf421111c32e4dbe010cb54e90c1d3c9be5672b..2d1f08524084873ea61ee03ad94879a50a826960 100644 (file)
@@ -237,7 +237,7 @@ void Delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, cons
                 option.rect.x()+SPACER,
                 option.rect.y()+option.rect.height()-fmBig.descent());
         painter->setFont(fontBig);
-        painter->drawText(titlePointF,qVariantValue<QString>(index.data()));
+        painter->drawText(titlePointF,index.data().value<QString>());
     }
 
     painter->restore();
index 21d2839c9968382380718bb0cdb0814631930df5..2ee765f88a077119e6aa5a38e3c93be032f76c07 100644 (file)
 #ifndef DELEGATE_H
 #define DELEGATE_H
 
+#include "qglobal.h"
+#if QT_VERSION >= 0x050000
+#include <QtWidgets>
+#else
 #include <QtGui>
+#endif
 
 class Delegate : public QItemDelegate
 {
index 39c9d9bafdb0bc99e9e8f855d56a2b64224e2bd5..ff3d0ebede1201bb1ad6bc6e7a8e3d20e6a0bebf 100644 (file)
@@ -44,6 +44,8 @@ void EventModel::Group::setTitle(const QList<Event>& mEvents) {
 // multiple of one hour.
 void EventModel::createTimeGroups()
 {
+    beginResetModel();
+
     mGroups.clear();
     mParents.clear();
     if (mEvents.empty()) return;
@@ -90,7 +92,7 @@ void EventModel::createTimeGroups()
     // the last group needs a title as well
     mGroups.last().setTitle(mEvents);
 
-    reset();
+    endResetModel();
 }
 
 void EventModel::createTrackGroups() {
@@ -174,7 +176,7 @@ QModelIndex EventModel::index(int row, int column, const QModelIndex& parent) co
 
     if (!parent.isValid())
     {
-        return createIndex(row, column, 0);
+        return createIndex(row, column);
     }
     else if (parent.internalId() == 0)
     {
@@ -199,7 +201,7 @@ QModelIndex EventModel::parent(const QModelIndex & index) const
 
         Event * event = static_cast<Event*>(index.internalPointer());
 
-        return createIndex(mParents[event->id()], 0, 0);
+        return createIndex(mParents[event->id()], 0);
     }
 
     return QModelIndex();
@@ -228,11 +230,11 @@ int EventModel::rowCount (const QModelIndex & parent) const
 
 void EventModel::clearModel()
 {
+    beginResetModel();
     mGroups.clear();
     mEvents.clear();
     mParents.clear();
-
-    reset();
+    endResetModel();
 }
 
 
index f0d92d751ca7568e791569edede0ad7ecf2aee9b..966d3150be155219cee2eeec33606ca565f1786f 100644 (file)
@@ -3,7 +3,8 @@ TEMPLATE = lib
 TARGET = mvc
 DESTDIR = ../bin
 CONFIG += static
-QT += sql 
+QT += sql
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
 QMAKE_CLEAN += ../bin/libmvc.a
 
 # module dependencies
index d8c7a126d9176eb9c195e210162ebd12e4cdbf00..139b1990798034fa7de249e66c429dab04df4212 100644 (file)
@@ -30,7 +30,7 @@ QSqlRecord const Track::sColumns = Track::toRecord(QList<QSqlField>()
     << QSqlField(CONFERENCEID, QVariant::Int)
     << QSqlField(NAME, QVariant::String));
 
-class TrackInsertException : OrmSqlException
+class TrackInsertException : public OrmSqlException
 {
 public:
     TrackInsertException(const QString& text) : OrmSqlException(text) {}
@@ -39,10 +39,17 @@ public:
 int Track::insert()
 {
     QSqlQuery query;
-    query.prepare("INSERT INTO " + sTableName + " (" + CONFERENCEID + "," + NAME + ")" + " VALUES " + "(\"" + QString::number(conferenceid()) + "\",\"" + name() + "\")");
+    QString trackname = name();
+    query.prepare(
+            QString("INSERT INTO %1 (%2, %3) VALUES (:xid_conference, :name)")
+            .arg(sTableName, CONFERENCEID, NAME));
+    query.bindValue(":xid_conference", conferenceid());
+    query.bindValue(":name", trackname);
     if (!query.exec())
     {
-        throw TrackInsertException("Exec Error");
+        throw TrackInsertException(
+                "Inserting track '" + trackname + "' into database failed: " +
+                query.lastError().text());
     }
     QVariant variant = query.lastInsertId();
     if (variant.isValid())
index 0dec7aad724aae9a0c25bb0ab5960057a14a0929..30f569a947d2afe0fd905f51b36d44d08a14764d 100644 (file)
 #include <QStringList>
 #include <QDateTime>
 #include <QDebug>
+#include <stdexcept>
 
-class OrmException
+class OrmException : public std::runtime_error
 {
 public:
-    OrmException(const QString& text) : mText(text) {}
+    OrmException(const QString& text) : std::runtime_error(text.toStdString()), mText(text) {}
     virtual ~OrmException(){}
     virtual const QString& text() const { return mText; }
 private:
@@ -41,7 +42,7 @@ private:
 class OrmNoObjectException : public OrmException
 {
 public:
-    OrmNoObjectException() : OrmException("No object exception"){}
+    OrmNoObjectException() : OrmException("SQL query expects one record but found none."){}
     ~OrmNoObjectException(){}
 };
 
index 90c7714bb490ec16ca89dd031fd578b9fad79309..bba8626c53313b98963cd1782a6c66c2869b1cf2 100644 (file)
@@ -20,6 +20,7 @@
 
 #include <QDomDocument>
 #include <QHash>
+#include <QTime>
 
 #include "schedulexmlparser.h"
 #include "sqlengine.h"
@@ -31,25 +32,39 @@ ScheduleXmlParser::ScheduleXmlParser(SqlEngine* sqlEngine, QObject *aParent): QO
 }
 
 
-void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, int conferenceId)
-{
+class ParseException: public std::runtime_error {
+public:
+    ParseException(const QString& message): std::runtime_error(message.toStdString()) {}
+};
+
+
+void checkEvent(QHash<QString,QString>& event) {
+    QString event_id = event["id"];
+    if (event_id.trimmed().isEmpty()) throw ParseException(QObject::tr("The ID of event '%1' is missing.").arg(event["title"]));
+    bool ok;
+    event_id.toInt(&ok);
+    if (!ok) throw ParseException(QObject::tr("The ID '%2' of event '%1' is not numeric.").arg(event["title"]).arg(event_id));
+}
+
+
+void ScheduleXmlParser::parseDataImpl(const QByteArray &aData, const QString& url, int conferenceId) {
     QDomDocument document;
     QString xml_error;
     int xml_error_line;
     int xml_error_column;
-    if (!document.setContent (aData, false, &xml_error, &xml_error_line, &xml_error_column)) {
-        error_message("Could not parse schedule: " + xml_error + " at line " + QString("%1").arg(xml_error_line) + " column " + QString("%1").arg(xml_error_column));
-        return;
+    if (!document.setContent(aData, false, &xml_error, &xml_error_line, &xml_error_column)) {
+        throw ParseException("Could not parse schedule: " + xml_error + " at line " + QString("%1").arg(xml_error_line) + " column " + QString("%1").arg(xml_error_column));
     }
 
     QDomElement scheduleElement = document.firstChildElement("schedule");
 
-    sqlEngine->beginTransaction();
+    TransactionRaii transaction(*sqlEngine); // begins the transaction
 
     QString conference_title;
     if (!scheduleElement.isNull())
     {
         QDomElement conferenceElement = scheduleElement.firstChildElement("conference");
+        QTime conference_day_change;
         if (!conferenceElement.isNull())
         {
             emit(parsingScheduleBegin());
@@ -61,12 +76,15 @@ void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, i
             conference["city"] = conferenceElement.firstChildElement("city").text();
             conference["start"] = conferenceElement.firstChildElement("start").text(); // date
             conference["end"] = conferenceElement.firstChildElement("end").text(); // date
-            conference["day_change"] = conferenceElement.firstChildElement("day_change").text(); // time
+            QString conferenceDayChangeStr = conferenceElement.firstChildElement("day_change").text(); // time, e.g. "04:00:00"
+            if (conferenceDayChangeStr.isEmpty()) conferenceDayChangeStr = "04:00:00";
+            conference["day_change"] = conferenceDayChangeStr;
             conference["timeslot_duration"] = conferenceElement.firstChildElement("timeslot_duration").text(); // time
             conference["url"] = url;
             sqlEngine->addConferenceToDB(conference, conferenceId);
             conferenceId = conference["id"].toInt();
             conference_title = conference["title"];
+            conference_day_change = QTime(0, 0).addSecs(conference["day_change"].toInt());
         }
 
         // we need to get count of all events in order to emit 'progressStatus' signal
@@ -106,10 +124,20 @@ void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, i
 
                         // process event's nodes
                         QHash<QString,QString> event;
-                        event["id"] = eventElement.attribute("id");;
+                        event["id"] = eventElement.attribute("id");
                         event["conference_id"] = QString::number(conferenceId, 10);
-                        event["start"] = eventElement.firstChildElement("start").text(); // time eg. 10:00
-                        event["date"] = dayElement.attribute("date"); // date eg. 2009-02-07
+                        QTime event_start = QTime::fromString(eventElement.firstChildElement("start").text(), sqlEngine->TIME_FORMAT);
+                        event["start"] = event_start.toString(sqlEngine->TIME_FORMAT); // time eg. 10:00
+                        QDate event_date;
+                        QDomElement eventDateElement = eventElement.firstChildElement("date");
+                        if (!eventDateElement.isNull()) {
+                            QString date_str = eventDateElement.text(); // date eg. 2009-02-07T10:00:00+00:00
+                            event_date = QDate::fromString(date_str.left(sqlEngine->DATE_FORMAT.size()), sqlEngine->DATE_FORMAT);
+                        } else {
+                            event_date = QDate::fromString(dayElement.attribute("date"),sqlEngine->DATE_FORMAT); // date eg. 2009-02-07
+                            if (event_start < conference_day_change) event_date = event_date.addDays(1);
+                        }
+                        event["date"] = event_date.toString(sqlEngine->DATE_FORMAT); // date eg. 2009-02-07
                         event["duration"] = eventElement.firstChildElement("duration").text(); // time eg. 00:30
                         event["room_name"] = eventElement.firstChildElement("room").text(); // string eg. "Janson"
                         event["tag"] = eventElement.firstChildElement("tag").text(); // string eg. "welcome"
@@ -120,6 +148,7 @@ void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, i
                         event["language"] = eventElement.firstChildElement("language").text(); // language eg. "English"
                         event["abstract"] = eventElement.firstChildElement("abstract").text(); // string
                         event["description"] = eventElement.firstChildElement("description").text(); // string
+                        checkEvent(event);
                         sqlEngine->addEventToDB(event);
                         // process persons' nodes
                         QDomElement personsElement = eventElement.firstChildElement("persons");
@@ -151,11 +180,18 @@ void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, i
             } // parsing room elements
         } // parsing day elements
     } // schedule element
-    sqlEngine->commitTransaction();
-    if (!conference_title.isNull()) {
-        emit parsingScheduleEnd(conferenceId);
-    } else {
-        error_message("Could not parse schedule");
+    if (conference_title.isNull()) throw ParseException("Could not parse schedule");
+
+    transaction.commit();
+    emit parsingScheduleEnd(conferenceId);
+}
+
+
+void ScheduleXmlParser::parseData(const QByteArray &aData, const QString& url, int conferenceId) {
+    try {
+        parseDataImpl(aData, url, conferenceId);
+    } catch (ParseException& e) {
+        error_message(e.what());
     }
 }
 
index 5d1442e9bac0d69211546b2bd72e3ccda6ced8e2..5ff33682c01383a0ea56b2079889d131d5b4faba 100644 (file)
@@ -28,6 +28,7 @@ class ScheduleXmlParser : public QObject
     Q_OBJECT
     private:
         SqlEngine* sqlEngine;
+        void parseDataImpl(const QByteArray &aData, const QString& url, int conferenceId);
     public:
         ScheduleXmlParser(SqlEngine* sqlEngine, QObject *aParent = NULL);
 
index c959e8d1551cc52d6a5388a90c1e3c2fad068b83..5a5c09bfd4b91265ff73e06b102c34e694964b74 100644 (file)
 #include <QSqlRecord>
 #include <QVariant>
 #include <QDateTime>
+#include "qglobal.h"
+#if QT_VERSION >= 0x050000
+#include <QStandardPaths>
+#else
+#include <QDesktopServices>
+#endif
 
 #include <QDir>
-#include <QDesktopServices>
 #include "sqlengine.h"
 #include "track.h"
 #include "conference.h"
 
 #include <QDebug>
 
-const QString DATE_FORMAT ("yyyy-MM-dd");
-const QString TIME_FORMAT ("hh:mm");
-
 SqlEngine::SqlEngine(QObject *aParent): QObject(aParent) {
+#if QT_VERSION >= 0x050000
+    QDir dbPath(QStandardPaths::writableLocation(QStandardPaths::DataLocation));
+#else
     QDir dbPath(QDesktopServices::storageLocation(QDesktopServices::DataLocation));
+#endif
     dbFilename = dbPath.absoluteFilePath("ConfClerk.sqlite");
 }
 
@@ -132,40 +138,32 @@ bool SqlEngine::applySqlFile(const QString sqlFile) {
 
 void SqlEngine::addConferenceToDB(QHash<QString,QString> &aConference, int conferenceId) {
     QSqlQuery query(db);
-    if (conferenceId <= 0) // insert conference
-    {
+    bool insert = conferenceId <= 0;
+    if (insert) { // insert conference
         query.prepare("INSERT INTO CONFERENCE (title,url,subtitle,venue,city,start,end,"
                                                 "day_change,timeslot_duration,active) "
                         " VALUES (:title,:url,:subtitle,:venue,:city,:start,:end,"
                                                 ":day_change,:timeslot_duration,:active)");
-        foreach (QString prop_name, (QList<QString>() << "title" << "url" << "subtitle" << "venue" << "city")) {
-            query.bindValue(QString(":") + prop_name, aConference[prop_name]);
-        }
-        query.bindValue(":start", QDateTime(QDate::fromString(aConference["start"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
-        query.bindValue(":end", QDateTime(QDate::fromString(aConference["end"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
-        query.bindValue(":day_change", -QTime::fromString(aConference["day_change"],TIME_FORMAT).secsTo(QTime(0,0)));
-        query.bindValue(":timeslot_duration", -QTime::fromString(aConference["timeslot_duration"],TIME_FORMAT).secsTo(QTime(0,0)));
-        query.bindValue(":active", 1);
-        query.exec();
-        emitSqlQueryError(query);
-        aConference["id"] = query.lastInsertId().toString(); // 'id' is assigned automatically
-    }
-    else // update conference
-    {
+    } else { // update conference
         query.prepare("UPDATE CONFERENCE set title=:title, url=:url, subtitle=:subtitle, venue=:venue, city=:city, start=:start, end=:end,"
                                             "day_change=:day_change, timeslot_duration=:timeslot_duration, active=:active "
                       "WHERE id=:id");
-        foreach (QString prop_name, (QList<QString>() << "title" << "url" << "subtitle" << "venue" << "city")) {
-            query.bindValue(QString(":") + prop_name, aConference[prop_name]);
-        }
-        query.bindValue(":start", QDateTime(QDate::fromString(aConference["start"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
-        query.bindValue(":end", QDateTime(QDate::fromString(aConference["end"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
-        query.bindValue(":day_change", -QTime::fromString(aConference["day_change"],TIME_FORMAT).secsTo(QTime(0,0)));
-        query.bindValue(":timeslot_duration", -QTime::fromString(aConference["timeslot_duration"],TIME_FORMAT).secsTo(QTime(0,0)));
-        query.bindValue(":active", 1);
-        query.bindValue(":id", conferenceId);
-        query.exec();
-        emitSqlQueryError(query);
+    }
+    foreach (QString prop_name, (QList<QString>() << "title" << "url" << "subtitle" << "venue" << "city")) {
+        query.bindValue(QString(":") + prop_name, aConference[prop_name]);
+    }
+    query.bindValue(":start", QDateTime(QDate::fromString(aConference["start"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
+    query.bindValue(":end", QDateTime(QDate::fromString(aConference["end"],DATE_FORMAT),QTime(0,0),Qt::UTC).toTime_t());
+    QTime dayChange = QTime::fromString(aConference["day_change"].left(TIME_FORMAT.size()), TIME_FORMAT);
+    query.bindValue(":day_change", QTime(0, 0).secsTo(dayChange));
+    query.bindValue(":timeslot_duration", -QTime::fromString(aConference["timeslot_duration"],TIME_FORMAT).secsTo(QTime(0,0)));
+    query.bindValue(":active", 1);
+    if (!insert) query.bindValue(":id", conferenceId);
+    query.exec();
+    emitSqlQueryError(query);
+    if (insert) {
+        aConference["id"] = query.lastInsertId().toString(); // 'id' is assigned automatically
+    } else {
         aConference["id"] = QVariant(conferenceId).toString();
     }
 }
@@ -179,6 +177,7 @@ void SqlEngine::addEventToDB(QHash<QString,QString> &aEvent) {
     Track track;
     int trackId;
     QString trackName = aEvent["track"];
+    if (trackName.isEmpty()) trackName = tr("No track");
     try
     {
         track = Track::retrieveByName(conferenceId, trackName);
@@ -191,8 +190,6 @@ void SqlEngine::addEventToDB(QHash<QString,QString> &aEvent) {
     }
     QDate startDate = QDate::fromString(aEvent["date"], DATE_FORMAT);
     QTime startTime = QTime::fromString(aEvent["start"], TIME_FORMAT);
-    // consider day_change (note that if day_change is e.g. at 04:00 AM, an event starting at 02:00 AM has the previous date in the XML file)
-    if (startTime < conference.dayChangeTime()) startDate = startDate.addDays(1);
     QDateTime startDateTime;
     startDateTime.setTimeSpec(Qt::UTC);
     startDateTime = QDateTime(startDate, startTime, Qt::UTC);
@@ -400,6 +397,14 @@ bool SqlEngine::commitTransaction() {
 }
 
 
+bool SqlEngine::rollbackTransaction() {
+    QSqlQuery query(db);
+    bool success = query.exec("ROLLBACK");
+    emitSqlQueryError(query);
+    return success;
+}
+
+
 bool SqlEngine::deleteConference(int id) {
     QSqlQuery query(db);
     bool success = query.exec("BEGIN IMMEDIATE TRANSACTION");
@@ -434,4 +439,3 @@ void SqlEngine::emitSqlQueryError(const QSqlQuery &query) {
     if (error.type() == QSqlError::NoError) return;
     emit dbError(error.text());
 }
-
index ae7bf06eab99106495d3217b842935ad14a14b8b..68d0679c1daa07725e8123e14f519f3d0208e123 100644 (file)
@@ -28,6 +28,9 @@
 class SqlEngine : public QObject {
     Q_OBJECT
     public:
+        const QString DATE_FORMAT = "yyyy-MM-dd";
+        const QString TIME_FORMAT = "hh:mm";
+
         QString dbFilename; ///< database filename including path
         QSqlDatabase db; ///< this may be private one day...
 
@@ -65,6 +68,7 @@ class SqlEngine : public QObject {
 
         bool beginTransaction();
         bool commitTransaction();
+        bool rollbackTransaction();
 
         /// search Events for .... returns true if success
         bool searchEvent(int conferenceId, const QHash<QString,QString> &columns, const QString &keyword);
@@ -78,5 +82,24 @@ class SqlEngine : public QObject {
         void dbError(const QString& message);
 };
 
+
+class TransactionRaii {
+    SqlEngine& sqlEngine;
+    bool committed = false;
+public:
+    TransactionRaii(SqlEngine& sqlEngine): sqlEngine(sqlEngine) {
+        sqlEngine.beginTransaction();
+    }
+
+    void commit() {
+        sqlEngine.commitTransaction();
+        committed = true;
+    }
+
+    ~TransactionRaii() {
+        if (!committed) sqlEngine.rollbackTransaction();
+    }
+};
+
 #endif /* SQLENGINE_H */