[amazon] pep8 fixes
This commit is contained in:
parent
170db43de1
commit
f7e7e45760
5 changed files with 56 additions and 50 deletions
|
|
@ -26,10 +26,11 @@ from weboob.capabilities.shop import OrderNotFound
|
||||||
from weboob.exceptions import BrowserIncorrectPassword
|
from weboob.exceptions import BrowserIncorrectPassword
|
||||||
|
|
||||||
from .pages import HomePage, LoginPage, AmazonPage, HistoryPage, \
|
from .pages import HomePage, LoginPage, AmazonPage, HistoryPage, \
|
||||||
OrderOldPage, OrderNewPage
|
OrderOldPage, OrderNewPage
|
||||||
|
|
||||||
__all__ = ['Amazon']
|
__all__ = ['Amazon']
|
||||||
|
|
||||||
|
|
||||||
class Amazon(LoginBrowser):
|
class Amazon(LoginBrowser):
|
||||||
BASEURL = 'https://www.amazon.com'
|
BASEURL = 'https://www.amazon.com'
|
||||||
MAX_RETRIES = 10
|
MAX_RETRIES = 10
|
||||||
|
|
@ -84,13 +85,13 @@ class Amazon(LoginBrowser):
|
||||||
their users to new pages, and the rest to old ones.
|
their users to new pages, and the rest to old ones.
|
||||||
"""
|
"""
|
||||||
if (not self.order_new.is_here() and not self.order_old.is_here()) \
|
if (not self.order_new.is_here() and not self.order_old.is_here()) \
|
||||||
or self.page.order_number() != order_id:
|
or self.page.order_number() != order_id:
|
||||||
try:
|
try:
|
||||||
self.order_new.go(order_id=order_id)
|
self.order_new.go(order_id=order_id)
|
||||||
except HTTPNotFound:
|
except HTTPNotFound:
|
||||||
self.order_old.go(order_id=order_id)
|
self.order_old.go(order_id=order_id)
|
||||||
if (not self.order_new.is_here() and not self.order_old.is_here()) \
|
if (not self.order_new.is_here() and not self.order_old.is_here()) \
|
||||||
or self.page.order_number() != order_id:
|
or self.page.order_number() != order_id:
|
||||||
raise OrderNotFound()
|
raise OrderNotFound()
|
||||||
return self.page
|
return self.page
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ from weboob.browser import URL
|
||||||
from ..browser import Amazon
|
from ..browser import Amazon
|
||||||
|
|
||||||
from .pages import HomePage, LoginPage, AmazonPage, HistoryPage, \
|
from .pages import HomePage, LoginPage, AmazonPage, HistoryPage, \
|
||||||
OrderOldPage, OrderNewPage
|
OrderOldPage, OrderNewPage
|
||||||
|
|
||||||
__all__ = ['AmazonFR']
|
__all__ = ['AmazonFR']
|
||||||
|
|
||||||
|
|
||||||
class AmazonFR(Amazon):
|
class AmazonFR(Amazon):
|
||||||
BASEURL = 'https://www.amazon.fr'
|
BASEURL = 'https://www.amazon.fr'
|
||||||
CURRENCY = u'€'
|
CURRENCY = u'€'
|
||||||
|
|
|
||||||
68
modules/amazon/fr/pages.py
Executable file → Normal file
68
modules/amazon/fr/pages.py
Executable file → Normal file
|
|
@ -28,6 +28,7 @@ import re
|
||||||
# Ugly array to avoid the use of french locale
|
# Ugly array to avoid the use of french locale
|
||||||
FRENCH_MONTHS = [u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre']
|
FRENCH_MONTHS = [u'janvier', u'février', u'mars', u'avril', u'mai', u'juin', u'juillet', u'août', u'septembre', u'octobre', u'novembre', u'décembre']
|
||||||
|
|
||||||
|
|
||||||
class AmazonPage(HTMLPage):
|
class AmazonPage(HTMLPage):
|
||||||
@property
|
@property
|
||||||
def logged(self):
|
def logged(self):
|
||||||
|
|
@ -51,8 +52,8 @@ class LoginPage(AmazonPage):
|
||||||
|
|
||||||
|
|
||||||
class HistoryPage(AmazonPage):
|
class HistoryPage(AmazonPage):
|
||||||
forced_encoding=True
|
forced_encoding = True
|
||||||
ENCODING='UTF-8'
|
ENCODING = 'UTF-8'
|
||||||
|
|
||||||
def iter_years(self):
|
def iter_years(self):
|
||||||
for year in self.opt_years():
|
for year in self.opt_years():
|
||||||
|
|
@ -74,8 +75,8 @@ class HistoryPage(AmazonPage):
|
||||||
|
|
||||||
def opt_years(self):
|
def opt_years(self):
|
||||||
return [x for x in self.doc.xpath(
|
return [x for x in self.doc.xpath(
|
||||||
'//select[@name="orderFilter"]/option/@value'
|
'//select[@name="orderFilter"]/option/@value'
|
||||||
) if x.startswith('year-')]
|
) if x.startswith('year-')]
|
||||||
|
|
||||||
|
|
||||||
class OrderPage(AmazonPage):
|
class OrderPage(AmazonPage):
|
||||||
|
|
@ -84,23 +85,25 @@ class OrderPage(AmazonPage):
|
||||||
# finalized payment amounts.
|
# finalized payment amounts.
|
||||||
# Payment for not yet shipped orders may change, and is not always
|
# Payment for not yet shipped orders may change, and is not always
|
||||||
# available.
|
# available.
|
||||||
# TODO : Other French status applied ?
|
|
||||||
return bool([x for s in [u'En préparation pour expédition']
|
return bool([x for s in [u'En préparation pour expédition'] # TODO : Other French status applied ?
|
||||||
for x in self.doc.xpath(u'//*[contains(text(),"%s")]' % s)])
|
for x in self.doc.xpath(u'//*[contains(text(),"%s")]' % s)])
|
||||||
|
|
||||||
def decimal_amount(self, amount):
|
def decimal_amount(self, amount):
|
||||||
m = re.match(u'.*EUR ([,0-9]+).*', amount)
|
m = re.match(u'.*EUR ([,0-9]+).*', amount)
|
||||||
if m:
|
if m:
|
||||||
return Decimal(m.group(1).replace(",","."))
|
return Decimal(m.group(1).replace(",", "."))
|
||||||
|
|
||||||
def month_to_int(self, text):
|
def month_to_int(self, text):
|
||||||
for (idx, month) in enumerate(FRENCH_MONTHS):
|
for (idx, month) in enumerate(FRENCH_MONTHS):
|
||||||
text = text.replace(month, str(idx + 1))
|
text = text.replace(month, str(idx + 1))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
class OrderNewPage(OrderPage):
|
class OrderNewPage(OrderPage):
|
||||||
forced_encoding=True
|
# Need to force encoding because of mixed encoding
|
||||||
ENCODING='ISO-8859-15'
|
forced_encoding = True
|
||||||
|
ENCODING = 'ISO-8859-15'
|
||||||
is_here = u'//*[contains(text(),"Commandé le")]'
|
is_here = u'//*[contains(text(),"Commandé le")]'
|
||||||
|
|
||||||
def order(self):
|
def order(self):
|
||||||
|
|
@ -157,8 +160,9 @@ class OrderNewPage(OrderPage):
|
||||||
'/following-sibling::div[1]/span/text()')[0].strip())
|
'/following-sibling::div[1]/span/text()')[0].strip())
|
||||||
|
|
||||||
def date_num(self):
|
def date_num(self):
|
||||||
return u' '.join(self.doc.xpath(
|
return u' '.join(
|
||||||
'//span[@class="order-date-invoice-item"]/text()'
|
self.doc.xpath(
|
||||||
|
'//span[@class="order-date-invoice-item"]/text()'
|
||||||
)).replace('\n', '')
|
)).replace('\n', '')
|
||||||
|
|
||||||
def tax(self):
|
def tax(self):
|
||||||
|
|
@ -168,18 +172,18 @@ class OrderNewPage(OrderPage):
|
||||||
return self.amount(u'Livraison :')
|
return self.amount(u'Livraison :')
|
||||||
|
|
||||||
def discount(self):
|
def discount(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
return self.amount(u'Bon de réduction', u'Subscribe & Save', u'Your Coupon Savings',
|
return self.amount(u'Bon de réduction', u'Subscribe & Save', u'Your Coupon Savings',
|
||||||
u'Lightning Deal')
|
u'Lightning Deal')
|
||||||
|
|
||||||
def gift(self):
|
def gift(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
return self.amount(u'Gift Card Amount')
|
return self.amount(u'Gift Card Amount')
|
||||||
|
|
||||||
def amount(self, *names):
|
def amount(self, *names):
|
||||||
return Decimal(sum(self.decimal_amount(amount.strip())
|
return Decimal(sum(self.decimal_amount(amount.strip())
|
||||||
for n in names for amount in self.doc.xpath(
|
for n in names for amount in self.doc.xpath(
|
||||||
'(//span[contains(text(),"%s")]/../..//span)[2]/text()' % n)))
|
'(//span[contains(text(),"%s")]/../..//span)[2]/text()' % n)))
|
||||||
|
|
||||||
def transactions(self):
|
def transactions(self):
|
||||||
for row in self.doc.xpath('//span[contains(text(),"Transactions")]'
|
for row in self.doc.xpath('//span[contains(text(),"Transactions")]'
|
||||||
|
|
@ -217,7 +221,7 @@ class OrderNewPage(OrderPage):
|
||||||
price *= Decimal(amount)
|
price *= Decimal(amount)
|
||||||
if url:
|
if url:
|
||||||
url = unicode(self.browser.BASEURL) + \
|
url = unicode(self.browser.BASEURL) + \
|
||||||
re.match(u'(/gp/product/.*)/ref=.*', url).group(1)
|
re.match(u'(/gp/product/.*)/ref=.*', url).group(1)
|
||||||
if label and price:
|
if label and price:
|
||||||
itm = Item()
|
itm = Item()
|
||||||
itm.label = label
|
itm.label = label
|
||||||
|
|
@ -227,8 +231,8 @@ class OrderNewPage(OrderPage):
|
||||||
|
|
||||||
|
|
||||||
class OrderOldPage(OrderPage):
|
class OrderOldPage(OrderPage):
|
||||||
forced_encoding=True
|
forced_encoding = True
|
||||||
ENCODING='ISO-8859-15'
|
ENCODING = 'ISO-8859-15'
|
||||||
is_here = u'//*[contains(text(),"Amazon.fr numéro de commande")]'
|
is_here = u'//*[contains(text(),"Amazon.fr numéro de commande")]'
|
||||||
|
|
||||||
def order(self):
|
def order(self):
|
||||||
|
|
@ -238,7 +242,7 @@ class OrderOldPage(OrderPage):
|
||||||
order.tax = Decimal(self.tax()) if not empty(self.tax()) else Decimal(0.00)
|
order.tax = Decimal(self.tax()) if not empty(self.tax()) else Decimal(0.00)
|
||||||
order.discount = Decimal(self.discount()) if not empty(self.discount()) else Decimal(0.00)
|
order.discount = Decimal(self.discount()) if not empty(self.discount()) else Decimal(0.00)
|
||||||
order.shipping = Decimal(self.shipping()) if not empty(self.shipping()) else Decimal(0.00)
|
order.shipping = Decimal(self.shipping()) if not empty(self.shipping()) else Decimal(0.00)
|
||||||
order.total =Decimal(self.grand_total()) if not empty(self.grand_total()) else Decimal(0.00)
|
order.total = Decimal(self.grand_total()) if not empty(self.grand_total()) else Decimal(0.00)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def order_date(self):
|
def order_date(self):
|
||||||
|
|
@ -250,7 +254,7 @@ class OrderOldPage(OrderPage):
|
||||||
'%d %m %Y')
|
'%d %m %Y')
|
||||||
|
|
||||||
def order_number(self):
|
def order_number(self):
|
||||||
num_com = u' '.join(self.doc.xpath(
|
num_com = u' '.join(self.doc.xpath(
|
||||||
u'//b[contains(text(),"Amazon.fr numéro de commande")]/../text()')
|
u'//b[contains(text(),"Amazon.fr numéro de commande")]/../text()')
|
||||||
).strip()
|
).strip()
|
||||||
return num_com
|
return num_com
|
||||||
|
|
@ -259,12 +263,12 @@ class OrderOldPage(OrderPage):
|
||||||
return self.sum_amounts(u'TVA:')
|
return self.sum_amounts(u'TVA:')
|
||||||
|
|
||||||
def discount(self):
|
def discount(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
return self.sum_amounts(u'Subscribe & Save:', u'Bon de réduction:',
|
return self.sum_amounts(u'Subscribe & Save:', u'Bon de réduction:',
|
||||||
u'Promotion Applied:', u'Your Coupon Savings:')
|
u'Promotion Applied:', u'Your Coupon Savings:')
|
||||||
|
|
||||||
def shipping(self):
|
def shipping(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
return self.sum_amounts(u'Shipping & Handling:', u'Free shipping:',
|
return self.sum_amounts(u'Shipping & Handling:', u'Free shipping:',
|
||||||
u'Free Shipping:')
|
u'Free Shipping:')
|
||||||
|
|
||||||
|
|
@ -291,7 +295,7 @@ class OrderOldPage(OrderPage):
|
||||||
break
|
break
|
||||||
|
|
||||||
def shipments(self):
|
def shipments(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
for cue in (u'Shipment #', u'Subscribe and Save Shipment'):
|
for cue in (u'Shipment #', u'Subscribe and Save Shipment'):
|
||||||
for shmt in self.doc.xpath('//b[contains(text(),"%s")]' % cue):
|
for shmt in self.doc.xpath('//b[contains(text(),"%s")]' % cue):
|
||||||
yield shmt
|
yield shmt
|
||||||
|
|
@ -325,8 +329,8 @@ class OrderOldPage(OrderPage):
|
||||||
yield itm
|
yield itm
|
||||||
|
|
||||||
def sum_amounts(self, *names):
|
def sum_amounts(self, *names):
|
||||||
return sum(self.amount(shmt,x) for shmt in self.shipments()
|
return sum(self.amount(shmt, x) for shmt in self.shipments()
|
||||||
for x in names)
|
for x in names)
|
||||||
|
|
||||||
def amount(self, shmt, name):
|
def amount(self, shmt, name):
|
||||||
for root in shmt.xpath(u'../../../../../../../..'
|
for root in shmt.xpath(u'../../../../../../../..'
|
||||||
|
|
@ -340,22 +344,22 @@ class OrderOldPage(OrderPage):
|
||||||
return Decimal(0)
|
return Decimal(0)
|
||||||
|
|
||||||
def gift(self, shmt):
|
def gift(self, shmt):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
return self.amount(shmt, u'Gift Card Amount:')
|
return self.amount(shmt, u'Gift Card Amount:')
|
||||||
|
|
||||||
def paymethods(self):
|
def paymethods(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
root = self.doc.xpath('//b[text()="Payment Method: "]/..')
|
root = self.doc.xpath('//b[text()="Payment Method: "]/..')
|
||||||
if len(root) == 0:
|
if len(root) == 0:
|
||||||
return
|
return
|
||||||
root = root[0]
|
root = root[0]
|
||||||
text = root.text_content().strip()
|
text = root.text_content().strip()
|
||||||
while text:
|
while text:
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
for pattern in [
|
for pattern in [
|
||||||
u'^.*Payment Method:',
|
u'^.*Payment Method:',
|
||||||
u'^([^\n]+)\n +\| Last digits: +([0-9]+)\n',
|
u'^([^\n]+)\n +\| Last digits: +([0-9]+)\n',
|
||||||
u'^Gift Card\n', # Skip gift card.
|
u'^Gift Card\n', # Skip gift card.
|
||||||
u'^Billing address.*$']:
|
u'^Billing address.*$']:
|
||||||
match = re.match(pattern, text, re.DOTALL+re.MULTILINE)
|
match = re.match(pattern, text, re.DOTALL+re.MULTILINE)
|
||||||
if match:
|
if match:
|
||||||
|
|
@ -367,7 +371,7 @@ class OrderOldPage(OrderPage):
|
||||||
break
|
break
|
||||||
|
|
||||||
def transactions(self):
|
def transactions(self):
|
||||||
# TODO : French translation
|
# TODO : French translation
|
||||||
for tr in self.doc.xpath(
|
for tr in self.doc.xpath(
|
||||||
u'//div[contains(b,"Credit Card transactions")]'
|
u'//div[contains(b,"Credit Card transactions")]'
|
||||||
u'/following-sibling::table[1]/tr'):
|
u'/following-sibling::table[1]/tr'):
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from .fr.browser import AmazonFR
|
||||||
|
|
||||||
__all__ = ['AmazonModule']
|
__all__ = ['AmazonModule']
|
||||||
|
|
||||||
|
|
||||||
class AmazonModule(Module, CapShop):
|
class AmazonModule(Module, CapShop):
|
||||||
NAME = 'amazon'
|
NAME = 'amazon'
|
||||||
MAINTAINER = u'Oleg Plakhotniuk'
|
MAINTAINER = u'Oleg Plakhotniuk'
|
||||||
|
|
@ -63,7 +64,6 @@ class AmazonModule(Module, CapShop):
|
||||||
return self.browser.get_order(id_)
|
return self.browser.get_order(id_)
|
||||||
|
|
||||||
def iter_orders(self):
|
def iter_orders(self):
|
||||||
|
|
||||||
return self.browser.iter_orders()
|
return self.browser.iter_orders()
|
||||||
|
|
||||||
def iter_payments(self, order):
|
def iter_payments(self, order):
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class HistoryPage(AmazonPage):
|
||||||
@pagination
|
@pagination
|
||||||
def iter_orders(self):
|
def iter_orders(self):
|
||||||
for id_ in self.doc.xpath(
|
for id_ in self.doc.xpath(
|
||||||
u'//span[contains(text(),"Order #")]/../span[2]/text()'):
|
u'//span[contains(text(),"Order #")]/../span[2]/text()'):
|
||||||
yield self.browser.to_order(id_.strip())
|
yield self.browser.to_order(id_.strip())
|
||||||
for next_ in self.doc.xpath(u'//ul[@class="a-pagination"]'
|
for next_ in self.doc.xpath(u'//ul[@class="a-pagination"]'
|
||||||
u'//a[contains(text(),"Next")]/@href'):
|
u'//a[contains(text(),"Next")]/@href'):
|
||||||
|
|
@ -71,8 +71,8 @@ class HistoryPage(AmazonPage):
|
||||||
|
|
||||||
def opt_years(self):
|
def opt_years(self):
|
||||||
return [x for x in self.doc.xpath(
|
return [x for x in self.doc.xpath(
|
||||||
'//select[@name="orderFilter"]/option/@value'
|
'//select[@name="orderFilter"]/option/@value'
|
||||||
) if x.startswith('year-')]
|
) if x.startswith('year-')]
|
||||||
|
|
||||||
|
|
||||||
class OrderPage(AmazonPage):
|
class OrderPage(AmazonPage):
|
||||||
|
|
@ -82,9 +82,9 @@ class OrderPage(AmazonPage):
|
||||||
# Payment for not yet shipped orders may change, and is not always
|
# Payment for not yet shipped orders may change, and is not always
|
||||||
# available.
|
# available.
|
||||||
return bool([x for s in [u'Not Yet Shipped', u'Not yet shipped',
|
return bool([x for s in [u'Not Yet Shipped', u'Not yet shipped',
|
||||||
u'Preparing for Shipment', u'Shipping now', u'In transit',
|
u'Preparing for Shipment', u'Shipping now', u'In transit',
|
||||||
u'On the way']
|
u'On the way']
|
||||||
for x in self.doc.xpath(u'//*[contains(text(),"%s")]' % s)])
|
for x in self.doc.xpath(u'//*[contains(text(),"%s")]' % s)])
|
||||||
|
|
||||||
|
|
||||||
class OrderNewPage(OrderPage):
|
class OrderNewPage(OrderPage):
|
||||||
|
|
@ -165,8 +165,8 @@ class OrderNewPage(OrderPage):
|
||||||
|
|
||||||
def amount(self, *names):
|
def amount(self, *names):
|
||||||
return Decimal(sum(AmTr.decimal_amount(amount.strip())
|
return Decimal(sum(AmTr.decimal_amount(amount.strip())
|
||||||
for n in names for amount in self.doc.xpath(
|
for n in names for amount in self.doc.xpath(
|
||||||
'(//span[contains(text(),"%s:")]/../..//span)[2]/text()' % n)))
|
'(//span[contains(text(),"%s:")]/../..//span)[2]/text()' % n)))
|
||||||
|
|
||||||
def transactions(self):
|
def transactions(self):
|
||||||
for row in self.doc.xpath('//span[contains(text(),"Transactions")]'
|
for row in self.doc.xpath('//span[contains(text(),"Transactions")]'
|
||||||
|
|
@ -204,7 +204,7 @@ class OrderNewPage(OrderPage):
|
||||||
price *= Decimal(amount)
|
price *= Decimal(amount)
|
||||||
if url:
|
if url:
|
||||||
url = unicode(self.browser.BASEURL) + \
|
url = unicode(self.browser.BASEURL) + \
|
||||||
re.match(u'(/gp/product/.*)/ref=.*', url).group(1)
|
re.match(u'(/gp/product/.*)/ref=.*', url).group(1)
|
||||||
if label and price:
|
if label and price:
|
||||||
itm = Item()
|
itm = Item()
|
||||||
itm.label = label
|
itm.label = label
|
||||||
|
|
@ -240,7 +240,7 @@ class OrderOldPage(OrderPage):
|
||||||
|
|
||||||
def discount(self):
|
def discount(self):
|
||||||
return self.sum_amounts(u'Subscribe & Save:', u'Promotion applied:',
|
return self.sum_amounts(u'Subscribe & Save:', u'Promotion applied:',
|
||||||
u'Promotion Applied:', u'Your Coupon Savings:')
|
u'Promotion Applied:', u'Your Coupon Savings:')
|
||||||
|
|
||||||
def shipping(self):
|
def shipping(self):
|
||||||
return self.sum_amounts(u'Shipping & Handling:', u'Free shipping:',
|
return self.sum_amounts(u'Shipping & Handling:', u'Free shipping:',
|
||||||
|
|
@ -302,8 +302,8 @@ class OrderOldPage(OrderPage):
|
||||||
yield itm
|
yield itm
|
||||||
|
|
||||||
def sum_amounts(self, *names):
|
def sum_amounts(self, *names):
|
||||||
return sum(self.amount(shmt,x) for shmt in self.shipments()
|
return sum(self.amount(shmt, x) for shmt in self.shipments()
|
||||||
for x in names)
|
for x in names)
|
||||||
|
|
||||||
def amount(self, shmt, name):
|
def amount(self, shmt, name):
|
||||||
for root in shmt.xpath(u'../../../../../../../..'
|
for root in shmt.xpath(u'../../../../../../../..'
|
||||||
|
|
@ -326,7 +326,7 @@ class OrderOldPage(OrderPage):
|
||||||
for pattern in [
|
for pattern in [
|
||||||
u'^.*Payment Method:',
|
u'^.*Payment Method:',
|
||||||
u'^([^\n]+)\n +\| Last digits: +([0-9]+)\n',
|
u'^([^\n]+)\n +\| Last digits: +([0-9]+)\n',
|
||||||
u'^Gift Card\n', # Skip gift card.
|
u'^Gift Card\n', # Skip gift card.
|
||||||
u'^Billing address.*$']:
|
u'^Billing address.*$']:
|
||||||
match = re.match(pattern, text, re.DOTALL+re.MULTILINE)
|
match = re.match(pattern, text, re.DOTALL+re.MULTILINE)
|
||||||
if match:
|
if match:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue