From 1f987be2e53febdd35dbda14695bae1c3243aa30 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Fri, 2 May 2025 22:02:38 +0200 Subject: [PATCH 1/3] initial refactoring with adapters --- setup.py | 1 + src/redturtle/rsync/adapters/__init__.py | 0 src/redturtle/rsync/adapters/adapter.py | 162 +++++++ src/redturtle/rsync/adapters/configure.zcml | 5 + src/redturtle/rsync/configure.zcml | 1 + src/redturtle/rsync/interfaces.py | 65 ++- src/redturtle/rsync/scripts/rsync.py | 471 +++++++++++--------- 7 files changed, 503 insertions(+), 202 deletions(-) create mode 100644 src/redturtle/rsync/adapters/__init__.py create mode 100644 src/redturtle/rsync/adapters/adapter.py create mode 100644 src/redturtle/rsync/adapters/configure.zcml diff --git a/setup.py b/setup.py index 24aee6c..ceb7013 100644 --- a/setup.py +++ b/setup.py @@ -73,5 +73,6 @@ target = plone [console_scripts] update_locale = redturtle.rsync.locales.update:update_locale + redturtle_rsync = redturtle.rsync.scripts.rsync:main """, ) diff --git a/src/redturtle/rsync/adapters/__init__.py b/src/redturtle/rsync/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/redturtle/rsync/adapters/adapter.py b/src/redturtle/rsync/adapters/adapter.py new file mode 100644 index 0000000..d3d6bcf --- /dev/null +++ b/src/redturtle/rsync/adapters/adapter.py @@ -0,0 +1,162 @@ +from pathlib import Path +from redturtle.rsync.interfaces import IRedturtleRsyncAdapter +from redturtle.rsync.interfaces import IRedturtleRsyncAdapter +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry +from zope.component import adapter +from zope.interface import implementer +from zope.interface import Interface +from zope.interface import Interface + +import json +import requests + + +class TimeoutHTTPAdapter(HTTPAdapter): + def __init__(self, *args, **kwargs): + if "timeout" in kwargs: + self.timeout = kwargs["timeout"] + del kwargs["timeout"] + super(TimeoutHTTPAdapter, self).__init__(*args, **kwargs) + + def send(self, request, **kwargs): + timeout = kwargs.get("timeout") + if timeout is None: + kwargs["timeout"] = self.timeout + return super(TimeoutHTTPAdapter, self).send(request, **kwargs) + + +@implementer(IRedturtleRsyncAdapter) +@adapter(Interface, Interface) +class RsyncAdapterBase: + """ + This is the base class for all rsync adapters. + It provides a common interface for all adapters and some default + implementations of the methods. + Default methods works with some data in restapi-like format. + """ + + def __init__(self, context, request): + self.context = context + self.request = request + + def requests_retry_session( + self, + retries=3, + backoff_factor=0.3, + status_forcelist=(500, 501, 502, 503, 504), + timeout=5.0, + session=None, + ): + """ + https://dev.to/ssbozy/python-requests-with-retries-4p03 + """ + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + # adapter = HTTPAdapter(max_retries=retry) + http_adapter = TimeoutHTTPAdapter(max_retries=retry, timeout=timeout) + session.mount("http://", http_adapter) + session.mount("https://", http_adapter) + return session + + def log_item_title(self, start): + """ + Return the title of the log item for the rsync command. + """ + return f"Report sync {start.isoformat()}" + + def set_args(self, parser): + """ + Set some additional arguments for the rsync command. + + For example: + parser.add_argument( + "--import-type", + choices=["xxx", "yyy", "zzz"], + help="Import type", + ) + """ + return + + def get_data(self, options): + """ + Convert the data to be used for the rsync command. + Return: + - data: the data to be used for the rsync command + - error: an error message if there was an error, None otherwise + """ + error = None + data = None + # first, read source data + if getattr(options, "source_path", None): + file_path = Path(options.source_path) + if file_path.exists() and file_path.is_file(): + with open(file_path, "r") as f: + try: + data = json.load(f) + except json.JSONDecodeError as e: + data = f.read() + else: + error = f"Source file not found in: {file_path}" + return data, error + elif getattr(options, "source_url", None): + http = self.requests_retry_session(retries=7, timeout=30.0) + response = http.get(options.source_url) + if response.status_code != 200: + error = f"Error getting data from {options.source_url}: {response.status_code}" + return data, error + if "application/json" in response.headers.get("Content-Type", ""): + try: + data = response.json() + except ValueError: + data = response.content + else: + data = response.content + + if data: + data, error = self.convert_source_data(data) + return data, error + + def convert_source_data(self, data): + """ + If needed, convert the source data to a format that can be used by the rsync command. + """ + return data, None + + def find_item_from_row(self, row): + """ + Find the item in the context from the given row of data. + This method should be implemented by subclasses to find the specific type of content item. + """ + raise NotImplementedError() + + def create_item(self, row): + """ + Create a new content item from the given row of data. + This method should be implemented by subclasses to create the specific type of content item. + """ + raise NotImplementedError() + + def update_item(self, item, row): + """ + Update an existing content item from the given row of data. + This method should be implemented by subclasses to update the specific type of content item. + """ + raise NotImplementedError() + + def delete_items(self, data, sync_uids): + """ + params: + - data: the data to be used for the rsync command + - sync_uids: the uids of the items thata has been updated + + Delete items if needed. + This method should be implemented by subclasses to delete the specific type of content item. + """ + raise NotImplementedError() diff --git a/src/redturtle/rsync/adapters/configure.zcml b/src/redturtle/rsync/adapters/configure.zcml new file mode 100644 index 0000000..a897690 --- /dev/null +++ b/src/redturtle/rsync/adapters/configure.zcml @@ -0,0 +1,5 @@ + + + + diff --git a/src/redturtle/rsync/configure.zcml b/src/redturtle/rsync/configure.zcml index 5577d89..14bc01b 100644 --- a/src/redturtle/rsync/configure.zcml +++ b/src/redturtle/rsync/configure.zcml @@ -16,6 +16,7 @@ + \1', + text, + re.MULTILINE | re.DOTALL, + ) + + def log_info(self, msg, type="info"): + """ + append a message to the logdata list and print it. + """ + style = "" + if type == "error": + style = "padding:5px;background-color:red;color:#fff" + msg = f"[{datetime.now()}] {msg}" + self.logdata.append(f'

{self.autolink(msg)}

') + + # print the message + if type == "error": + logger.error(msg) + elif type == "warning": + logger.warning(msg) else: - # TODO: verifica sulle date di aggiornamento della pagina remota vs. locale - data = extractor(container, response, **kwargs) - if verbose: - # TODO - logger.warning('DEBUG: %s', data) - if data: - # default: se non ci sono dati di ultima modifica non si fanno - # modifiche - update = False - if 'modification_date' in data: - # BBB: le due date devono esistere ed essere entrambe DateTime - update = (data['modification_date'] > obj.modification_date) - if update or force_update: - return updater(obj, data, **kwargs) - else: - # se la pagina remota non ha i metadati e come se fosse stata cancellate - # quindi va cancllata anche quella locale - return deleter(obj) - return obj - else: - # create - if not response: - logger.error('unable to fetch %s (%s)', remoteurl, response.status_code) + if self.options.verbose: + logger.info(msg) + + def write_log(self): + """ + Write the log into the database. + """ + logpath = getattr(self.options, "logpath", None) + if not logpath: + logger.warning("No logpath specified, skipping log write into database.") + return + logcontainer = api.content.get(logpath) + if not logcontainer: + logger.warning( + f'Log container not found with path "{logpath}", skipping log write into database.' + ) + return + description = f"{self.n_items} elementi trovati, {self.n_created} creati, {self.n_updated} aggiornati, {self.n_todelete} da eliminare" + blockid = str(uuid.uuid4()) + api.content.create( + logcontainer, + "Document", + title=self.adapter.log_item_title(start=self.start), + description=description, + blocks={ + blockid: { + "@type": "html", + "html": "\n".join(self.logdata), + } + }, + blocks_layout={ + "items": [blockid], + }, + ) + + def get_data(self): + """ + get the data from the adapter. + + The adapter should return: + - data: the data to be used for the rsync command + - error: an error message if there was an error in the data generation + """ + try: + data, error = self.adapter.get_data(options=self.options) + except Exception as e: + msg = f"Error in data generation: {e}" + self.log_info(msg=msg, type="error") + return None + if error: + msg = f"Error in data generation: {error}" + self.log_info(msg=msg, type="error") return None + if not data: + msg = "No data to sync." + self.log_info(msg=msg, type="error") + return None + return data + + def create_item(self, row): + """ + Create the item. + """ + try: + res = self.adapter.create_item(row) + except Exception as e: + msg = f"[Error] Unable to create item {row}: {e}" + self.log_info(msg=msg, type="error") + return + if not res: + msg = f"[Error] item {row} not created." + self.log_info(msg=msg, type="error") + return + + # adapter could create a list of items (maybe also children or related items) + if isinstance(res, list): + self.n_created += len(res) + for item in res: + msg = f"[CREATED] {item.absolute_url()}" + self.log_info(msg=msg) else: - data = extractor(container, response, **kwargs) - if data: - obj = creator(container, data, id=remoteid, **kwargs) - return obj - - -""" -# ESEMPIO: ALMA2021 vs. Magazine -from unibo.api.rsync import rsync -remoteurl = 'http://magazine.dev.dsaw.unibo.it/archivio/2018/mio-articolo-con-il-nuovo-font' -remoteid = '6fc2a87d4aa64cc7ad6b5bd0838a4c0c' # AKA http://magazine.dev.dsaw.unibo.it/archivio/2018/mio-articolo-con-il-nuovo-font/uuid - -def magazine_extractor(response, lang): - data = extruct.extract(response.text) - return data - -container = api.content.get('/alma2021/it/notizie') -obj_it = rsync(container, remoteid, remoteurl, extractor=magazine_extractor, lang='it') -container = api.content.get('/alma2021/en/news') -obj_en = rsync(container, remoteid, remoteurl, extractor=magazine_extractor, lang='en') -""" + self.n_created += 1 + msg = f"[CREATED] {item.absolute_url()}" + self.log_info(msg=msg) + return res + + def update_item(self, item, row): + """ + Update the item. + """ + try: + res = self.adapter.update_item(item=item, row=row) + except Exception as e: + msg = f"[Error] Unable to update item {item.absolute_url()}: {e}" + self.log_info(msg=msg, type="error") + return + + if not res: + msg = f"[SKIPPED] {item.absolute_url()}" + self.log_info(msg=msg) + return + + # adapter could create a list of items (maybe also children or related items) + if isinstance(res, list): + self.n_updated += len(res) + for updated in res: + msg = f"[UPDATED] {updated.absolute_url()}" + self.log_info(msg=msg) + self.sync_uids.add(updated.UID()) + else: + self.n_updated += 1 + msg = f"[UPDATED] {item.absolute_url()}" + self.log_info(msg=msg) + self.sync_uids.add(item.UID()) + + def delete_items(self, data): + """ + See if there are items to delete. + """ + res = self.adapter.delete_items(data=data, sync_uids=self.sync_uids) + if not res: + return + if isinstance(res, list): + self.n_todelete += len(res) + for item in res: + msg = f"[DELETED] {item}" + self.log_info(msg=msg) + else: + self.n_todelete += 1 + msg = f"[DELETED] {res}" + self.log_info(msg=msg) + + def rsync(self): + """ + Do the rsync. + """ + self.start = datetime.now() + logger.info(f"[{self.start}] - START RSYNC") + data = self.get_data() + if not data: + # we already logged the error + logger.info(f"[{datetime.now()}] - END RSYNC") + return + + self.n_items = len(data) + self.log_info(msg=f"START - ITERATE DATA ({self.n_items} items)") + + last_commit = 0 + i = 0 + for row in data: + i += 1 + item = self.adapter.find_item_from_row(row) + if not item: + self.create_item(row=row) + else: + self.update_item(item=item, row=row) + + if self.n_updated + self.n_created - last_commit > 5: + last_commit = self.n_updated + self.n_created + if not getattr(self.options, "dry_run", False): + logger.info( + f"[{datetime.now()}] COMMIT ({i}/{self.n_items} items processed)" + ) + transaction.commit() + + self.delete_items(data) + + +def _main(args): + + runner = ScriptRunner(args=args) + runner.rsync() + runner.write_log() + if not getattr(runner.options, "dry_run", False): + print(f"[{datetime.now()}] COMMIT") + transaction.commit() + + +def main(): + _main(sys.argv[3:]) + + +if __name__ == "__main__": + main() From 54bc7cfdafa511e6ca43210fcef6668a623caed6 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Tue, 13 May 2025 16:00:23 +0200 Subject: [PATCH 2/3] some improvements --- src/redturtle/rsync/__init__.py | 2 +- src/redturtle/rsync/adapters/adapter.py | 12 ++-- src/redturtle/rsync/adapters/configure.zcml | 9 ++- src/redturtle/rsync/browser/configure.zcml | 10 ++- src/redturtle/rsync/configure.zcml | 7 +- src/redturtle/rsync/interfaces.py | 2 +- src/redturtle/rsync/locales/update.py | 26 +++---- src/redturtle/rsync/permissions.zcml | 9 +-- src/redturtle/rsync/scripts/rsync.py | 79 +++++++++++++-------- src/redturtle/rsync/setuphandlers.py | 1 - src/redturtle/rsync/testing.py | 11 +-- src/redturtle/rsync/tests/test_robot.py | 21 +++--- src/redturtle/rsync/tests/test_setup.py | 29 ++++---- 13 files changed, 122 insertions(+), 96 deletions(-) diff --git a/src/redturtle/rsync/__init__.py b/src/redturtle/rsync/__init__.py index b077e2b..6c8bab9 100644 --- a/src/redturtle/rsync/__init__.py +++ b/src/redturtle/rsync/__init__.py @@ -3,4 +3,4 @@ from zope.i18nmessageid import MessageFactory -_ = MessageFactory('redturtle.rsync') +_ = MessageFactory("redturtle.rsync") diff --git a/src/redturtle/rsync/adapters/adapter.py b/src/redturtle/rsync/adapters/adapter.py index d3d6bcf..6f26c19 100644 --- a/src/redturtle/rsync/adapters/adapter.py +++ b/src/redturtle/rsync/adapters/adapter.py @@ -1,12 +1,10 @@ from pathlib import Path from redturtle.rsync.interfaces import IRedturtleRsyncAdapter -from redturtle.rsync.interfaces import IRedturtleRsyncAdapter from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry from zope.component import adapter from zope.interface import implementer from zope.interface import Interface -from zope.interface import Interface import json import requests @@ -65,11 +63,11 @@ def requests_retry_session( session.mount("https://", http_adapter) return session - def log_item_title(self, start): + def log_item_title(self, start, options): """ Return the title of the log item for the rsync command. """ - return f"Report sync {start.isoformat()}" + return f"Report sync {start.strftime('%d-%m-%Y %H:%M:%S')}" def set_args(self, parser): """ @@ -100,7 +98,7 @@ def get_data(self, options): with open(file_path, "r") as f: try: data = json.load(f) - except json.JSONDecodeError as e: + except json.JSONDecodeError: data = f.read() else: error = f"Source file not found in: {file_path}" @@ -136,7 +134,7 @@ def find_item_from_row(self, row): """ raise NotImplementedError() - def create_item(self, row): + def create_item(self, row, options): """ Create a new content item from the given row of data. This method should be implemented by subclasses to create the specific type of content item. @@ -159,4 +157,4 @@ def delete_items(self, data, sync_uids): Delete items if needed. This method should be implemented by subclasses to delete the specific type of content item. """ - raise NotImplementedError() + return diff --git a/src/redturtle/rsync/adapters/configure.zcml b/src/redturtle/rsync/adapters/configure.zcml index a897690..5c0a861 100644 --- a/src/redturtle/rsync/adapters/configure.zcml +++ b/src/redturtle/rsync/adapters/configure.zcml @@ -1,5 +1,8 @@ - + - + diff --git a/src/redturtle/rsync/browser/configure.zcml b/src/redturtle/rsync/browser/configure.zcml index 5327771..3125fde 100644 --- a/src/redturtle/rsync/browser/configure.zcml +++ b/src/redturtle/rsync/browser/configure.zcml @@ -2,10 +2,14 @@ xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:plone="http://namespaces.plone.org/plone" - i18n_domain="redturtle.rsync"> + i18n_domain="redturtle.rsync" + > - + diff --git a/src/redturtle/rsync/configure.zcml b/src/redturtle/rsync/configure.zcml index 14bc01b..a0bf241 100644 --- a/src/redturtle/rsync/configure.zcml +++ b/src/redturtle/rsync/configure.zcml @@ -3,7 +3,8 @@ xmlns:genericsetup="http://namespaces.zope.org/genericsetup" xmlns:i18n="http://namespaces.zope.org/i18n" xmlns:plone="http://namespaces.plone.org/plone" - i18n_domain="redturtle.rsync"> + i18n_domain="redturtle.rsync" + > @@ -22,18 +23,18 @@ diff --git a/src/redturtle/rsync/interfaces.py b/src/redturtle/rsync/interfaces.py index 638e0e9..55cfa16 100644 --- a/src/redturtle/rsync/interfaces.py +++ b/src/redturtle/rsync/interfaces.py @@ -14,7 +14,7 @@ class IRedturtleRsyncAdapter(Interface): def __init__(context, request): """Initialize the adapter with the given context and request.""" - def log_item_title(start): + def log_item_title(start, options): """ Return the title of the log item for the rsync command. """ diff --git a/src/redturtle/rsync/locales/update.py b/src/redturtle/rsync/locales/update.py index ca753e5..70a8da5 100644 --- a/src/redturtle/rsync/locales/update.py +++ b/src/redturtle/rsync/locales/update.py @@ -5,12 +5,12 @@ import subprocess -domain = 'redturtle.rsync' -os.chdir(pkg_resources.resource_filename(domain, '')) -os.chdir('../../../') -target_path = 'src/redturtle/rsync/' -locale_path = target_path + 'locales/' -i18ndude = './bin/i18ndude' +domain = "redturtle.rsync" +os.chdir(pkg_resources.resource_filename(domain, "")) +os.chdir("../../../") +target_path = "src/redturtle/rsync/" +locale_path = target_path + "locales/" +i18ndude = "./bin/i18ndude" # ignore node_modules files resulting in errors excludes = '"*.html *json-schema*.xml"' @@ -18,15 +18,15 @@ def locale_folder_setup(): os.chdir(locale_path) - languages = [d for d in os.listdir('.') if os.path.isdir(d)] + languages = [d for d in os.listdir(".") if os.path.isdir(d)] for lang in languages: folder = os.listdir(lang) - if 'LC_MESSAGES' in folder: + if "LC_MESSAGES" in folder: continue else: - lc_messages_path = lang + '/LC_MESSAGES/' + lc_messages_path = lang + "/LC_MESSAGES/" os.mkdir(lc_messages_path) - cmd = 'msginit --locale={0} --input={1}.pot --output={2}/LC_MESSAGES/{3}.po'.format( # NOQA: E501 + cmd = "msginit --locale={0} --input={1}.pot --output={2}/LC_MESSAGES/{3}.po".format( # NOQA: E501 lang, domain, lang, @@ -37,11 +37,11 @@ def locale_folder_setup(): shell=True, ) - os.chdir('../../../../') + os.chdir("../../../../") def _rebuild(): - cmd = '{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot --exclude {excludes} --create {domain} {target_path}'.format( # NOQA: E501 + cmd = "{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot --exclude {excludes} --create {domain} {target_path}".format( # NOQA: E501 i18ndude=i18ndude, locale_path=locale_path, domain=domain, @@ -55,7 +55,7 @@ def _rebuild(): def _sync(): - cmd = '{0} sync --pot {1}/{2}.pot {3}*/LC_MESSAGES/{4}.po'.format( + cmd = "{0} sync --pot {1}/{2}.pot {3}*/LC_MESSAGES/{4}.po".format( i18ndude, locale_path, domain, diff --git a/src/redturtle/rsync/permissions.zcml b/src/redturtle/rsync/permissions.zcml index 1f79c8a..74de0f4 100644 --- a/src/redturtle/rsync/permissions.zcml +++ b/src/redturtle/rsync/permissions.zcml @@ -1,10 +1,11 @@ + xmlns="http://namespaces.zope.org/zope" + xmlns:zcml="http://namespaces.zope.org/zcml" + i18n_domain="plone" + > - + diff --git a/src/redturtle/rsync/scripts/rsync.py b/src/redturtle/rsync/scripts/rsync.py index 234bb33..7b1f48d 100644 --- a/src/redturtle/rsync/scripts/rsync.py +++ b/src/redturtle/rsync/scripts/rsync.py @@ -8,8 +8,8 @@ import logging import re import sys -import uuid import transaction +import uuid logger = logging.getLogger(__name__) @@ -79,6 +79,18 @@ def autolink(self, text): re.MULTILINE | re.DOTALL, ) + def get_frontend_url(self, item): + frontend_domain = api.portal.get_registry_record( + name="volto.frontend_domain", default="" + ) + if not frontend_domain or frontend_domain == "https://": + frontend_domain = "http://localhost:3000" + if frontend_domain.endswith("/"): + frontend_domain = frontend_domain[:-1] + portal_url = api.portal.get().portal_url() + + return item.absolute_url().replace(portal_url, frontend_domain) + def log_info(self, msg, type="info"): """ append a message to the logdata list and print it. @@ -117,7 +129,7 @@ def write_log(self): api.content.create( logcontainer, "Document", - title=self.adapter.log_item_title(start=self.start), + title=self.adapter.log_item_title(start=self.start, options=self.options), description=description, blocks={ blockid: { @@ -154,12 +166,12 @@ def get_data(self): return None return data - def create_item(self, row): + def create_item(self, row, options): """ Create the item. """ try: - res = self.adapter.create_item(row) + res = self.adapter.create_item(row=row, options=self.options) except Exception as e: msg = f"[Error] Unable to create item {row}: {e}" self.log_info(msg=msg, type="error") @@ -173,27 +185,27 @@ def create_item(self, row): if isinstance(res, list): self.n_created += len(res) for item in res: - msg = f"[CREATED] {item.absolute_url()}" + msg = f"[CREATED] {'/'.join(item.getPhysicalPath())}" self.log_info(msg=msg) else: self.n_created += 1 - msg = f"[CREATED] {item.absolute_url()}" + msg = f"[CREATED] {'/'.join(res.getPhysicalPath())}" self.log_info(msg=msg) return res - def update_item(self, item, row): + def update_item(self, item, row, options): """ Update the item. """ try: - res = self.adapter.update_item(item=item, row=row) + res = self.adapter.update_item(item=item, row=row, options=options) except Exception as e: - msg = f"[Error] Unable to update item {item.absolute_url()}: {e}" + msg = f"[Error] Unable to update item {self.get_frontend_url(item)}: {e}" self.log_info(msg=msg, type="error") return if not res: - msg = f"[SKIPPED] {item.absolute_url()}" + msg = f"[SKIPPED] {self.get_frontend_url(item)}" self.log_info(msg=msg) return @@ -206,7 +218,7 @@ def update_item(self, item, row): self.sync_uids.add(updated.UID()) else: self.n_updated += 1 - msg = f"[UPDATED] {item.absolute_url()}" + msg = f"[UPDATED] {self.get_frontend_url(item)}" self.log_info(msg=msg) self.sync_uids.add(item.UID()) @@ -242,35 +254,42 @@ def rsync(self): self.n_items = len(data) self.log_info(msg=f"START - ITERATE DATA ({self.n_items} items)") - last_commit = 0 + # last_commit = 0 i = 0 - for row in data: + for row in data[:200]: i += 1 - item = self.adapter.find_item_from_row(row) + if i % 100 == 0: + logger.info(f"Progress: {i}/{self.n_items}") + try: + item = self.adapter.find_item_from_row(row=row, options=self.options) + except Exception as e: + msg = f"[Error] Unable to find item from row {row}: {e}" + self.log_info(msg=msg, type="error") + continue if not item: - self.create_item(row=row) + self.create_item(row=row, options=self.options) else: - self.update_item(item=item, row=row) + self.update_item(item=item, row=row, options=self.options) - if self.n_updated + self.n_created - last_commit > 5: - last_commit = self.n_updated + self.n_created - if not getattr(self.options, "dry_run", False): - logger.info( - f"[{datetime.now()}] COMMIT ({i}/{self.n_items} items processed)" - ) - transaction.commit() + # if self.n_updated + self.n_created - last_commit > 5: + # last_commit = self.n_updated + self.n_created + # if not getattr(self.options, "dry_run", False): + # logger.info( + # f"[{datetime.now()}] COMMIT ({i}/{self.n_items} items processed)" + # ) + # transaction.commit() self.delete_items(data) def _main(args): - - runner = ScriptRunner(args=args) - runner.rsync() - runner.write_log() - if not getattr(runner.options, "dry_run", False): - print(f"[{datetime.now()}] COMMIT") - transaction.commit() + with api.env.adopt_user(username="admin"): + runner = ScriptRunner(args=args) + runner.rsync() + runner.write_log() + if not getattr(runner.options, "dry_run", False): + print(f"[{datetime.now()}] COMMIT") + transaction.commit() def main(): diff --git a/src/redturtle/rsync/setuphandlers.py b/src/redturtle/rsync/setuphandlers.py index 2e244de..0543099 100644 --- a/src/redturtle/rsync/setuphandlers.py +++ b/src/redturtle/rsync/setuphandlers.py @@ -5,7 +5,6 @@ @implementer(INonInstallable) class HiddenProfiles(object): - def getNonInstallableProfiles(self): """Hide uninstall profile from site-creation and quickinstaller.""" return [ diff --git a/src/redturtle/rsync/testing.py b/src/redturtle/rsync/testing.py index a525f00..7871cba 100644 --- a/src/redturtle/rsync/testing.py +++ b/src/redturtle/rsync/testing.py @@ -11,7 +11,6 @@ class RedturtleRsyncLayer(PloneSandboxLayer): - defaultBases = (PLONE_FIXTURE,) def setUpZope(self, app, configurationContext): @@ -19,13 +18,15 @@ def setUpZope(self, app, configurationContext): # The z3c.autoinclude feature is disabled in the Plone fixture base # layer. import plone.app.dexterity + self.loadZCML(package=plone.app.dexterity) import plone.restapi + self.loadZCML(package=plone.restapi) self.loadZCML(package=redturtle.rsync) def setUpPloneSite(self, portal): - applyProfile(portal, 'redturtle.rsync:default') + applyProfile(portal, "redturtle.rsync:default") REDTURTLE_RSYNC_FIXTURE = RedturtleRsyncLayer() @@ -33,13 +34,13 @@ def setUpPloneSite(self, portal): REDTURTLE_RSYNC_INTEGRATION_TESTING = IntegrationTesting( bases=(REDTURTLE_RSYNC_FIXTURE,), - name='RedturtleRsyncLayer:IntegrationTesting', + name="RedturtleRsyncLayer:IntegrationTesting", ) REDTURTLE_RSYNC_FUNCTIONAL_TESTING = FunctionalTesting( bases=(REDTURTLE_RSYNC_FIXTURE,), - name='RedturtleRsyncLayer:FunctionalTesting', + name="RedturtleRsyncLayer:FunctionalTesting", ) @@ -49,5 +50,5 @@ def setUpPloneSite(self, portal): REMOTE_LIBRARY_BUNDLE_FIXTURE, z2.ZSERVER_FIXTURE, ), - name='RedturtleRsyncLayer:AcceptanceTesting', + name="RedturtleRsyncLayer:AcceptanceTesting", ) diff --git a/src/redturtle/rsync/tests/test_robot.py b/src/redturtle/rsync/tests/test_robot.py index e748035..59cac43 100644 --- a/src/redturtle/rsync/tests/test_robot.py +++ b/src/redturtle/rsync/tests/test_robot.py @@ -11,18 +11,21 @@ def test_suite(): suite = unittest.TestSuite() current_dir = os.path.abspath(os.path.dirname(__file__)) - robot_dir = os.path.join(current_dir, 'robot') + robot_dir = os.path.join(current_dir, "robot") robot_tests = [ - os.path.join('robot', doc) for doc in os.listdir(robot_dir) - if doc.endswith('.robot') and doc.startswith('test_') + os.path.join("robot", doc) + for doc in os.listdir(robot_dir) + if doc.endswith(".robot") and doc.startswith("test_") ] for robot_test in robot_tests: robottestsuite = robotsuite.RobotTestSuite(robot_test) robottestsuite.level = ROBOT_TEST_LEVEL - suite.addTests([ - layered( - robottestsuite, - layer=REDTURTLE_RSYNC_ACCEPTANCE_TESTING, - ), - ]) + suite.addTests( + [ + layered( + robottestsuite, + layer=REDTURTLE_RSYNC_ACCEPTANCE_TESTING, + ), + ] + ) return suite diff --git a/src/redturtle/rsync/tests/test_setup.py b/src/redturtle/rsync/tests/test_setup.py index 3257e20..9c2c37e 100644 --- a/src/redturtle/rsync/tests/test_setup.py +++ b/src/redturtle/rsync/tests/test_setup.py @@ -21,48 +21,45 @@ class TestSetup(unittest.TestCase): def setUp(self): """Custom shared utility setup for tests.""" - self.portal = self.layer['portal'] + self.portal = self.layer["portal"] if get_installer: - self.installer = get_installer(self.portal, self.layer['request']) + self.installer = get_installer(self.portal, self.layer["request"]) else: - self.installer = api.portal.get_tool('portal_quickinstaller') + self.installer = api.portal.get_tool("portal_quickinstaller") def test_product_installed(self): """Test if redturtle.rsync is installed.""" - self.assertTrue(self.installer.is_product_installed( - 'redturtle.rsync')) + self.assertTrue(self.installer.is_product_installed("redturtle.rsync")) def test_browserlayer(self): """Test that IRedturtleRsyncLayer is registered.""" from plone.browserlayer import utils from redturtle.rsync.interfaces import IRedturtleRsyncLayer - self.assertIn( - IRedturtleRsyncLayer, - utils.registered_layers()) + self.assertIn(IRedturtleRsyncLayer, utils.registered_layers()) -class TestUninstall(unittest.TestCase): +class TestUninstall(unittest.TestCase): layer = REDTURTLE_RSYNC_INTEGRATION_TESTING def setUp(self): - self.portal = self.layer['portal'] + self.portal = self.layer["portal"] if get_installer: - self.installer = get_installer(self.portal, self.layer['request']) + self.installer = get_installer(self.portal, self.layer["request"]) else: - self.installer = api.portal.get_tool('portal_quickinstaller') + self.installer = api.portal.get_tool("portal_quickinstaller") roles_before = api.user.get_roles(TEST_USER_ID) - setRoles(self.portal, TEST_USER_ID, ['Manager']) - self.installer.uninstall_product('redturtle.rsync') + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + self.installer.uninstall_product("redturtle.rsync") setRoles(self.portal, TEST_USER_ID, roles_before) def test_product_uninstalled(self): """Test if redturtle.rsync is cleanly uninstalled.""" - self.assertFalse(self.installer.is_product_installed( - 'redturtle.rsync')) + self.assertFalse(self.installer.is_product_installed("redturtle.rsync")) def test_browserlayer_removed(self): """Test that IRedturtleRsyncLayer is removed.""" from plone.browserlayer import utils from redturtle.rsync.interfaces import IRedturtleRsyncLayer + self.assertNotIn(IRedturtleRsyncLayer, utils.registered_layers()) From 178f5d5aee8ca04fa761dab01607369aa6ae8940 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Tue, 13 May 2025 17:12:28 +0200 Subject: [PATCH 3/3] add reindex --- src/redturtle/rsync/scripts/rsync.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redturtle/rsync/scripts/rsync.py b/src/redturtle/rsync/scripts/rsync.py index 7b1f48d..74abc95 100644 --- a/src/redturtle/rsync/scripts/rsync.py +++ b/src/redturtle/rsync/scripts/rsync.py @@ -216,11 +216,13 @@ def update_item(self, item, row, options): msg = f"[UPDATED] {updated.absolute_url()}" self.log_info(msg=msg) self.sync_uids.add(updated.UID()) + updated.reindexObject() else: self.n_updated += 1 msg = f"[UPDATED] {self.get_frontend_url(item)}" self.log_info(msg=msg) self.sync_uids.add(item.UID()) + item.reindexObject() def delete_items(self, data): """