diff --git a/kmymoney/converter/webpricequote.h b/kmymoney/converter/webpricequote.h --- a/kmymoney/converter/webpricequote.h +++ b/kmymoney/converter/webpricequote.h @@ -134,6 +134,39 @@ explicit WebPriceQuote(QObject* = 0); ~WebPriceQuote(); + /** + * Hold errors reported from price quote fetching and parsing + * + * The implementation provides a type safe way to use + * bit operations like '|=' for combining values and '&' + * for checking value presence. + */ + class Errors { + public: + enum Type { + None, + Data, + Date, + DateFormat, + Price, + Script, + Source, + Symbol, + Success, + URL, + }; + + inline Errors() { } + inline Errors(Type type) { m_type.append(type); } + inline Errors(const Errors &e) { m_type = e.m_type; } + inline Errors &operator |=(Type t) { if (!m_type.contains(t)) m_type.append(t); return *this; } + inline bool operator &(Type t) const { return m_type.contains(t); } + + static Errors noError; + protected: + QList m_type; + }; + typedef enum _quoteSystemE { Native = 0, FinanceQuote @@ -150,10 +183,12 @@ * @param _source the source of the quote (must be a valid value returned * by quoteSources(). Send QString() to use the default * source. + * @param errors returns errors that occurred when fetching the quote. + * This parameter is optional * @return bool Whether the quote fetch process was launched successfully */ - bool launch(const QString& _symbol, const QString& _id, const QString& _source = QString()); + bool launch(const QString& _symbol, const QString& _id, const QString& _source = QString(), Errors &_errors = Errors::noError); /** * This returns a list of the names of the quote sources @@ -171,14 +206,14 @@ void error(const QString&); protected slots: - void slotParseQuote(const QString&); + bool slotParseQuote(const QString&, Errors &errors = Errors::noError); protected: static const QMap defaultQuoteSources(); private: - bool launchNative(const QString& _symbol, const QString& _id, const QString& _source = QString()); - bool launchFinanceQuote(const QString& _symbol, const QString& _id, const QString& _source = QString()); + bool launchNative(const QString& _symbol, const QString& _id, const QString& _source = QString(), Errors &_errors = Errors::noError); + bool launchFinanceQuote(const QString& _symbol, const QString& _id, const QString& _source = QString(), Errors &_errors = Errors::noError); void enter_loop(); static const QStringList quoteSourcesNative(); diff --git a/kmymoney/converter/webpricequote.cpp b/kmymoney/converter/webpricequote.cpp --- a/kmymoney/converter/webpricequote.cpp +++ b/kmymoney/converter/webpricequote.cpp @@ -53,6 +53,8 @@ QString WebPriceQuote::m_financeQuoteScriptPath; QStringList WebPriceQuote::m_financeQuoteSources; +WebPriceQuote::Errors WebPriceQuote::Errors::noError; + class WebPriceQuote::Private { public: @@ -88,31 +90,34 @@ delete d; } -bool WebPriceQuote::launch(const QString& _symbol, const QString& _id, const QString& _sourcename) +bool WebPriceQuote::launch(const QString& _symbol, const QString& _id, const QString& _sourcename, Errors &_errors) { if (_sourcename.contains("Finance::Quote")) - return (launchFinanceQuote(_symbol, _id, _sourcename)); + return (launchFinanceQuote(_symbol, _id, _sourcename, _errors)); else - return (launchNative(_symbol, _id, _sourcename)); + return (launchNative(_symbol, _id, _sourcename, _errors)); } -bool WebPriceQuote::launchNative(const QString& _symbol, const QString& _id, const QString& _sourcename) +bool WebPriceQuote::launchNative(const QString& _symbol, const QString& _id, const QString& _sourcename, Errors &_errors) { bool result = true; d->m_symbol = _symbol; d->m_id = _id; -// emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); + emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); // Get sources from the config file QString sourcename = _sourcename; if (sourcename.isEmpty()) sourcename = "KMyMoney Currency"; if (quoteSources().contains(sourcename)) d->m_source = WebPriceQuoteSource(sourcename); - else + else { emit error(i18n("Source %1 does not exist.", sourcename)); + _errors |= Errors::Source; + result = false; + } KUrl url; @@ -144,7 +149,8 @@ result = true; } else { emit error(i18n("Unable to launch: %1", url.toLocalFile())); - slotParseQuote(QString()); + _errors |= Errors::Script; + result = slotParseQuote(QString(), _errors); } } else { emit status(i18n("Fetching URL %1...", url.prettyUrl())); @@ -155,7 +161,6 @@ kDebug(Private::dbgArea()) << "Downloaded" << tmpFile << "from" << url; QFile f(tmpFile); if (f.open(QIODevice::ReadOnly)) { - result = true; // Find out the page encoding and convert it to unicode QByteArray page = f.readAll(); KEncodingProber prober(KEncodingProber::Universal); @@ -165,22 +170,25 @@ codec = QTextCodec::codecForLocale(); QString quote = codec->toUnicode(page); f.close(); - slotParseQuote(quote); + emit status(i18n("URL found: %1...", url.prettyUrl())); + result = slotParseQuote(quote, _errors); } else { emit error(i18n("Failed to open downloaded file")); - slotParseQuote(QString()); + _errors |= Errors::URL; + result = slotParseQuote(QString(), _errors); } KIO::NetAccess::removeTempFile(tmpFile); } else { emit error(KIO::NetAccess::lastErrorString()); - slotParseQuote(QString()); + _errors |= Errors::URL; + result = slotParseQuote(QString(), _errors); } } return result; } bool WebPriceQuote::launchFinanceQuote(const QString& _symbol, const QString& _id, - const QString& _sourcename) + const QString& _sourcename, Errors &_errors) { bool result = true; d->m_symbol = _symbol; @@ -204,21 +212,29 @@ // This seems to work best if we just block until done. if (d->m_filter.waitForFinished()) { - result = true; } else { emit error(i18n("Unable to launch: %1", m_financeQuoteScriptPath)); - slotParseQuote(QString()); + _errors |= Errors::Script; + result = slotParseQuote(QString(), _errors); } return result; } -void WebPriceQuote::slotParseQuote(const QString& _quotedata) +/** + * Parse quote data according to currently selected web price quote source + * + * @param _quotedata quote data to parse + * @return true parsing successful + * @return false parsing unsuccessful + */ +bool WebPriceQuote::slotParseQuote(const QString& _quotedata, Errors &errors) { QString quotedata = _quotedata; d->m_quoteData = quotedata; bool gotprice = false; bool gotdate = false; + bool result = true; kDebug(Private::dbgArea()) << "quotedata" << _quotedata; @@ -246,6 +262,9 @@ if (symbolRegExp.indexIn(quotedata) > -1) { kDebug(Private::dbgArea()) << "Symbol" << symbolRegExp.cap(1); emit status(i18n("Symbol found: '%1'", symbolRegExp.cap(1))); + } else { + errors |= Errors::Symbol; + emit error(i18n("Unable to parse symbol for %1", d->m_symbol)); } if (priceRegExp.indexIn(quotedata) > -1) { @@ -273,34 +292,50 @@ d->m_price = pricestr.toDouble(); kDebug(Private::dbgArea()) << "Price" << pricestr; emit status(i18n("Price found: '%1' (%2)", pricestr, d->m_price)); + } else { + errors |= Errors::Price; + emit error(i18n("Unable to parse price for %1", d->m_symbol)); + result = false; } if (dateRegExp.indexIn(quotedata) > -1) { QString datestr = dateRegExp.cap(1); + emit status(i18n("Date found: '%1'", datestr)); MyMoneyDateFormat dateparse(d->m_source.m_dateformat); try { d->m_date = dateparse.convertString(datestr, false /*strict*/); gotdate = true; kDebug(Private::dbgArea()) << "Date" << datestr; - emit status(i18n("Date found: '%1'", d->m_date.toString()));; - } catch (const MyMoneyException &) { - // emit error(i18n("Unable to parse date %1 using format %2: %3").arg(datestr,dateparse.format(),e.what())); + emit status(i18n("Date format found: '%1' -> %2", datestr, d->m_date.toString())); + } catch (const MyMoneyException &e) { + errors |= Errors::DateFormat; + emit error(i18n("Unable to parse date %1 using format %2: %3").arg(datestr,dateparse.format(), e.what())); d->m_date = QDate::currentDate(); + emit status(i18n("Using current date for %1").arg(d->m_symbol)); gotdate = true; } + } else { + errors |= Errors::Date; + emit error(i18n("Unable to parse date for %1").arg(d->m_symbol)); + d->m_date = QDate::currentDate(); + emit status(i18n("Using current date for %1").arg(d->m_symbol)); + gotdate = true; } if (gotprice && gotdate) { emit quote(d->m_id, d->m_symbol, d->m_date, d->m_price); } else { - emit error(i18n("Unable to update price for %1 (no price or no date)", d->m_symbol)); emit failed(d->m_id, d->m_symbol); + result = false; } } else { + errors |= Errors::Data; emit error(i18n("Unable to update price for %1 (empty quote data)", d->m_symbol)); emit failed(d->m_id, d->m_symbol); + result = false; } + return result; } const QMap WebPriceQuote::defaultQuoteSources() diff --git a/kmymoney/dialogs/settings/ksettingsonlinequotes.h b/kmymoney/dialogs/settings/ksettingsonlinequotes.h --- a/kmymoney/dialogs/settings/ksettingsonlinequotes.h +++ b/kmymoney/dialogs/settings/ksettingsonlinequotes.h @@ -53,15 +53,22 @@ void resetConfig(); protected slots: + void slotDeleteEntry(); + void slotShowEntry(); void slotUpdateEntry(); void slotLoadWidgets(); void slotEntryChanged(); void slotNewEntry(); + void slotCheckEntry(); + void slotLogStatus(const QString &s); + void slotLogQuote(const QString &id, const QString &symbol, const QString &date, double price); void slotEntryRenamed(QListWidgetItem* item); void slotStartRename(QListWidgetItem* item); protected: void loadList(const bool updateResetList = false); + void clearIcons(); + void setupIcons(const WebPriceQuote::Errors &errors = WebPriceQuote::Errors::noError); private: QList m_resetList; diff --git a/kmymoney/dialogs/settings/ksettingsonlinequotes.cpp b/kmymoney/dialogs/settings/ksettingsonlinequotes.cpp --- a/kmymoney/dialogs/settings/ksettingsonlinequotes.cpp +++ b/kmymoney/dialogs/settings/ksettingsonlinequotes.cpp @@ -22,6 +22,7 @@ #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes @@ -31,6 +32,7 @@ #include #include #include +#include // ---------------------------------------------------------------------------- // Project Includes @@ -47,7 +49,7 @@ m_updateButton->setEnabled(false); - KGuiItem updateButtenItem(i18nc("Accepts the entered data and stores it", "&Update"), + KGuiItem updateButtenItem(i18nc("Accepts the entered data and stores it", "&Accept"), KIcon("dialog-ok"), i18n("Accepts the entered data and stores it"), i18n("Use this to accept the modified data.")); @@ -59,14 +61,29 @@ i18n("Use this to delete the selected online source entry")); m_deleteButton->setGuiItem(deleteButtenItem); + KGuiItem checkButtonItem(i18nc("Check the selected source entry", "&Check Source"), + KIcon("document-edit-verify"), + i18n("Check the selected source entry"), + i18n("Use this to check the selected online source entry")); + m_checkButton->setGuiItem(checkButtonItem); + + KGuiItem showButtonItem(i18nc("Show the selected source entry in a web browser", "&Show page"), + KIcon("applications-internet"), + i18n("Show the selected source entry in a web browser"), + i18n("Use this to show the selected online source entry")); + m_showButton->setGuiItem(showButtonItem); + KGuiItem newButtenItem(i18nc("Create a new source entry for online quotes", "&New..."), KIcon("document-new"), i18n("Create a new source entry for online quotes"), i18n("Use this to create a new entry for online quotes")); m_newButton->setGuiItem(newButtenItem); connect(m_updateButton, SIGNAL(clicked()), this, SLOT(slotUpdateEntry())); connect(m_newButton, SIGNAL(clicked()), this, SLOT(slotNewEntry())); + connect(m_checkButton, SIGNAL(clicked()), this, SLOT(slotCheckEntry())); + connect(m_deleteButton, SIGNAL(clicked()), this, SLOT(slotDeleteEntry())); + connect(m_showButton, SIGNAL(clicked()), this, SLOT(slotShowEntry())); connect(m_quoteSourceList, SIGNAL(itemSelectionChanged()), this, SLOT(slotLoadWidgets())); connect(m_quoteSourceList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(slotEntryRenamed(QListWidgetItem*))); @@ -79,8 +96,7 @@ connect(m_editPrice, SIGNAL(textChanged(QString)), this, SLOT(slotEntryChanged())); connect(m_skipStripping, SIGNAL(toggled(bool)), this, SLOT(slotEntryChanged())); - // FIXME deleting a source is not yet implemented - m_deleteButton->setEnabled(false); + m_logWindow->setVisible(false); } void KSettingsOnlineQuotes::loadList(const bool updateResetList) @@ -171,14 +187,44 @@ void KSettingsOnlineQuotes::slotEntryChanged() { + clearIcons(); bool modified = m_editURL->text() != m_currentItem.m_url || m_editSymbol->text() != m_currentItem.m_sym || m_editDate->text() != m_currentItem.m_date || m_editDateFormat->text() != m_currentItem.m_dateformat || m_editPrice->text() != m_currentItem.m_price || m_skipStripping->isChecked() != m_currentItem.m_skipStripping; m_updateButton->setEnabled(modified); + m_checkButton->setEnabled(!modified); +} + +void KSettingsOnlineQuotes::slotDeleteEntry() +{ + QListWidgetItem* item = m_quoteSourceList->findItems(m_currentItem.m_name, Qt::MatchExactly).at(0); + if (!item) + return; + + int ret = KMessageBox::warningContinueCancel(this, + i18n("Are you sure to delete this online quote ?"), + i18n("Delete online quote"), + KStandardGuiItem::cont(), + KStandardGuiItem::cancel(), + QString("DeletingOnlineQuote")); + if (ret == KMessageBox::Cancel) + return; + + delete item; + m_currentItem.remove(); + slotEntryChanged(); +} + +void KSettingsOnlineQuotes::slotShowEntry() +{ + if (m_currentItem.m_url.contains("%2")) + QDesktopServices::openUrl(m_currentItem.m_url.arg("BTC","GBP")); + else + QDesktopServices::openUrl(m_currentItem.m_url.arg("GBP")); } void KSettingsOnlineQuotes::slotUpdateEntry() @@ -190,6 +236,7 @@ m_currentItem.m_price = m_editPrice->text(); m_currentItem.m_skipStripping = m_skipStripping->isChecked(); m_currentItem.write(); + m_checkButton->setEnabled(true); slotEntryChanged(); } @@ -205,6 +252,66 @@ } } +void KSettingsOnlineQuotes::clearIcons() +{ + QPixmap emptyIcon; + m_urlCheckLabel->setPixmap(emptyIcon); + m_dateCheckLabel->setPixmap(emptyIcon); + m_priceCheckLabel->setPixmap(emptyIcon); + m_symbolCheckLabel->setPixmap(emptyIcon); + m_dateFormatCheckLabel->setPixmap(emptyIcon); +} + +void KSettingsOnlineQuotes::setupIcons(const WebPriceQuote::Errors &errors) +{ + QPixmap okIcon(BarIcon("dialog-ok-apply")); + QPixmap failIcon(BarIcon("dialog-cancel")); + + if (errors & WebPriceQuote::Errors::URL) + m_urlCheckLabel->setPixmap(failIcon); + else { + m_urlCheckLabel->setPixmap(okIcon); + m_symbolCheckLabel->setPixmap(errors & WebPriceQuote::Errors::Symbol ? failIcon : okIcon); + m_priceCheckLabel->setPixmap(errors & WebPriceQuote::Errors::Price ? failIcon : okIcon); + if (errors & WebPriceQuote::Errors::Date) + m_dateCheckLabel->setPixmap(failIcon); + else { + m_dateCheckLabel->setPixmap(okIcon); + m_dateFormatCheckLabel->setPixmap(errors & WebPriceQuote::Errors::DateFormat? failIcon : okIcon); + } + } +} + +void KSettingsOnlineQuotes::slotCheckEntry() +{ + WebPriceQuote quote; + m_logWindow->setVisible(true); + m_logWindow->clear(); + clearIcons(); + + connect("e, SIGNAL(status(const QString&)), this, SLOT(slotLogStatus(const QString&))); + connect("e, SIGNAL(error(const QString&)), this, SLOT(slotLogStatus(const QString&))); + connect("e, SIGNAL(failed(const QString&)), this, SLOT(slotLogStatus(const QString&))); + connect("e, SIGNAL(quote(const QString&, const QString&, const QString&, double)), this, SLOT(slotLogQuote(const QString&, const QString&, const QString&, double))); + WebPriceQuote::Errors errors; + if (m_currentItem.m_url.contains("%2")) + quote.launch("BTC > GBP", "BTC GBP", m_currentItem.m_name, errors); + else + quote.launch("GBP", "GBP", m_currentItem.m_name, errors); + setupIcons(errors); +} + +void KSettingsOnlineQuotes::slotLogStatus(const QString &s) +{ + new QListWidgetItem(s, m_logWindow); + m_logWindow->scrollToBottom(); +} + +void KSettingsOnlineQuotes::slotLogQuote(const QString &id, const QString &symbol, const QString &date, double price) +{ + slotLogStatus(QString("%1 %2 %3 %4").arg(id, symbol, date).arg(price)); +} + void KSettingsOnlineQuotes::slotStartRename(QListWidgetItem* item) { m_quoteInEditing = true; diff --git a/kmymoney/dialogs/settings/ksettingsonlinequotesdecl.ui b/kmymoney/dialogs/settings/ksettingsonlinequotesdecl.ui --- a/kmymoney/dialogs/settings/ksettingsonlinequotesdecl.ui +++ b/kmymoney/dialogs/settings/ksettingsonlinequotesdecl.ui @@ -1,155 +1,166 @@ - - - - - KSettingsOnlineQuotesDecl - - - - 0 - 0 - 512 - 442 - - - - - 0 - 0 - - - - Online Quotes - - - - - - - 7 - 3 - 0 - 0 - - - - - Name - - - true - - - true - - - - - - - - Details - - - <i>Enter regular expressions which can be used to parse the data returned from the URL entered above. The symbol, price, and date must be found in the quote data to be usable. You may also try the KMyMoney user's mailinglist at <a href="mailto:kmymoney@kde.org">kmymoney@kde.org</a> to find what settings work for other users in your country.</i> - - - - - - Date - - - false - - - - - - - Symbol - - - false - - - - - - - Regular Expression to extract the symbol from the downloaded data - - - - - - - Regular Expression to extract the price from the downloaded data - - - - - - - URL to be used to download the quote - - - Enter the URL from which stock quotes will be fetched. <b>%1</b> will be replaced with the symbol for the security being quoted. For currency conversions, <b>%2</b> will be replaced with the currency to be quoted and <b>%1</b> with the currency the quote is based on. - - - - - - - Price - - - false - - - - - - - Regular Expression to extract the date from the downloaded data - - - - - - - Date Format - - - false - - - - - - - URL - - - false - - - - - - - Regular Expression to extract the date from the downloaded data - - - - - - - Skip HTML stripping - - - <p>For easier processing of the data returned by the online source, KMyMoney usually strips unused parts before it is parsed with the regular expressions. If matching of the fields relies on those items, then use this option to turn stripping off.</p> + + KSettingsOnlineQuotesDecl + + + + 0 + 0 + 512 + 598 + + + + + 0 + 0 + + + + Online Quotes + + + + + + + 0 + 0 + + + + + + + + <html><head/><body><p><span style=" font-style:italic;">Enter regular expressions which can be used to parse the data returned from the URL entered above. The symbol, price, and date must be found in the quote data to be usable. You may also try the KMyMoney forum at </span><a href="https://forum.kde.org/viewforum.php?f=69"><span style=" text-decoration: underline; color:#2980b9;">https://forum.kde.org/viewforum.php?f=69</span></a> or the <span style=" font-style:italic;">user's mailinglist at </span><a href="mailto:kmymoney@kde.org"><span style=" font-style:italic; text-decoration: underline; color:#2980b9;">kmymoney@kde.org</span></a><span style=" font-style:italic;"> to find what settings work for other users in your country.</span></p></body></html> + + + Details + + + + + + + + Regular Expression to extract the symbol from the downloaded data + + + + + + + URL + + + false + + + + + + + Price + + + false + + + + + + + Date + + + false + + + + + + + Regular Expression to extract the date from the downloaded data + + + + + + + Regular Expression to extract the date from the downloaded data + + + + + + + URL to be used to download the quote + + + Enter the URL from which stock quotes will be fetched. <b>%1</b> will be replaced with the symbol for the security being quoted. For currency conversions, <b>%2</b> will be replaced with the currency to be quoted and <b>%1</b> with the currency the quote is based on. + + + + + + + + + + + + + + + + + + + + + Regular Expression to extract the price from the downloaded data + + + + + + + Date Format + + + false + + + + + + + Symbol + + + false + + + + + + + + + + + + + + + + + + + + + <p>For easier processing of the data returned by the online source, KMyMoney usually strips unused parts before it is parsed with the regular expressions. If matching of the fields relies on those items, then use this option to turn stripping off.</p> <p>The following items are usually removed by stripping: @@ -159,61 +170,116 @@ <li>duplicate whitespace</li> </ul> </p> - - - - - - - - - - - - New - - - - - - - Delete - - - - - - - - 240 - 20 - - - - QSizePolicy::Expanding - - - Qt::Horizontal - - - - - - - Update - - - - + + + Skip HTML stripping + + + + + + + + + + + + + + + + + + true + + + + + + + + + New + + + + + + + Delete + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Check Source + + + + + + + Show Page + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Accept + + + - - - - - KListWidget - QListWidget -
klistwidget.h
-
-
+
+ + + + + + KLineEdit + QLineEdit +
klineedit.h
+
+ + KPushButton + QPushButton +
kpushbutton.h
+
+ + KListWidget + QListWidget +
klistwidget.h
+
+
+ +