From 2562e6df967ed7e638ed068e86fe9eae7275628c Mon Sep 17 00:00:00 2001 From: Greg Smith Date: Thu, 25 Jan 2018 21:07:00 -0600 Subject: [PATCH 1/6] Updated for BGG 2018 --- README.rst | 3 + bggcli/__init__.py | 96 +++++++ bggcli/commands/collection_import.py | 66 ++++- bggcli/ui/__init__.py | 21 +- bggcli/ui/gamepage.py | 415 ++++++++++++++++++++++----- 5 files changed, 519 insertions(+), 82 deletions(-) diff --git a/README.rst b/README.rst index ed139ca..adcb1df 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,10 @@ Introduction Only 3 operations are implemented at this time: +Updated for BGG 2018 * bulk import/update for your game collection from a CSV file + +Not tested with BGG 2018 * bulk delete from a CSV file * bulk export as a CSV file, WITH version information (game's version is missing in the default export) diff --git a/bggcli/__init__.py b/bggcli/__init__.py index eac5cdf..30f8a80 100644 --- a/bggcli/__init__.py +++ b/bggcli/__init__.py @@ -1,3 +1,6 @@ +# Updated BGG_SUPPORTED_FIELDS +# Added BGG_EXPORTED_FIELDS and BGG_GAMEDB_FIELDS + import os if os.environ.get('CI') == 'true': @@ -18,3 +21,96 @@ 'pp_currency', 'currvalue', 'cv_currency', 'acquisitiondate', 'acquiredfrom', 'quantity', 'privatecomment', '_versionid'] + +BGG_SUPPORTED_FIELDS = ['own', + 'want', 'wanttobuy', 'wanttoplay', 'prevowned', + 'preordered', + 'fortrade', 'conditiontext', # these must be in this order + 'wishlist', 'wishlistpriority', 'wishlistcomment', # these must be in this order + 'comment', 'rating', + 'pricepaid', 'currvalue', + 'acquisitiondate', 'acquiredfrom', 'quantity', 'privatecomment', + 'haspartslist', 'wantpartslist','publisherid', 'imageid', + 'year', 'language', 'other', + #'cv_currency', 'pp_currency', + 'objectname', + 'objectid', '_versionid', 'invlocation' + ] + +# More fields in the add/edit collection dialog: +# Inventory Date +# Inventory Location +# Barcode +# Aquisition Date +# Inventory Date +# Inventory Location +# Custom Title +# Custom Image Id +# Publisher Id +# Year +# Other +# Barcode + +# list of headings in direct downloaded collection.csv +BGG_EXPORTED_FIELDS = [ +'objectname', +'objectid', +'rating', +'numplays', +'weight', +'own', +'fortrade', +'want', +'wanttobuy', +'wanttoplay', +'prevowned', +'preordered', +'wishlist', +'wishlistpriority', +'wishlistcomment', +'comment', +'conditiontext', +'haspartslist', +'wantpartslist', +'numowned', +'publisherid', +'imageid', +'year', +'language', +'other', +'pricepaid', +'pp_currency', +'currvalue', +'cv_currency', +'acquisitiondate', +'acquiredfrom', +'quantity', +'privatecomment', +'version_publishers', +'version_languages', +'version_yearpublished', +'version_nickname', +] + +BGG_GAMEDB_FIELDS = [ +'collid', +'baverage', +'average', +'avgweight', +'rank', +'objecttype', +'originalname', +'minplayers', +'maxplayers', +'playingtime', +'maxplaytime', +'minplaytime', +'yearpublished', +'bggrecplayers', +'bggbestplayers', +'bggrecagerange', +'bgglanguagedependence', +'itemtype', +] + +#print(set(BGG_EXPORTED_FIELDS)-set(BGG_SUPPORTED_FIELDS)) diff --git a/bggcli/commands/collection_import.py b/bggcli/commands/collection_import.py index cdaaf65..870c252 100644 --- a/bggcli/commands/collection_import.py +++ b/bggcli/commands/collection_import.py @@ -22,6 +22,7 @@ Arguments: The CSV file with games to import """ +# Updated for BGG 2018 import sys from bggcli.commands import check_file from bggcli.ui.gamepage import GamePage @@ -29,23 +30,80 @@ from bggcli.util.csvreader import CsvReader from bggcli.util.logger import Logger from bggcli.util.webdriver import WebDriver +import traceback - +LOOPLIMIT = 30 def execute(args, options): + print('Executing!') login = args['--login'] file_path = check_file(args) csv_reader = CsvReader(file_path) csv_reader.open() - + rows = [] + try: + csv_reader.iterate(lambda row: rows.append(row)) + except: + pass + + print len(rows) + # interestingrows=rows[130:] + # print len(interestingrows) + #sys.exit() + Logger.info("Importing games for '%s' account..." % login) with WebDriver('collection-import', args, options) as web_driver: if not LoginPage(web_driver.driver).authenticate(login, args['--password']): sys.exit(1) - Logger.info("Importing %s games..." % csv_reader.rowCount) + Logger.info("Importing %s games to collection..." % csv_reader.rowCount) game_page = GamePage(web_driver.driver) - csv_reader.iterate(lambda row: game_page.update(row)) + #csv_reader.iterate(lambda row: game_page.update(row)) + #badrows = [] + #rows = rows[:5] + rows.reverse() + firstrow = rows[0] + loop = 1 + Logger.info('Loop {}'.format(loop)) + if loop > LOOPLIMIT: + Logger.info("Loop limit of {} reached.".format(loop)) + return + while len(rows): + row = rows.pop() + Logger.info('Name: {}'.format(row['objectname'])) + + if firstrow is None or firstrow == row: + loop += 1 + Logger.info('Loop {}'.format(loop)) + if rows: + firstrow = rows[0] + Logger.info('First assigned {}'.format(firstrow['objectname'])) + else: + firstrow = None + Logger.info('First assigned None') + try: + val = game_page.update(row) + Logger.info('update returned {}'.format(val)) + + if val: + Logger.info('Updated!') + else: + Logger.info('returned False??, back in queue.') + rows.insert(0,row) + + except Exception as e: + traceback.print_exc(limit=2, file=sys.stdout) + + Logger.info('Exception occurred, back in queue.') + rows.insert(0,row) + + #badrows.append(row) + # for row in rows: + # try: + # game_page.update(row) + # except: + # badrows.append(row) + # print Logger.info("Import has finished.") diff --git a/bggcli/ui/__init__.py b/bggcli/ui/__init__.py index 027e8ec..aa4056b 100644 --- a/bggcli/ui/__init__.py +++ b/bggcli/ui/__init__.py @@ -1,3 +1,6 @@ +# Modified update_select() by adding by_index parameter +# Modified timeout from 5 to 15 seconds + import os from selenium.webdriver.common.by import By @@ -14,7 +17,7 @@ def __init__(self, driver): if os.environ.get('CI') == 'true': timeout = 60 else: - timeout = 5 + timeout = 15 self.wait = WebDriverWait(driver, timeout) @staticmethod @@ -29,22 +32,24 @@ def update_checkbox(root_el, xpath, value): elem.click() @staticmethod - def update_select(el, value, by_text=False): + def update_select(el, value, by_text=False, by_index=False): select = Select(el) if value == '': select.select_by_index(0) + elif by_index: + select.select_by_index(value) elif by_text: select.select_by_visible_text(value) else: select.select_by_value(value) def update_textarea(self, root_el, fieldname, value): - root_el.find_element_by_xpath(".//td[@class='collection_%smod editfield']" % fieldname) \ - .click() - form = self.wait.until(EC.element_to_be_clickable( - (By.XPATH, ".//form[contains(@id, '%s')]" % fieldname))) - self.update_text(form.find_element_by_xpath(".//textarea"), value) - form.find_element_by_xpath(".//input[contains(@onclick, 'CE_SaveData')]").click() + #root_el.find_element_by_xpath(".//td[@class='collection_%smod editfield']" % fieldname) \ + # .click() + input = self.wait.until(EC.element_to_be_clickable( + (By.XPATH, ".//textarea[contains(@id, '%s')]" % fieldname))) + self.update_text(input, value) + #form.find_element_by_xpath(".//input[contains(@onclick, 'CE_SaveData')]").click() def wait_and_accept_alert(self): self.wait.until(EC.alert_is_present()) diff --git a/bggcli/ui/gamepage.py b/bggcli/ui/gamepage.py index 227ff78..c9a2e94 100644 --- a/bggcli/ui/gamepage.py +++ b/bggcli/ui/gamepage.py @@ -1,3 +1,7 @@ +#!/usr/local/bin/python +# -*- coding: utf-8 -*- + +# Updated for BGG 2018 """ bgg.gamepage ~~~~~~~~~~~~ @@ -5,21 +9,23 @@ Selenium Page Object to bind the game details page """ - +import time +import inspect from selenium.common.exceptions import NoSuchElementException, WebDriverException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.action_chains import ActionChains from bggcli import BGG_BASE_URL, BGG_SUPPORTED_FIELDS from bggcli.ui import BasePage from bggcli.util.logger import Logger - def in_private_info_popup(func): """ Ensure the Private Info popup is displayed when updating its attributes """ - + # Not verified for BGG 2018 + def _wrapper(self, *args, **kwargs): try: self.itemEl \ @@ -40,7 +46,7 @@ def in_version_popup(func): """ Ensure the Version popup is displayed when updating its attributes """ - + # Not verified for BGG 2018 def _wrapper(self, *args, **kwargs): try: self.itemEl \ @@ -69,6 +75,8 @@ class GamePage(BasePage): # 'year', 'language', 'other', 'pricepaid', 'pp_currency', 'currvalue', 'cv_currency', # 'acquisitiondate', 'acquiredfrom', 'quantity', 'privatecomment', '_versionid' # ] + #objectname objectid rating own fortrade want wanttobuy wanttoplay prevowned preordered wishlist wishlistpriority wishlistcomment comment conditiontext haspartslist wantpartslist publisherid imageid year language other pricepaid pp_currency currvalue cv_currency acquisitiondate acquiredfrom quantity privatecomment _versionid + def __init__(self, driver): BasePage.__init__(self, driver) @@ -77,6 +85,7 @@ def __init__(self, driver): self.privateInfoPopupEl = None self.versionPopupEl = None + # Verified BGG 2018 def goto(self, game_attrs): """ Set Web Driver on the game details page @@ -85,31 +94,221 @@ def goto(self, game_attrs): """ self.driver.get("%s/boardgame/%s" % (BGG_BASE_URL, game_attrs['objectid'])) + # Verified BGG 2018 def update(self, game_attrs): + Logger.info("update()", append=True, break_line=True) + + # First, see if objectid exists. + id = game_attrs.get('objectid',None) + + # if objectid doesn't exist, find it via the name: + # this doesn't work at the moment. All the work is done in updateid() + if id is None: + pass + return self.updateid(game_attrs) + + # Verified BGG 2018 + def updateid(self, game_attrs): """ Update game details :param game_attrs: Game attributes as a dictionary """ + # General use of Selenium is + # A) goto page, + # B) find element using xpath expression, then + # C) take action related to the element. + # + # Flow of this function is: + # 1) Go to game page + # 2) Try to click "Add to Collection" button. + # 3) If it didn't exist, + # a) click the "In Collection" button. + # b) click the "Edit" button + # 4) Open the additional two form dialogs + # (Show Advanced, Show Custom) + # 5) fill out items on form, dependencies show which values must + # exist before the indicated item. + # For example, 'wishlistpriority':{'wishlist':1}, + # means that to set wishlistpriority, then + # wishlist must equal 1. + # 6) Finally, find the form element and submit it. + + + Logger.info("updateid()", append=True, break_line=True) + Logger.info("{} {}, ".format(game_attrs.get('objectid',''),game_attrs.get('objectname','')), append=True, break_line=True) + self.goto(game_attrs) - + + Logger.info("page, ", append=True, break_line=False) + + #
+ # + # In Collection + # self.span = self.driver.find_element_by_xpath( + # "//span[@ng-show='colltoolbarctrl.collection.items.length' and contains(.,'In Collection')]") + + # # self.itemEl = self.driver.find_element_by_xpath( + # # "//table[@class='collectionmodule_table']") + # Logger.info(" (in col'n)", append=True, break_line=False) + try: - self.itemEl = self.driver.find_element_by_xpath( - "//table[@class='collectionmodule_table']") - Logger.info(" (already in collection)", append=True, break_line=False) + Logger.info("edit button? ", append=True, break_line=False) + buttonelement = self.driver.find_element_by_xpath( + "(//button[contains(@ng-click, 'colltoolbarctrl.editItem')])[last()]").click() except NoSuchElementException: - self.driver.find_element_by_xpath( - "(//a[contains(@onclick, 'CE_ModuleAddItem')])[last()]").click() - self.itemEl = self.wait.until(EC.element_to_be_clickable( - (By.XPATH, "//table[@class='collectionmodule_table']"))) - + Logger.info(" not found. ", append=True, break_line=False) + Logger.info("(i.e. game in col'n)...", append=True, break_line=False) + # div = self.driver.find_element_by_xpath( + # "(//div[@class, 'toolbar-actions'])[last()]") + + Logger.info("'In Col'n' button? ", append=True, break_line=False) + button = self.driver.find_element_by_xpath( + '(' + '//' + #'div[@is-open="colltoolbarctrl.isOpen" and descendant::' + #'div[@class="dropdown"]' + #'span[@class="toolbar-action"]' + 'button[@id="button-collection" and descendant::' + 'span[starts-with(@ng-show,"colltoolbarctrl.collection.items.length") and contains(text(),"In Collection")]' + ']' + #']' + ')[last()]' + ) + + Logger.info("Click. ", append=True, break_line=False) + button.click() + + + + #time.sleep(2) + # + + #clickable = self.driver.find_element_by_xpath( + #'(//button[contains(text(),"Edit")])[last()]') + clickable = self.driver.find_element_by_xpath( + '//span[@class="collection-dropdown-item-edit" and //button[contains(text(),"Edit")]]') + Logger.info("Click col'n dropdown. ", append=True, break_line=False) + clickable.click() + #time.sleep(10) + # action = ActionChains(self.driver) + # #action=selenium.interactions.Actions(self.driver); + # #import selenium.webdriver.common.actions.pointer_actions + # #selenium.webdriver.common.actions.pointer_actions.click(clickable) + + + # print (clickable.get_attribute('outerHTML').encode("utf-8")) + # #print (clickable.size) + # time.sleep(2) + # action.move_to_element(clickable) + # action.perform() + # time.sleep(1) + # action.click(clickable) + # action.perform() + # time.sleep(10) +# # clickable.click() + # return + + # //*[@id="mainbody"]/div/div[1]/div[2]/div[2]/ng-include/div/ng-include/div/div/div[2]/div[4]/span[2]/div[1]/span[1]/span/span[2]/span/span/span/button + # $x(“//input[starts-with(@name,’btn’)]”) + # + # self.driver.find_element_by_xpath( + # "(//a[contains(@onclick, 'CE_ModuleAddItem')])[last()]").click() + # self.itemEl = self.wait.until(EC.element_to_be_clickable( + # (By.XPATH, "//table[@class='collectionmodule_table']"))) + + + # /html/body/div[2]/div/div + #