From 5113f32e59de7216a85d1efa2326f25fce7bc569 Mon Sep 17 00:00:00 2001 From: Oleg Plakhotniuk Date: Tue, 24 Feb 2015 23:27:13 -0600 Subject: [PATCH] Victoria's Secret module. Closes #1731 --- modules/vicsec/__init__.py | 23 +++++ modules/vicsec/browser.py | 194 +++++++++++++++++++++++++++++++++++++ modules/vicsec/favicon.png | Bin 0 -> 2026 bytes modules/vicsec/module.py | 60 ++++++++++++ modules/vicsec/test.py | 33 +++++++ 5 files changed, 310 insertions(+) create mode 100644 modules/vicsec/__init__.py create mode 100644 modules/vicsec/browser.py create mode 100644 modules/vicsec/favicon.png create mode 100644 modules/vicsec/module.py create mode 100644 modules/vicsec/test.py diff --git a/modules/vicsec/__init__.py b/modules/vicsec/__init__.py new file mode 100644 index 00000000..6e789a91 --- /dev/null +++ b/modules/vicsec/__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 VicSecModule + +__all__ = ['VicSecModule'] diff --git a/modules/vicsec/browser.py b/modules/vicsec/browser.py new file mode 100644 index 00000000..a5204348 --- /dev/null +++ b/modules/vicsec/browser.py @@ -0,0 +1,194 @@ +# -*- 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 OrderNotFound, Order, Item, Payment +from weboob.exceptions import BrowserIncorrectPassword + +from datetime import datetime +from decimal import Decimal +from itertools import chain +import re + + +__all__ = ['VicSec'] + + +class VicSecPage(HTMLPage): + @property + def logged(self): + return bool(self.doc.xpath( + '//a[@href="https://www.victoriassecret.com/account/profile"]')) + + +class LoginPage(VicSecPage): + def login(self, email, password): + form = self.get_form(name='accountLogonForm') + form['j_username'] = email + form['j_password'] = password + form.submit() + + +class HistoryPage(VicSecPage): + def iter_orders(self): + return [onum for date, onum in sorted( + chain(self.orders(), self.returns()), reverse=True)] + + def orders(self): + for tr in self.doc.xpath('//table[@class="order-status"]/tbody[1]/tr'): + date = datetime.strptime(tr.xpath('td[1]/text()')[0], '%m/%d/%Y') + num = tr.xpath('td[2]/a/text()')[0] + status = tr.xpath('td[4]/span/text()')[0] + if status == u'Delivered': + yield date, num + + def returns(self): + for tr in self.doc.xpath('//table[@class="order-status"]/tbody[3]/tr'): + num = tr.xpath('td[1]/a/text()')[0] + date = datetime.strptime(tr.xpath('td[2]/text()')[0], '%m/%d/%Y') + status = tr.xpath('td[4]/span/text()')[0] + if status == u'Complete': + yield date, num + + +class OrderPage(VicSecPage): + def order(self): + order = Order(id=self.order_number()) + order.date = self.order_date() + order.tax = self.tax() + order.discount = self.discount() + order.shipping = self.shipping() + return order + + def payments(self): + for tr in self.doc.xpath('//tbody[@class="payment-summary"]' + '//th[text()="Payment Summary"]/../../../tbody/tr'): + method = tr.xpath('td[1]/text()')[0] + amount = tr.xpath('td[2]')[0].text_content().strip() + pmt = Payment() + pmt.date = self.order_date() + pmt.method = unicode(method) + pmt.amount = AmTr.decimal_amount(amount) + yield pmt + + def items(self): + for tr in self.doc.xpath('//tbody[@class="order-items"]/tr'): + label = tr.xpath('*//h1')[0].text_content() + price = AmTr.decimal_amount(re.match(r'^\s*([^\s]+)(\s+.*)?', + tr.xpath('*//div[@class="price"]')[0].text_content(), + re.DOTALL).group(1)) + url = 'http:' + tr.xpath('*//img/@src')[0] + item = Item() + item.label = unicode(label) + item.url = unicode(url) + item.price = price + yield item + + def is_void(self): + return not self.doc.xpath('//tbody[@class="order-items"]/tr') + + def order_number(self): + return self.order_info(u'Order Number') + + def order_date(self): + return datetime.strptime(self.order_info(u'Order Date'), '%m/%d/%Y') + + def tax(self): + return self.payment_part(u'Sales Tax') + + def shipping(self): + return self.payment_part(u'Shipping & Handling') + + def order_info(self, which): + info = self.doc.xpath('//p[@class="orderinfo details"]' + )[0].text_content() + return re.match(u'.*%s:\\s+([^\\s]+)\\s'%which,info,re.DOTALL).group(1) + + def discount(self): + # Sometimes subtotal doesn't add up with items. + # I saw that "Special Offer" was actually added to the item's price, + # instead of being subtracted. Looks like a bug on VS' side. + # To compensate for it I'm correcting discount value. + dcnt = self.payment_part(u'Special Offer') + subt = self.payment_part(u'Merchandise Subtotal') + rett = self.payment_part(u'Return Merchandise Total') + items = sum(i.price for i in self.items()) + return dcnt + subt + rett - items + + def payment_part(self, which): + for node in self.doc.xpath('//tbody[@class="payment-summary"]' + '//td[contains(text(),"%s")]/../td[2]' % which): + x = node.text_content().strip() + return Decimal(0) if x == u'FREE' else AmTr.decimal_amount(x) + return Decimal(0) + + +class VicSec(LoginBrowser): + BASEURL = 'https://www.victoriassecret.com' + login = URL(r'/account/signin/overlay$', LoginPage) + history = URL(r'/account/orderhistory$', HistoryPage) + order = URL(r'/account/orderdetails\?orderNumber=(?P\d+)$', + r'/account/orderdetails.*$', + OrderPage) + unknown = URL(r'/.*$', VicSecPage) + + def get_currency(self): + # Victoria's Secret uses only U.S. dollars. + return Currency.get_currency(u'$') + + def get_order(self, id_): + return self.to_order(id_).order() + + def iter_orders(self): + for order in self.to_history().iter_orders(): + yield self.to_order(order).order() + + def iter_payments(self, order): + return self.to_order(order.id).payments() + + def iter_items(self, order): + return self.to_order(order.id).items() + + @need_login + def to_history(self): + self.history.stay_or_go() + assert self.history.is_here() + return self.page + + @need_login + def to_order(self, order_num): + self.order.stay_or_go(order_num=order_num) + assert self.order.is_here(order_num=order_num) + if self.page.is_void(): + raise OrderNotFound() + return self.page + + def do_login(self): + self.session.cookies.clear() + # Need to go there two times. Perhaps because of cookies... + self.login.go() + self.login.go().login(self.username, self.password) + self.history.go() + if not self.history.is_here(): + raise BrowserIncorrectPassword() diff --git a/modules/vicsec/favicon.png b/modules/vicsec/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5b653f609c19a2e07b089987cab2bd84b94f0971 GIT binary patch literal 2026 zcmV004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Ra0v7}sH@pU%*8l(p@<~KNR9M5^S9?%Z)f)erTca^e z*V3lPSTotwt*d4aGfi^tO%vY_N=?+p1fMxZQQdptnn_vt>W)oZ6$;5vDLN5~AoMUb z4bc!QA8<)zR1i2k&XcqEUTgjO)<)`+&PM^P!C;}+`wJxs&Els+v` z-2qT&x{Yqd3ZmW)9(9ghKtOgKfSZX#x-1{V=yIBJ3a`*fc0_joq`8oQ9PEg@2P!^)-#%k0~r9Tn9?iJ`JfHbg;D1}F4-9SBQBfGVm0H*TXMUx$mCN|ZL zqATcAra?CW1oP;I&6m-`@JuEe98lc@5b(}Q?M2B4u8qXBkD`dSwJGw~#dN6|c!_?9 zrvAutR8aAW%V`q2!h@ffj*C4PBaOF*VevGz&?to484bta@GLxbO89e2W5<2%#z+ued@btK|4S)WwR&8 zQ{U^4vyk7V31O%d20$Q}7V*j~>N%Vi87L==n!*4mc>DFNf@tVV2jRtg6x}r@{sdqm zn?~|O?EP%bcDknlM)v?XEv}~FC((2ko+KLD2q8lYk}l2pc_bC~44}Uzta#$uh&7TO z5KOZm%7h2dUjJa)&_PifoLt$Y`guz33?PF6>=MQW^3ou`sC*p;m%qmk?Hw)52$ALlG zcWsuWZ&z*d1HO%^9>ukb``sRUi2dO?E;rvSrL%!PG}zJ9ws>-D`GuE#@N7S@i=y(e zYsq=w^Vj~ncHYNFpH5kpxGv#5s#d3^JXZ$cSh_Xw8^+6CRL@r<-HpmSSKz>#Z>%Sr z>wO3)o)7FFVPX5i+JN=N`idiR%34+6s(PYKid_Q_BX?Xp zxC+R?w8g;7JAfn8(KNbBTVBs5#%|%u{4_3&czG-i#=;$03FJM=JI8zDmczH#Cr1@P zQ2=>&`P^|05=W-ut4Q82JDQVv+-3;ZkmE2c93B`r9D`%=%%;5QFr+8BazrB^Sde+u z#~enOcNXkh&C2^`BEB4?qs6oln41RGIH(M!iSGutmPY1`I>C8lEmpL%?;c9*cS8FvQnmJ^4lp3|B3Y3 z>Ccp~PEukvE~(xB)VjETB+swu$nn7d0{e$)Eh6_}oT$N9GguonFCixiJNo;X1LIKU ztSrZz3=}to1dyt2242XwVdV6BprYv-{sWn7W6)3-DcneT; zk7J#y`9WRGc%hvWacCub^0UaC|1>lF{OsTw(>W{iso=g@3^2UnL$~wpavci`MFY}@ zo?==SjxEpBVvfs(2m=7ai}Ha^>>jUQjYx*c;YIE=tR2I~6zMJS8jBsB0D|(Qgrfqb z(XA*P2Jb19-1Gsz$dbF6#*fVd9An`AACqwAmT&+9nbY4&C;_(q?d=!yfRb79Yo1<( zcSfIvXYQ)H)JHZZ6hgXYtuTWMbua*TpD*(>?c8SFMJbp_=Z;k>;q$d{U2Jr@{k1jB zxPajfJ}q_r(`9FF)&AV}WM8BV9)hp{?e$A_x_4BO_PX^80NrnYJiw1d3Q<2q9=bOG z001R)MObuXVRU6WV{&C-bY%cCFflbPFfuJNGE_1$Ix;mnGczqPHaajcW%{{>0000b zbVXQnWMOn=I&E)cX=Zr. + + +from weboob.capabilities.shop import CapShop +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import VicSec + + +__all__ = ['VicSecModule'] + + +class VicSecModule(Module, CapShop): + NAME = 'vicsec' + MAINTAINER = u'Oleg Plakhotniuk' + EMAIL = 'olegus8@gmail.com' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'Victoria\'s Secret' + CONFIG = BackendConfig( + ValueBackendPassword('email', label='Username', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = VicSec + + def create_default_browser(self): + return self.create_browser(self.config['email'].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/vicsec/test.py b/modules/vicsec/test.py new file mode 100644 index 00000000..ab5ca00c --- /dev/null +++ b/modules/vicsec/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 VicSecTest(BackendTest): + MODULE = 'vicsec' + + 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)