From b8c1cd9ec81a73f183f7e6e71c2767099b6b5a46 Mon Sep 17 00:00:00 2001 From: Dima Uryvskiy Date: Tue, 5 Jul 2022 18:24:04 +0300 Subject: [PATCH] Add functionality for changing the time interval for cron tasks --- doc/1.Setup/Cron tasks.md | 53 ++++++++++++++++++ resources/pot/retailcrm-es_ES.pot | 14 ++++- resources/pot/retailcrm-ru_RU.pot | 11 ++++ src/assets/css/debug-info.css | 4 -- src/assets/css/debug-info.min.css | 2 +- src/assets/js/retailcrm-cron-info.js | 33 +++++++++-- .../class-wc-retailcrm-abstracts-settings.php | 10 ++++ src/include/class-wc-retailcrm-base.php | 32 ++++++++++- src/include/class-wc-retailcrm-plugin.php | 19 ++++--- src/languages/retailcrm-es_ES.mo | Bin 9751 -> 9947 bytes src/languages/retailcrm-ru_RU.mo | Bin 11999 -> 12280 bytes 11 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 doc/1.Setup/Cron tasks.md diff --git a/doc/1.Setup/Cron tasks.md b/doc/1.Setup/Cron tasks.md new file mode 100644 index 0000000..c468c05 --- /dev/null +++ b/doc/1.Setup/Cron tasks.md @@ -0,0 +1,53 @@ +### Настройки cron задач + +В версии 4.4.5 добавлен функционал для изменения интервалов времени выполнения cron задач. + +Для изменения интервала времени необходимо с помощью фильтра **retailcrm_add_cron_interval** добавить пользовательский интервал. Затем изменить интервал для cron задач с помощью фильтра **retailcrm_cron_schedules**. +Кастомизацию необходимо добавить на сервере в директорию wp-content -> mu-plugins -> mu-simla.php. После добавления кастомизации в настройках модуля необходимо очистить старые cron задачи. +Перейдите в настройки, откройте "Отладочная информация" и нажмите на кнопку "Очистить cron задачи", появится окно с сообщением об успешной очистке, интервалы будут применены. + +Если необходимо вернуть стандартные интервалы, то удаляем кастомизацию и в настройках так же очищаем старые cron задачи. + +**Интервалы по умолчанию:** +```php +[ + 'icml' => 'three_hours', + 'history' => 'five_minutes', + 'inventories' => 'fiveteen_minutes', +] +``` +> Важно! При использовании фильтра **retailcrm_cron_schedules**, можно использовать ключи: 'icml', 'history', 'inventories'. + +**Фильтры:** + +> retailcrm_add_cron_interval - позволяет добавить пользовательский интервал времени. + +> retailcrm_cron_schedules - позволяет изменить интервал времени для cron задач. + +**Пример использования:** +```php + [ + 'interval' => 120, // seconds + 'display' => __('Every 2 minutes') + ] + ]; +} + + +add_filter('retailcrm_cron_schedules', 'change_cron_tasks'); + +function change_cron_tasks($cronTasks) +{ + $cronTasks['history'] = 'two_minutes'; + + return $cronTasks; +} +``` + + diff --git a/resources/pot/retailcrm-es_ES.pot b/resources/pot/retailcrm-es_ES.pot index ae070f4..36b5a15 100644 --- a/resources/pot/retailcrm-es_ES.pot +++ b/resources/pot/retailcrm-es_ES.pot @@ -326,4 +326,16 @@ msgid "Short description" msgstr "Descripción corta" msgid "In the catalog, you can use a full or short description of the product" -msgstr "En el catálogo, puedes utilizar una descripción del producto corta o completa" \ No newline at end of file +msgstr "En el catálogo, puedes utilizar una descripción del producto corta o completa" + +msgid "If you change the time interval, need to clear the old cron tasks" +msgstr "Si cambias el Intervalo de Tiempo tienes que limpiar los cron tareas antiguos" + +msgid "Clear cron tasks" +msgstr "Borrar tareas cron" + +msgid "Clear" +msgstr "Borrar" + +msgid "Cron tasks cleared" +msgstr "Trabajos cron borrados" diff --git a/resources/pot/retailcrm-ru_RU.pot b/resources/pot/retailcrm-ru_RU.pot index f36cacf..b40b676 100644 --- a/resources/pot/retailcrm-ru_RU.pot +++ b/resources/pot/retailcrm-ru_RU.pot @@ -337,3 +337,14 @@ msgstr "Краткое описание" msgid "In the catalog, you can use a full or short description of the product" msgstr "В каталоге можно использовать полное или краткое описание товара" +msgid "If you change the time interval, need to clear the old cron tasks" +msgstr "Если вы изменили временной интервал, необходимо очистить старые cron задачи" + +msgid "Clear cron tasks" +msgstr "Очистить cron задачи" + +msgid "Clear" +msgstr "Очистить" + +msgid "Cron tasks cleared" +msgstr "Cron задачи очищены" diff --git a/src/assets/css/debug-info.css b/src/assets/css/debug-info.css index 5a7d4c5..0485b0d 100644 --- a/src/assets/css/debug-info.css +++ b/src/assets/css/debug-info.css @@ -1,7 +1,3 @@ -.retail-cron-info { - padding: 5px 30px !important; -} - .retail-cron-info-title { font-weight: bold; } diff --git a/src/assets/css/debug-info.min.css b/src/assets/css/debug-info.min.css index e533148..18e69aa 100644 --- a/src/assets/css/debug-info.min.css +++ b/src/assets/css/debug-info.min.css @@ -1 +1 @@ -.retail-cron-info{padding:5px 30px!important}.retail-cron-info-title{font-weight:700} \ No newline at end of file +.retail-cron-info-title{font-weight:700} \ No newline at end of file diff --git a/src/assets/js/retailcrm-cron-info.js b/src/assets/js/retailcrm-cron-info.js index 40c6bbe..4fd5da7 100644 --- a/src/assets/js/retailcrm-cron-info.js +++ b/src/assets/js/retailcrm-cron-info.js @@ -2,14 +2,20 @@ jQuery(function () { function RetailcrmCronInfo() { this.title = jQuery('.debug_info_options').get(0) + this.submitButton = jQuery('button[id="clear_cron_tasks"]').get(0); if (typeof this.title === 'undefined') { return false; } - this.history = 0; + if (typeof this.submitButton === 'undefined') { + return false; + } + this.icml = 0; + this.history = 0; this.inventories = 0; + this.messageSuccessful = ''; let _this = this; @@ -24,14 +30,19 @@ jQuery(function () { _this.history = response.history; _this.icml = response.icml; _this.inventories = response.inventories; + _this.messageSuccessful = response.translate.tr_successful; _this.displayInfoAboutCron( response.translate.tr_td_cron, response.translate.tr_td_icml, response.translate.tr_td_history, - response.translate.tr_td_inventories + response.translate.tr_td_inventories, ); }) + + this.clearCronTasks = this.clearCronTasks.bind(this); + + jQuery(this.submitButton).click(this.clearCronTasks); } RetailcrmCronInfo.prototype.displayInfoAboutCron = function (cron, icml, history, inventories) { @@ -40,11 +51,23 @@ jQuery(function () { this.infoTable = jQuery('tbody[class="retail-debug-info"]').get(0); jQuery(this.infoTable).append("" + cron + " : " + ""); - jQuery(this.infoTable).append("" + icml + " : " + this.icml + ""); - jQuery(this.infoTable).append("" + history + " : " + this.history + ""); - jQuery(this.infoTable).append("" + inventories + " : " + this.inventories + ""); + jQuery(this.infoTable).append("" + icml + " " + this.icml + ""); + jQuery(this.infoTable).append("" + history + " " + this.history + ""); + jQuery(this.infoTable).append("" + inventories + " " + this.inventories + ""); } + RetailcrmCronInfo.prototype.clearCronTasks = function () { + let _this = this; + + jQuery.ajax({ + type: "POST", + url: window.location.origin + '/wp-admin/admin-ajax.php?action=clear_cron_tasks', + success: function (response) { + alert(_this.messageSuccessful); + } + }); + }; + window.RetailcrmCronInfo = RetailcrmCronInfo; if (!(typeof RetailcrmCronInfo === 'undefined')) { diff --git a/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php b/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php index 2c4a9f9..865090d 100644 --- a/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php +++ b/src/include/abstracts/class-wc-retailcrm-abstracts-settings.php @@ -62,6 +62,7 @@ abstract class WC_Retailcrm_Abstracts_Settings extends WC_Integration 'heading', 'class' => 'debug_info_options' ]; + + $this->form_fields['clear_cron_tasks'] = [ + 'label' => __('Clear', 'retailcrm'), + 'title' => __('Clear cron tasks', 'retailcrm'), + 'type' => 'button', + 'description' => __('If you change the time interval, need to clear the old cron tasks', 'retailcrm'), + 'desc_tip' => true, + 'id' => 'clear_cron_tasks' + ]; } } elseif (empty($apiUrl) === false && empty($apiKey) === false) { $api = new WC_Retailcrm_Proxy( diff --git a/src/include/class-wc-retailcrm-base.php b/src/include/class-wc-retailcrm-base.php index 7f01e30..3efa577 100644 --- a/src/include/class-wc-retailcrm-base.php +++ b/src/include/class-wc-retailcrm-base.php @@ -82,6 +82,7 @@ if (!class_exists('WC_Retailcrm_Base')) { add_action('wp_ajax_content_upload', [$this, 'count_upload_data'], 99); add_action('wp_ajax_generate_icml', [$this, 'generate_icml']); add_action('wp_ajax_upload_selected_orders', [$this, 'upload_selected_orders']); + add_action('wp_ajax_clear_cron_tasks', [$this, 'clear_cron_tasks']); add_action('admin_print_footer_scripts', [$this, 'ajax_generate_icml'], 99); add_action('woocommerce_update_customer', [$this, 'update_customer'], 10, 1); add_action('user_register', [$this, 'create_customer'], 10, 2); @@ -122,9 +123,18 @@ if (!class_exists('WC_Retailcrm_Base')) { */ public function api_sanitized($settings) { + $timeInterval = apply_filters( + 'retailcrm_cron_schedules', + [ + 'icml' => 'three_hours', + 'history' => 'five_minutes', + 'inventories' => 'fiveteen_minutes', + ] + ); + if (isset($settings['sync']) && $settings['sync'] == static::YES) { if (!wp_next_scheduled('retailcrm_inventories')) { - wp_schedule_event(time(), 'fiveteen_minutes', 'retailcrm_inventories'); + wp_schedule_event(time(), $timeInterval['inventories'], 'retailcrm_inventories'); } } elseif (isset($settings['sync']) && $settings['sync'] == static::NO) { wp_clear_scheduled_hook('retailcrm_inventories'); @@ -132,7 +142,7 @@ if (!class_exists('WC_Retailcrm_Base')) { if (isset($settings['history']) && $settings['history'] == static::YES) { if (!wp_next_scheduled('retailcrm_history')) { - wp_schedule_event(time(), 'five_minutes', 'retailcrm_history'); + wp_schedule_event(time(), $timeInterval['history'], 'retailcrm_history'); } } elseif (isset($settings['history']) && $settings['history'] == static::NO) { wp_clear_scheduled_hook('retailcrm_history'); @@ -140,7 +150,7 @@ if (!class_exists('WC_Retailcrm_Base')) { if (isset($settings['icml']) && $settings['icml'] == static::YES) { if (!wp_next_scheduled('retailcrm_icml')) { - wp_schedule_event(time(), 'three_hours', 'retailcrm_icml'); + wp_schedule_event(time(), $timeInterval['icml'], 'retailcrm_icml'); } } elseif (isset($settings['icml']) && $settings['icml'] == static::NO) { wp_clear_scheduled_hook('retailcrm_icml'); @@ -153,6 +163,21 @@ if (!class_exists('WC_Retailcrm_Base')) { return $settings; } + /** + * If you change the time interval, need to clear the old cron tasks + * + * @codeCoverageIgnore Check in another tests + */ + public function clear_cron_tasks() + { + wp_clear_scheduled_hook('retailcrm_icml'); + wp_clear_scheduled_hook('retailcrm_history'); + wp_clear_scheduled_hook('retailcrm_inventories'); + + //Add new cron tasks + $this->api_sanitized($this->settings); + } + /** * Generate ICML * @@ -532,6 +557,7 @@ if (!class_exists('WC_Retailcrm_Base')) { 'tr_td_cron' => __('Cron launches', 'retailcrm'), 'tr_td_icml' => __('Generation ICML', 'retailcrm'), 'tr_td_history' => __('Syncing history', 'retailcrm'), + 'tr_successful' => __('Cron tasks cleared', 'retailcrm'), 'tr_td_inventories' => __('Syncing inventories', 'retailcrm'), ]; diff --git a/src/include/class-wc-retailcrm-plugin.php b/src/include/class-wc-retailcrm-plugin.php index b9cc646..d307268 100644 --- a/src/include/class-wc-retailcrm-plugin.php +++ b/src/include/class-wc-retailcrm-plugin.php @@ -38,27 +38,28 @@ class WC_Retailcrm_Plugin { $this->file = $file; - add_filter('cron_schedules', array($this, 'filter_cron_schedules'), 10, 1); + add_filter('cron_schedules', [$this, 'filter_cron_schedules'], 10, 1); } public function filter_cron_schedules($schedules) { return array_merge( $schedules, - array( - 'five_minutes' => array( + [ + 'five_minutes' => [ 'interval' => 300, // seconds 'display' => __('Every 5 minutes') - ), - 'three_hours' => array( + ], + 'three_hours' => [ 'interval' => 10800, // seconds 'display' => __('Every 3 hours') - ), - 'fiveteen_minutes' => array( + ], + 'fiveteen_minutes' => [ 'interval' => 900, // seconds 'display' => __('Every 15 minutes') - ) - ) + ] + ], + apply_filters('retailcrm_add_cron_interval', $schedules) ); } diff --git a/src/languages/retailcrm-es_ES.mo b/src/languages/retailcrm-es_ES.mo index 684591a8190ddbf1cce0fa1bbf6e551a80fe2c5f..44b7a355f5d2b6dd2ec3aad733dbd11df2ad6daa 100644 GIT binary patch delta 2655 zcmYk+TWl0n9LMpq^r9`cwdK|=RL8b}(n6us3S#L6iWXaLEuf;%ZFkxZ-3#5_LK=ky z(-;vYF)Lm|qJShC6KilIibj2qAQ3N(i4Y(G3=e7~hVY=`i++FIS;I;H`xCr(5k-ibl<;x4!Sbu6QP z6c^*yI2}E7QjDd@U)eFf{K#$p4AsH6n1{b&5&r4cbC^vD^=UW_ zYmhO`O4LL;QT^{gP4syzVSIChi$WScNBS~9U_M^Jd+;~+{zR6o36!E%v>2-}iVhya zO3Y<;%2*v%<5FzK5NhCKs0p3HRg7=`;6gL5XH?B_JfOk`v@|IIpDS*LB02( zTmJ^Ts9(S)T*AvMa3{{flQ;=~$9ecK#wy?0`>e+Y{icY$-h?Qp;d=3A8*G>REM>wv(SpXW4567 zbP)C2)2<_^fnUOEJct!|23e%Jj+%&m=E7v$fX$Q0zf$xD4NBz|EXBWZ0#0QXY`^iM zRxlH_@;X$;>hW&u!bP|n$-X&(%D`!yihrRNlF!elSb@yRT#j?0Ex3U(51sL}GV%(u z(TWbEI{W~&k}pt)@_W>0bRBhQbEv3&GB#r!>JSdQ?m_jt7rXIQWJnXg$%RgJ0Y_6S zt3=JL2DP#l)C6qQN@A!N_n`(njC$@X)RtVunRpGAu_C@n^0j1l+v|0h`VtC-auubf&)~C)wmjaQ4>3kbMP##Ll66_iFaaw zzW?o9C^gTaR+>Pq@Ca%PK5^Smp)&9jvN&@MpT+FEjA_TcsDaO6HO^*q9omhki8;6k z51}S>9>>4`Oe-~D2a*!wpjN&UAH(-hdzrzs>Dx?1l49ndGSz|);0Dyh_Pg!xB6Bn+ z(8hD9{_8opn%G*5tK%LnbYsB15l8LO%c#%oD00rsDC%oCh1%;MQCn~kIp8L*JT+jM zYa=eFeKW4dx3L9pqQ+~eApbfn9h{5_IE+g9Gst$C7cdLoLB=reqt3_}DwY2r+iw=q z$wRmrS)_RpwS}X27EhuUbZ}NGv!m#tK36BCjSHnw9cXo0Db3?$LE`7^X>sjVC!sl~ zi#Fhibj7vaRXIx+9nDAGN)6tdzJVP?rCZlVsc3^$RuapJ7JVZsvxx_ZW`bppAJzM@ zkmE8gsuORveZPXg#aiW!Ygs_RFgo=);a;p5Q^lSY2Tx1Z732lIO ze>1U_&{+=>O0YIjN4AI1hHWBrB)ff^h{^zA5e}i^C)gLWQ5Co7bY`a3Z_KiE zD>f13x72mXCXb(Gy^DY%A7JI6>QTLNPnC-502`LbmOSNov;J_@Eb|4og%f$+>B(&GrOfsY$LjF~yB*%N z16Hd$jj-jntxm`0QRZid?5H(3WLp6zxXodufpo9#n7&ZV=^F}1lUGZ>%S>J_Ta@u1 DR2C?g delta 2435 zcmYk+e@vBC9LMnkTtpFsD}sQDk0Aa);R@joA}vVdM}S78W=34NPkNOuC`h&%kF)$o z+gOpa>DJOM|0yTz=4}0#OEy+pwOK3LY%zl|W7X=f*6-dQIJD0A-q(4K&-0vfzUTXR ztKn?D_w7R88ACfr+(mT68KZ&s=kP;Ioo7q}`Y{o+FbP-Sd@ROP+=5xyXvYs=F4sL+ zhG%gJ-o#A&5qY0y=I~q3fFJdu7EH%Z)B`6m8829`U^dqu;9~q5d6)SaHR125_bg>~ zT2K+{{u=aSJ93LTiVK+E9H+CKfqr|#5NhIK)XK+j9cHoZFmA`SIEc#3HyFSPti(mE zRuk_=E$A>d;wz{HkE0g&2PQMWNm^h`3TC7Jp#XW8DYxS_s0a7h>n?2N`Z#XK+qe=} z^O7{&fkoJk>+ogNLat*8euvC$(wWTDgn9gEpcG575_SAf<3jAiMfeixzDuYH-bPJ0 zj{KQl`O*Dz$#X3(L2blE)z%T5i~Xqgz3M0bI_t{}@Mnhk(OG_px^c{U2Q~387{GsU z1qPUn&6sA?LPA)G$8Z&1!d>_oDpUE~q)c|;QhXte{KwL{$Nj7cn?teYxSCYmt=pGR%z9KMT#$lT@-gF1>n?1?evO;lzakL74*g{T*nqjpq> zD$2d6&*eE()t<5AuVW?GH&8_w&rLEJ_2P7F!(7yYPNItXJZdA?Q1f}C_J&E+gn!x_ zawuUP!B))2YE)*PLcORDbp+>7JG_o0$9!(DCvgqezoDMX;~P{4O0f=uNaj5A7M;Tk z+`?jP;O*L}ha2$}?!XUGEB8}L1z3ztxDU1PE2zxfL}lPMYKK3eGVm8F(`g*M#+PG; zzW?=fSfr`O6BtBIIDr8y&6z3A5NbiaxEU{_7BY#A=x24Bs1=ooUeu0H;U2t(I-*p^ z7#=a1nC;QoL`NxV!cEwYTF`ma_*G;rW&~ZFKuu7`w)qfD3+lyPsPSWV{6*B!yoUO! zhLCbG@1ctQ3-qQxGdfEB9i(7QcJ9oC0c!(pWBd?uW@Zo{!7;4D4S6#YKZ~lBevHMd zsFc5hWZ#TnJdPuen(y;WWV%R_s3fIyBXZ296Ys^-$ePSYsMJp3SEyOEvr$xvr!WR} z95sZpruS)=+K?t!D;n@8EcRB>+es*QvrT!~HG5$#vnqEX+gD{)+rEl}iT`UVn$`A- zVxDf=jhg=Vxu2*|$*ZxG>1x`F+M-IbgwVG2KhJDyp;JnzVj75e;vu4vP*o`n^@Pr& zir7Z95%&;kcN3Y!eY&8zn!k`v9I=5|NvH@OBlZ)@=Rra(mS`qgiDkrYB7=CGC?_5z z9wgKb5USEAbwLXfoU?gY7q+QNeLh_bR3xN%j(Y!Vx%5>pbwr9CueGknCv3mns*gow zv6om&sQA_N@yxad=!6N?x4zPSgxVVIt|_`@-ZL=+pC%SW6X%!sqJ1eNarv&(?RL2B zJ#NUU*jelJx<|Uhoe?M8?Hr8+dxPPQU|WY9tw@`SAE?R-L{De^?Te1)eBvvr56;{? m9E~_3clu#ov_BjQI+0Gt?d}=A;Do~6U7eAz-f%ImEbd>Kg6*3C diff --git a/src/languages/retailcrm-ru_RU.mo b/src/languages/retailcrm-ru_RU.mo index 8844daebfc396ddafbb6b8ba743444d34f3a251e..c4ddf79656e730eb1b057fd47f321d4eec90d11e 100644 GIT binary patch delta 2795 zcmZA2drZ}39LMoTxk}y;LBUHu2~8AJP!lUuE(wI7=DjjRI0AB0IF**|9JB;SvI*Ks zOS|V<8|V=|rQn=3Z8i5hP1j{j{^*ZPU9Hw1mTmR^{LaB#&-ngc&-1%H_vg^R{#L2y z>ZtGohO~tkPrMOkjBbpN;zHUo#296_VhnD_q4*NsgZnTM-wc&cVJzkII2Ny9GX8?| zF_uckVG;7&V=Bq0VlS5^*oGtVTa3q_F#`Vy-H)JERgi$gFat;7qELASCQ&ZKF}MS1 z+w4bmpo;ZorVZK7G z?N!u!*Fye=YWOdliP5y0f|f)IH8DyF%t;R0Y6h+dSrnDF*pbH1%E@YdS z-KY_Cpho^KY7c#cnz_&MUc82x7)`HP9g~fkiDj689uFD*%&S~J#gCCrn@XN)DV|67 zAY(p6&CGbtj5bv&s)BS>M+z_%*P%A&F4U$yf<<@^XX7w-UO8r?zVo~v%A7<^=>=@U z@9;h>W?1U+R@4V?p+qD4;)aNnF$*7@? zsD^f zAk%?b>kmTqU;*XNP#qh_^pZ96r!oJ^lyhSf?!`9z1Dnyy+v@3Hjsmxgg@dsOb!tkG zSv3`?4z?hF<{+0x@B-#yIA?qv7GOHQgT;7x#?s)XOJ?V(!sWPZ5XTQyp`Tw5Rrn=x z+u5KcsFR}IKa}8PIim-Y(HR>x#-lN=COG_elICq7X{bsIgE;?fLRsh6MO{bw+_7JH{;mFuyq!gniXP57cs*7k;Htn#X|`bwAOt|F@I zTvm0x+tv7NS#6qC?{ZaG?uOt;&I=8-6?fI`D;{w%taE?dIN#d1E0Gz811+{c&>Yyw zb!XreWB>0)@JsuY-EE(+yY+JC^rRXqSG~LYu4VT*HSMu`?A}0I=h>ubzOzX;qC5uz z&Gu>AZ`sEKZI1Z=lWY`LEZ}1Ksu=O}-uJ`yn+f;$O*GoCg2^ delta 2509 zcmX}tdrZ}39LMp8xEwAD0tEr32S`dTf+&ch+`K{DjtX=kQG^2$7dJuY*6Ik=qWMRT z6K-s+g4Je!MCysHB(PStR&$+WmyKDtR@NH!hq+doR`1U_zj)a9yq@QGdG61TzxH0) z2P88xI{;y&WJU}JRQS{Og1vIt{rSdNicg-fsjqp%f|u-7WTfU%U_crU(*X*i1; z@CQu66e{Ds%d97(iXHsaU^6bmQ+N-)k9y%tT#5^pH!+#Ajb0>T8dA5}h-z>vs-8|% z2alqjA3=5SZH%OSbDm5z7e2>SoUv<6Rg-Z&-d4hZ@Op)C}aJ zHc=sJ3U}fP9Ka%c8CeB$5j7*6s%bc$YS zQ%#{Nm_c>qYg~m_QJXV{Go($Mh1FP#YjF(Q@FMEHHS9!LftuOf*pDtJnRGI9s2<jK7<*#6YEhU8^<5<0v^S;*a(%F#!Zd18&w{~9XOArn86Mm zzz*b9a~acV-`pXio~N!1jC>br56q%IFkhfL@+UHj#?Im1hMQ0wA3{z2>!^y);0S(Y zl{*-fmS`BY={`o)^DVmQocWy$vt$xzB?1ePgJ8-~YusSjhLw~Lp*l8=8u2fdu@pKe z7o(P70*~Wm9K=r6K^^`TwIsGI=3mDxpV~MGrT}%Us!=`dL!FL6+=P?31sCu!%;n}9 zd=YDL8nx*nIP|Ko2pvIu5mEKM#!c0C7P-tI81}d}2(g3MPB;n8uaeG`4r;jyqyuVS zlNwB{C)h0hPtzWJlF$P75=xH}O~ih}PH2&Imi7{xiDsgfQ0gW)v;P03DyI^h1k*^g z6G~x3NMQW_^5Rb^x$wnOAd?Q!L=B`22XEyg&3p4R<6PS0fgjWG8&$zAS=oE4sDQvR@cZl~W2D;V(|_nz?`^Pcw3 s@ORqxlHGgOJLNm(8}z