From bea188e7f444a25a9547446d694d1bb165012aa6 Mon Sep 17 00:00:00 2001 From: Oleg Plakhotniuk Date: Tue, 24 Feb 2015 23:30:15 -0600 Subject: [PATCH] Ideel module. Closes #1733 --- modules/ideel/__init__.py | 23 ++++++ modules/ideel/browser.py | 163 ++++++++++++++++++++++++++++++++++++++ modules/ideel/favicon.png | Bin 0 -> 2571 bytes modules/ideel/module.py | 58 ++++++++++++++ modules/ideel/test.py | 33 ++++++++ 5 files changed, 277 insertions(+) create mode 100644 modules/ideel/__init__.py create mode 100644 modules/ideel/browser.py create mode 100644 modules/ideel/favicon.png create mode 100644 modules/ideel/module.py create mode 100644 modules/ideel/test.py diff --git a/modules/ideel/__init__.py b/modules/ideel/__init__.py new file mode 100644 index 00000000..22e4fdd2 --- /dev/null +++ b/modules/ideel/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .module import IdeelModule + +__all__ = ['IdeelModule'] diff --git a/modules/ideel/browser.py b/modules/ideel/browser.py new file mode 100644 index 00000000..e00bd862 --- /dev/null +++ b/modules/ideel/browser.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.capabilities.bank.transactions import \ + AmericanTransaction as AmTr +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.pages import HTMLPage +from weboob.capabilities.base import Currency +from weboob.capabilities.shop import Order, Item, Payment, OrderNotFound +from weboob.exceptions import BrowserIncorrectPassword + +import re +from decimal import Decimal +from datetime import datetime +from itertools import takewhile, count + + +__all__ = ['Ideel'] + + +class IdeelPage(HTMLPage): + @property + def logged(self): + return bool(self.doc.xpath('//a[@href="/logout"]')) + + +class LoginPage(IdeelPage): + def login(self, username, password): + form = self.get_form(xpath='//form[@id="iform"]') + form['login'] = username + form['password'] = password + form.submit() + + +class HistoryPage(IdeelPage): + def exists(self): + return bool(self.doc.xpath('//table[@id="order_history"]')) + + def iter_orders(self): + return (tr.xpath('td[1]/a/text()')[0][1:] + for tr in self.doc.xpath('//table[@id="order_history"]/tbody/tr')) + + +class OrderPage(IdeelPage): + def exists(self): + return bool(self.order_number()) + + def order(self): + order = Order(id=self.order_number()) + order.date = self.order_date() + order.tax = self.tax() + order.shipping = self.shipping() + order.discount = self.discount() + order.total = self.total() + return order + + def items(self): + for tr in self.doc.xpath('//table[contains(@class,"items_table")]' + '/tr[td[@class="items_desc"]]'): + label = tr.xpath('*//div[@class="item_desc"]//span/text()')[0] + url = tr.xpath('*//div[@class="item_img"]//@src')[0] + onclk = tr.xpath('*//div[@class="item_img"]//@onclick') + if onclk: + url=re.match(r'window.open\(\'([^\']*)\'.*', onclk[0]).group(1) + if url.startswith('/'): + url = self.browser.BASEURL + url + price = tr.xpath('td[@class="items_price"]/span/text()')[0] + qty = tr.xpath('td[@class="items_qty"]//span/text()')[0] + price = AmTr.decimal_amount(price) * Decimal(qty) + item = Item() + item.label = unicode(label) + item.url = unicode(url) + item.price = price + yield item + + def payments(self): + # There's no payment information on Ideel, so we'll make one up. + p = Payment() + p.date = self.order_date() + p.method = u'DEFAULT PAYMENT' + p.amount = self.total() + yield p + + def order_number(self): + return next(iter(self.doc.xpath( + u'//b[text()="Order Number:"]/../strong/text()')), None) + + def order_date(self): + txt = self.doc.xpath('//div[@id="purchase-notice"]/text()')[0] + date = re.match(r'.* (\w+ \d+, \d+)$', txt).group(1) + return datetime.strptime(date, '%b %d, %Y') + + def tax(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="taxes"]/text()')[0]) + + def shipping(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="shipping_fee"]/text()')[0]) + + def discount(self): + TAGS = ['coupon_discount_amount', 'promo_discount_amount', + 'total_rewards', 'applied_credit'] + return -sum(AmTr.decimal_amount(x[1:][:-1]) for tag in TAGS + for x in self.doc.xpath('//span[@id="%s"]/text()' % tag)) + + def total(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="total"]/text()')[0]) + + +class Ideel(LoginBrowser): + BASEURL = 'http://www.ideel.com' + login = URL(r'https://www.ideel.com/login$', LoginPage) + history = URL(r'/my_account/orders\?page=(?P\d+)$', HistoryPage) + order = URL(r'/my_account/orders/(?P\d+)$', OrderPage) + unknown = URL(r'/.*$', IdeelPage) + + def get_currency(self): + # Ideel uses only U.S. dollars. + return Currency.get_currency(u'$') + + @need_login + def get_order(self, id_): + if self.order.go(order=id_).exists(): + return self.page.order() + raise OrderNotFound() + + @need_login + def iter_orders(self): + exists = HistoryPage.exists + hists = takewhile(exists, (self.history.go(page=i) for i in count(1))) + return (self.get_order(x) for h in hists for x in h.iter_orders()) + + @need_login + def iter_payments(self, order): + return self.order.stay_or_go(order=order.id).payments() + + @need_login + def iter_items(self, order): + return self.order.stay_or_go(order=order.id).items() + + def do_login(self): + self.login.stay_or_go().login(self.username, self.password) + if not self.page.logged: + raise BrowserIncorrectPassword() diff --git a/modules/ideel/favicon.png b/modules/ideel/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4341000b9e7e8b167257d6679e3a61f1bf361e GIT binary patch literal 2571 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 literal 0 HcmV?d00001 diff --git a/modules/ideel/module.py b/modules/ideel/module.py new file mode 100644 index 00000000..e446c163 --- /dev/null +++ b/modules/ideel/module.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.shop import CapShop +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Ideel + +__all__ = ['IdeelModule'] + +class IdeelModule(Module, CapShop): + NAME = 'ideel' + MAINTAINER = u'Oleg Plakhotniuk' + EMAIL = 'olegus8@gmail.com' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'Ideel' + CONFIG = BackendConfig( + ValueBackendPassword('username', label='User name', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = Ideel + + def create_default_browser(self): + return self.create_browser(self.config['username'].get(), + self.config['password'].get()) + + def get_currency(self): + return self.browser.get_currency() + + def get_order(self, id_): + return self.browser.get_order(id_) + + def iter_orders(self): + return self.browser.iter_orders() + + def iter_payments(self, order): + return self.browser.iter_payments(order) + + def iter_items(self, order): + return self.browser.iter_items(order) diff --git a/modules/ideel/test.py b/modules/ideel/test.py new file mode 100644 index 00000000..19884d55 --- /dev/null +++ b/modules/ideel/test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.tools.test import BackendTest + + +class IdeelTest(BackendTest): + MODULE = 'ideel' + + def test_history(self): + """ + Test that at least one item was ordered in the whole history. + """ + b = self.backend + items = (i for o in b.iter_orders() for i in b.iter_items(o)) + item = next(items, None) + self.assertNotEqual(item, None)