From da87859a325a01b256b9890674d0503202e54863 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:30:45 +0200 Subject: [PATCH 01/13] Renamed admin's action_list to historical_records This is a more accurate name for the contents of the queryset. Also did a small cleanup of `_object_history_list.html`, incl. removing the `scope` attribute of a `
{% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}
{% endif %}{% trans "This object doesn't have a change history." %}
From 16b7de7482d0898c9f83c9042c01442fe35ba66e Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Sat, 24 Feb 2024 22:08:25 +0100 Subject: [PATCH 02/13] Made object history list template overridable Including renaming it to remove the `_` prefix (indicating that it's internal / not meant for usage by library users), and adding an `object_history_list_template` field to the admin class. This rename also changed the order of messages in the translation files. Also deprecated the template tag `simple_history_admin_list.display_list`, as it's now entirely unused within the project. --- CHANGES.rst | 6 +++ docs/admin.rst | 18 +++++++ simple_history/admin.py | 2 + .../locale/ar/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/cs_CZ/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/de/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/fr/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/id/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/nb/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/pl/LC_MESSAGES/django.po | 40 ++++++++-------- .../locale/pt_BR/LC_MESSAGES/django.po | 40 ++++++++-------- .../locale/ru_RU/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/ur/LC_MESSAGES/django.po | 48 +++++++++---------- .../locale/zh_Hans/LC_MESSAGES/django.po | 48 +++++++++---------- .../simple_history/object_history.html | 5 +- ...ory_list.html => object_history_list.html} | 0 .../templatetags/simple_history_admin_list.py | 9 +++- .../tests/tests/test_deprecation.py | 13 +++++ 18 files changed, 304 insertions(+), 261 deletions(-) rename simple_history/templates/simple_history/{_object_history_list.html => object_history_list.html} (100%) create mode 100644 simple_history/tests/tests/test_deprecation.py diff --git a/CHANGES.rst b/CHANGES.rst index 653c6951e..270a82d44 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Unreleased ---------- - Support custom History ``Manager`` and ``QuerySet`` classes (gh-1280) +- Renamed the (previously internal) admin template + ``simple_history/_object_history_list.html`` to + ``simple_history/object_history_list.html``, and added the field + ``SimpleHistoryAdmin.object_history_list_template`` for overriding it (gh-1128) +- Deprecated the undocumented template tag ``simple_history_admin_list.display_list()``; + it will be removed in version 3.8 (gh-1128) 3.5.0 (2024-02-19) ------------------ diff --git a/docs/admin.rst b/docs/admin.rst index 1b34f92c4..ba9cf1cd9 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -69,6 +69,24 @@ admin class .. image:: screens/5_history_list_display.png + +Customizing the History Admin Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you'd like to customize the HTML of ``SimpleHistoryAdmin``'s object history pages, +you can override the following attributes with the names of your own templates: + +- ``object_history_template``: The main object history page, which includes (inserts) + ``object_history_list_template``. +- ``object_history_list_template``: The table listing an object's historical records and + the changes made between them. +- ``object_history_form_template``: The form pre-filled with the details of an object's + historical record, which also allows you to revert the object to a previous version. + +If you'd like to only customize certain parts of the mentioned templates, look for +``block`` template tags in the source code that you can override. + + Disabling the option to revert an object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/simple_history/admin.py b/simple_history/admin.py index a126ab2f1..758b13f5f 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -20,6 +20,7 @@ class SimpleHistoryAdmin(admin.ModelAdmin): object_history_template = "simple_history/object_history.html" + object_history_list_template = "simple_history/object_history_list.html" object_history_form_template = "simple_history/object_history_form.html" def get_urls(self): @@ -80,6 +81,7 @@ def history_view(self, request, object_id, extra_context=None): ) context = { "title": self.history_view_title(request, obj), + "object_history_list_template": self.object_history_list_template, "historical_records": historical_records, "module_name": capfirst(force_str(opts.verbose_name_plural)), "object": obj, diff --git a/simple_history/locale/ar/LC_MESSAGES/django.po b/simple_history/locale/ar/LC_MESSAGES/django.po index 109e825e4..c7234735e 100644 --- a/simple_history/locale/ar/LC_MESSAGES/django.po +++ b/simple_history/locale/ar/LC_MESSAGES/django.po @@ -60,30 +60,6 @@ msgstr "تغيير" msgid "Deleted" msgstr "تمت إزالته" -#: simple_history/templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "عنصر" - -#: simple_history/templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "التاريخ/الوقت" - -#: simple_history/templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "تعليق" - -#: simple_history/templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "تغير من قبل" - -#: simple_history/templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "سبب التغير" - -#: simple_history/templates/simple_history/_object_history_list.html:37 -msgid "None" -msgstr "فارغ" - #: simple_history/templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -121,6 +97,30 @@ msgstr "اضغط على زر 'استرجاع' ادناه للاسترجاع له msgid "Press the 'Change History' button below to edit the history." msgstr "اضغط على زر 'تعديل سجل التغيرات' ادناه لتعديل التاريخ." +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "عنصر" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "التاريخ/الوقت" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "تعليق" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "تغير من قبل" + +#: simple_history/templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "سبب التغير" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "فارغ" + #: simple_history/templates/simple_history/submit_line.html:4 msgid "Revert" msgstr "استرجاع" diff --git a/simple_history/locale/cs_CZ/LC_MESSAGES/django.po b/simple_history/locale/cs_CZ/LC_MESSAGES/django.po index 0f8dd76e6..aa8dabca6 100644 --- a/simple_history/locale/cs_CZ/LC_MESSAGES/django.po +++ b/simple_history/locale/cs_CZ/LC_MESSAGES/django.po @@ -59,30 +59,6 @@ msgstr "Změněno" msgid "Deleted" msgstr "Smazáno" -#: simple_history/templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "Objekt" - -#: simple_history/templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "Datum/čas" - -#: simple_history/templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "Komentář" - -#: simple_history/templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "Změnil" - -#: simple_history/templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "Důvod změny" - -#: simple_history/templates/simple_history/_object_history_list.html:37 -msgid "None" -msgstr "Žádné" - #: simple_history/templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -121,6 +97,30 @@ msgstr "Stisknutím tlačítka 'Vrátit změny' se vrátíte k této verzi objek msgid "Press the 'Change History' button below to edit the history." msgstr "Chcete-li historii upravit, stiskněte tlačítko 'Změnit historii'" +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Objekt" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Datum/čas" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Komentář" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Změnil" + +#: simple_history/templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "Důvod změny" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "Žádné" + #: simple_history/templates/simple_history/submit_line.html:4 msgid "Revert" msgstr "Vrátit změny" diff --git a/simple_history/locale/de/LC_MESSAGES/django.po b/simple_history/locale/de/LC_MESSAGES/django.po index 37ac7542e..e38d45689 100644 --- a/simple_history/locale/de/LC_MESSAGES/django.po +++ b/simple_history/locale/de/LC_MESSAGES/django.po @@ -48,30 +48,6 @@ msgstr "Geändert" msgid "Deleted" msgstr "Gelöscht" -#: simple_history/templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "Objekt" - -#: simple_history/templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "Datum/Uhrzeit" - -#: simple_history/templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "Kommentar" - -#: simple_history/templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "Geändert von" - -#: simple_history/templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "Änderungsgrund" - -#: simple_history/templates/simple_history/_object_history_list.html:37 -msgid "None" -msgstr "Keine/r" - #: simple_history/templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -108,6 +84,30 @@ msgstr "" msgid "Or press the 'Change History' button to edit the history." msgstr "Oder wählen Sie 'Historie ändern', um diese zu bearbeiten." +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Objekt" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Datum/Uhrzeit" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Kommentar" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Geändert von" + +#: simple_history/templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "Änderungsgrund" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "Keine/r" + #: simple_history/templates/simple_history/submit_line.html:3 msgid "Revert" msgstr "Wiederherstellen" diff --git a/simple_history/locale/fr/LC_MESSAGES/django.po b/simple_history/locale/fr/LC_MESSAGES/django.po index 883afbad8..6528938c8 100644 --- a/simple_history/locale/fr/LC_MESSAGES/django.po +++ b/simple_history/locale/fr/LC_MESSAGES/django.po @@ -59,30 +59,6 @@ msgstr "Modifié" msgid "Deleted" msgstr "Effacé" -#: .\simple_history\templates\simple_history\_object_history_list.html:9 -msgid "Object" -msgstr "Objet" - -#: .\simple_history\templates\simple_history\_object_history_list.html:13 -msgid "Date/time" -msgstr "Date/heure" - -#: .\simple_history\templates\simple_history\_object_history_list.html:14 -msgid "Comment" -msgstr "Commentaire" - -#: .\simple_history\templates\simple_history\_object_history_list.html:15 -msgid "Changed by" -msgstr "Modifié par" - -#: .\simple_history\templates\simple_history\_object_history_list.html:16 -msgid "Change reason" -msgstr "Raison de la modification" - -#: .\simple_history\templates\simple_history\_object_history_list.html:37 -msgid "None" -msgstr "Aucun" - #: .\simple_history\templates\simple_history\object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -125,6 +101,30 @@ msgid "Press the 'Change History' button below to edit the history." msgstr "" "Cliquez sur le bouton 'Historique' ci-dessous pour modifier l'historique." +#: .\simple_history\templates\simple_history\object_history_list.html:9 +msgid "Object" +msgstr "Objet" + +#: .\simple_history\templates\simple_history\object_history_list.html:13 +msgid "Date/time" +msgstr "Date/heure" + +#: .\simple_history\templates\simple_history\object_history_list.html:14 +msgid "Comment" +msgstr "Commentaire" + +#: .\simple_history\templates\simple_history\object_history_list.html:15 +msgid "Changed by" +msgstr "Modifié par" + +#: .\simple_history\templates\simple_history\object_history_list.html:16 +msgid "Change reason" +msgstr "Raison de la modification" + +#: .\simple_history\templates\simple_history\object_history_list.html:42 +msgid "None" +msgstr "Aucun" + #: .\simple_history\templates\simple_history\submit_line.html:4 msgid "Revert" msgstr "Rétablir" diff --git a/simple_history/locale/id/LC_MESSAGES/django.po b/simple_history/locale/id/LC_MESSAGES/django.po index 86e9e2848..b649d6b39 100644 --- a/simple_history/locale/id/LC_MESSAGES/django.po +++ b/simple_history/locale/id/LC_MESSAGES/django.po @@ -58,30 +58,6 @@ msgstr "Diubah" msgid "Deleted" msgstr "Dihapus" -#: .\simple_history\templates\simple_history\_object_history_list.html:9 -msgid "Object" -msgstr "Objek" - -#: .\simple_history\templates\simple_history\_object_history_list.html:13 -msgid "Date/time" -msgstr "Tanggal/waktu" - -#: .\simple_history\templates\simple_history\_object_history_list.html:14 -msgid "Comment" -msgstr "Komentar" - -#: .\simple_history\templates\simple_history\_object_history_list.html:15 -msgid "Changed by" -msgstr "Diubah oleh" - -#: .\simple_history\templates\simple_history\_object_history_list.html:16 -msgid "Change reason" -msgstr "Alasan perubahan" - -#: .\simple_history\templates\simple_history\_object_history_list.html:37 -msgid "None" -msgstr "Tidak ada" - #: .\simple_history\templates\simple_history\object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -122,6 +98,30 @@ msgstr "" msgid "Press the 'Change History' button below to edit the history." msgstr "Tekan tombol 'Ubah Riwayat' di bawah ini untuk mengubah riwayat." +#: .\simple_history\templates\simple_history\object_history_list.html:9 +msgid "Object" +msgstr "Objek" + +#: .\simple_history\templates\simple_history\object_history_list.html:13 +msgid "Date/time" +msgstr "Tanggal/waktu" + +#: .\simple_history\templates\simple_history\object_history_list.html:14 +msgid "Comment" +msgstr "Komentar" + +#: .\simple_history\templates\simple_history\object_history_list.html:15 +msgid "Changed by" +msgstr "Diubah oleh" + +#: .\simple_history\templates\simple_history\object_history_list.html:16 +msgid "Change reason" +msgstr "Alasan perubahan" + +#: .\simple_history\templates\simple_history\object_history_list.html:42 +msgid "None" +msgstr "Tidak ada" + #: .\simple_history\templates\simple_history\submit_line.html:4 msgid "Revert" msgstr "Kembalikan" diff --git a/simple_history/locale/nb/LC_MESSAGES/django.po b/simple_history/locale/nb/LC_MESSAGES/django.po index e2167f858..fdbd51d41 100644 --- a/simple_history/locale/nb/LC_MESSAGES/django.po +++ b/simple_history/locale/nb/LC_MESSAGES/django.po @@ -60,30 +60,6 @@ msgstr "Endret" msgid "Deleted" msgstr "Slettet" -#: simple_history/templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "Objekt" - -#: simple_history/templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "Dato/tid" - -#: simple_history/templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "Kommentar" - -#: simple_history/templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "Endret av" - -#: simple_history/templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "Endringsårsak" - -#: simple_history/templates/simple_history/_object_history_list.html:37 -msgid "None" -msgstr "Ingen" - #: simple_history/templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -125,6 +101,30 @@ msgstr "" msgid "Press the 'Change History' button below to edit the history." msgstr "Trykk på 'Endre historikk'-knappen under for å endre historikken." +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Objekt" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Dato/tid" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Kommentar" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Endret av" + +#: simple_history/templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "Endringsårsak" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "Ingen" + #: simple_history/templates/simple_history/submit_line.html:4 msgid "Revert" msgstr "Tilbakestill" diff --git a/simple_history/locale/pl/LC_MESSAGES/django.po b/simple_history/locale/pl/LC_MESSAGES/django.po index 161b014c2..d420d2ebc 100644 --- a/simple_history/locale/pl/LC_MESSAGES/django.po +++ b/simple_history/locale/pl/LC_MESSAGES/django.po @@ -57,26 +57,6 @@ msgid "" msgstr "" "Wybierz datę z poniższej listy aby przywrócić poprzednią wersję tego obiektu." -#: templates/simple_history/object_history.html:17 -msgid "Object" -msgstr "Obiekt" - -#: templates/simple_history/object_history.html:18 -msgid "Date/time" -msgstr "Data/czas" - -#: templates/simple_history/object_history.html:19 -msgid "Comment" -msgstr "Komentarz" - -#: templates/simple_history/object_history.html:20 -msgid "Changed by" -msgstr "Zmodyfikowane przez" - -#: templates/simple_history/object_history.html:38 -msgid "None" -msgstr "Brak" - #: templates/simple_history/object_history.html:46 msgid "This object doesn't have a change history." msgstr "Ten obiekt nie ma historii zmian." @@ -103,6 +83,26 @@ msgstr "Naciśnij przycisk „Przywróć” aby przywrócić tę wersję obiektu msgid "Or press the 'Change History' button to edit the history." msgstr "Lub naciśnij przycisk „Historia zmian” aby edytować historię." +#: templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Obiekt" + +#: templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Data/czas" + +#: templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Komentarz" + +#: templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Zmodyfikowane przez" + +#: templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "Brak" + #: templates/simple_history/submit_line.html:3 msgid "Revert" msgstr "Przywróć" diff --git a/simple_history/locale/pt_BR/LC_MESSAGES/django.po b/simple_history/locale/pt_BR/LC_MESSAGES/django.po index b328efa9c..f5286ece4 100644 --- a/simple_history/locale/pt_BR/LC_MESSAGES/django.po +++ b/simple_history/locale/pt_BR/LC_MESSAGES/django.po @@ -57,26 +57,6 @@ msgstr "" "Escolha a data desejada na lista a seguir para reverter as modificações " "feitas nesse objeto." -#: simple_history/templates/simple_history/object_history.html:17 -msgid "Object" -msgstr "Objeto" - -#: simple_history/templates/simple_history/object_history.html:18 -msgid "Date/time" -msgstr "Data/hora" - -#: simple_history/templates/simple_history/object_history.html:19 -msgid "Comment" -msgstr "Comentário" - -#: simple_history/templates/simple_history/object_history.html:20 -msgid "Changed by" -msgstr "Modificado por" - -#: simple_history/templates/simple_history/object_history.html:38 -msgid "None" -msgstr "-" - #: simple_history/templates/simple_history/object_history.html:46 msgid "This object doesn't have a change history." msgstr "Esse objeto não tem um histórico de modificações." @@ -104,6 +84,26 @@ msgstr "" msgid "Or press the 'Change History' button to edit the history." msgstr "Ou clique em 'Histórico de Modificações' para modificar o histórico." +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Objeto" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Data/hora" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Comentário" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Modificado por" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "-" + #: simple_history/templates/simple_history/submit_line.html:3 msgid "Revert" msgstr "Reverter" diff --git a/simple_history/locale/ru_RU/LC_MESSAGES/django.po b/simple_history/locale/ru_RU/LC_MESSAGES/django.po index 9865513d7..c50888eae 100644 --- a/simple_history/locale/ru_RU/LC_MESSAGES/django.po +++ b/simple_history/locale/ru_RU/LC_MESSAGES/django.po @@ -50,26 +50,6 @@ msgstr "Изменено" msgid "Deleted" msgstr "Удалено" -#: templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "Объект" - -#: templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "Дата/время" - -#: templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "Комментарий" - -#: templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "Изменено" - -#: templates/simple_history/_object_history_list.html:36 -msgid "None" -msgstr "None" - #: templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -105,6 +85,30 @@ msgstr "" msgid "Or press the 'Change History' button to edit the history." msgstr "Или нажмите кнопку 'Изменить запись', чтобы изменить историю." +#: templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "Объект" + +#: templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "Дата/время" + +#: templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "Комментарий" + +#: templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "Изменено" + +#: templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "Причина изменения" + +#: templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "None" + #: templates/simple_history/submit_line.html:3 msgid "Revert" msgstr "Восстановить" @@ -112,7 +116,3 @@ msgstr "Восстановить" #: templates/simple_history/submit_line.html:4 msgid "Change History" msgstr "Изменить запись" - -#: templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "Причина изменения" diff --git a/simple_history/locale/ur/LC_MESSAGES/django.po b/simple_history/locale/ur/LC_MESSAGES/django.po index dc9c014e2..a55605eb9 100644 --- a/simple_history/locale/ur/LC_MESSAGES/django.po +++ b/simple_history/locale/ur/LC_MESSAGES/django.po @@ -58,30 +58,6 @@ msgstr "بدل گیا" msgid "Deleted" msgstr "حذف کر دیا گیا" -#: .\simple_history\templates\simple_history\_object_history_list.html:9 -msgid "Object" -msgstr "آبجیکٹ" - -#: .\simple_history\templates\simple_history\_object_history_list.html:13 -msgid "Date/time" -msgstr "تاریخ/وقت" - -#: .\simple_history\templates\simple_history\_object_history_list.html:14 -msgid "Comment" -msgstr "تبصرہ" - -#: .\simple_history\templates\simple_history\_object_history_list.html:15 -msgid "Changed by" -msgstr "کی طرف سے تبدیل" - -#: .\simple_history\templates\simple_history\_object_history_list.html:16 -msgid "Change reason" -msgstr "تبدیلی کا سبب" - -#: .\simple_history\templates\simple_history\_object_history_list.html:37 -msgid "None" -msgstr "کوئی نہیں" - #: .\simple_history\templates\simple_history\object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -121,6 +97,30 @@ msgstr "" msgid "Press the 'Change History' button below to edit the history." msgstr "تاریخ میں ترمیم کرنے کے لیے نیچے دیے گئے 'تاریخ کو تبدیل کریں' کے بٹن کو دبائیں." +#: .\simple_history\templates\simple_history\object_history_list.html:9 +msgid "Object" +msgstr "آبجیکٹ" + +#: .\simple_history\templates\simple_history\object_history_list.html:13 +msgid "Date/time" +msgstr "تاریخ/وقت" + +#: .\simple_history\templates\simple_history\object_history_list.html:14 +msgid "Comment" +msgstr "تبصرہ" + +#: .\simple_history\templates\simple_history\object_history_list.html:15 +msgid "Changed by" +msgstr "کی طرف سے تبدیل" + +#: .\simple_history\templates\simple_history\object_history_list.html:16 +msgid "Change reason" +msgstr "تبدیلی کا سبب" + +#: .\simple_history\templates\simple_history\object_history_list.html:42 +msgid "None" +msgstr "کوئی نہیں" + #: .\simple_history\templates\simple_history\submit_line.html:4 msgid "Revert" msgstr "تبدیلی واپس کریں" diff --git a/simple_history/locale/zh_Hans/LC_MESSAGES/django.po b/simple_history/locale/zh_Hans/LC_MESSAGES/django.po index c69ba1ce8..0fe74ebfe 100644 --- a/simple_history/locale/zh_Hans/LC_MESSAGES/django.po +++ b/simple_history/locale/zh_Hans/LC_MESSAGES/django.po @@ -58,30 +58,6 @@ msgstr "已修改" msgid "Deleted" msgstr "已删除" -#: simple_history/templates/simple_history/_object_history_list.html:9 -msgid "Object" -msgstr "记录对象" - -#: simple_history/templates/simple_history/_object_history_list.html:13 -msgid "Date/time" -msgstr "日期/时间" - -#: simple_history/templates/simple_history/_object_history_list.html:14 -msgid "Comment" -msgstr "备注" - -#: simple_history/templates/simple_history/_object_history_list.html:15 -msgid "Changed by" -msgstr "修改人" - -#: simple_history/templates/simple_history/_object_history_list.html:16 -msgid "Change reason" -msgstr "修改原因" - -#: simple_history/templates/simple_history/_object_history_list.html:37 -msgid "None" -msgstr "无" - #: simple_history/templates/simple_history/object_history.html:11 msgid "" "Choose a date from the list below to revert to a previous version of this " @@ -119,6 +95,30 @@ msgstr "按下面的“还原”按钮还原记录到当前版本。" msgid "Press the 'Change History' button below to edit the history." msgstr "按下面的“修改历史记录”按钮编辑历史记录。" +#: simple_history/templates/simple_history/object_history_list.html:9 +msgid "Object" +msgstr "记录对象" + +#: simple_history/templates/simple_history/object_history_list.html:13 +msgid "Date/time" +msgstr "日期/时间" + +#: simple_history/templates/simple_history/object_history_list.html:14 +msgid "Comment" +msgstr "备注" + +#: simple_history/templates/simple_history/object_history_list.html:15 +msgid "Changed by" +msgstr "修改人" + +#: simple_history/templates/simple_history/object_history_list.html:16 +msgid "Change reason" +msgstr "修改原因" + +#: simple_history/templates/simple_history/object_history_list.html:42 +msgid "None" +msgstr "无" + #: simple_history/templates/simple_history/submit_line.html:4 msgid "Revert" msgstr "还原" diff --git a/simple_history/templates/simple_history/object_history.html b/simple_history/templates/simple_history/object_history.html index c1e79d133..58c3d4536 100644 --- a/simple_history/templates/simple_history/object_history.html +++ b/simple_history/templates/simple_history/object_history.html @@ -1,8 +1,5 @@ {% extends "admin/object_history.html" %} {% load i18n %} -{% load url from simple_history_compat %} -{% load admin_urls %} -{% load display_list from simple_history_admin_list %} {% block content %} @@ -11,7 +8,7 @@ {% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}{% endif %}{% trans "This object doesn't have a change history." %}
{% endif %} diff --git a/simple_history/templates/simple_history/_object_history_list.html b/simple_history/templates/simple_history/object_history_list.html similarity index 100% rename from simple_history/templates/simple_history/_object_history_list.html rename to simple_history/templates/simple_history/object_history_list.html diff --git a/simple_history/templatetags/simple_history_admin_list.py b/simple_history/templatetags/simple_history_admin_list.py index e9c598628..b6546784c 100644 --- a/simple_history/templatetags/simple_history_admin_list.py +++ b/simple_history/templatetags/simple_history_admin_list.py @@ -1,8 +1,15 @@ +import warnings + from django import template register = template.Library() -@register.inclusion_tag("simple_history/_object_history_list.html", takes_context=True) +@register.inclusion_tag("simple_history/object_history_list.html", takes_context=True) def display_list(context): + warnings.warn( + "'include' the context variable 'object_history_list_template' instead." + " This will be removed in version 3.8.", + DeprecationWarning, + ) return context diff --git a/simple_history/tests/tests/test_deprecation.py b/simple_history/tests/tests/test_deprecation.py new file mode 100644 index 000000000..7c8ca9990 --- /dev/null +++ b/simple_history/tests/tests/test_deprecation.py @@ -0,0 +1,13 @@ +import unittest + +from simple_history import __version__ +from simple_history.templatetags.simple_history_admin_list import display_list + + +class DeprecationWarningTest(unittest.TestCase): + def test__display_list__warns_deprecation_and_is_yet_to_be_removed(self): + with self.assertWarns(DeprecationWarning): + display_list({}) + # DEV: `display_list()` (and the file `simple_history_admin_list.py`) should be + # removed when 3.8 is released + self.assertLess(__version__, "3.8") From 18f00bddbfdd0b0d65757982197d670315174ad1 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:22:13 +0100 Subject: [PATCH 03/13] Made admin history queryset overridable --- CHANGES.rst | 2 ++ simple_history/admin.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 270a82d44..36bcfd412 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ Unreleased ``SimpleHistoryAdmin.object_history_list_template`` for overriding it (gh-1128) - Deprecated the undocumented template tag ``simple_history_admin_list.display_list()``; it will be removed in version 3.8 (gh-1128) +- Added ``SimpleHistoryAdmin.get_history_queryset()`` for overriding which ``QuerySet`` + is used to list the historical records (gh-1128) 3.5.0 (2024-02-19) ------------------ diff --git a/simple_history/admin.py b/simple_history/admin.py index 758b13f5f..30a5104d1 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,3 +1,5 @@ +from typing import Any + from django import http from django.apps import apps as django_apps from django.conf import settings @@ -6,6 +8,7 @@ from django.contrib.admin.utils import unquote from django.contrib.auth import get_permission_codename, get_user_model from django.core.exceptions import PermissionDenied +from django.db.models import QuerySet from django.shortcuts import get_object_or_404, render from django.urls import re_path, reverse from django.utils.encoding import force_str @@ -13,6 +16,7 @@ from django.utils.text import capfirst from django.utils.translation import gettext as _ +from .manager import HistoryManager from .utils import get_history_manager_for_model, get_history_model_for_model SIMPLE_HISTORY_EDIT = getattr(settings, "SIMPLE_HISTORY_EDIT", False) @@ -47,10 +51,9 @@ def history_view(self, request, object_id, extra_context=None): pk_name = opts.pk.attname history = getattr(model, model._meta.simple_history_manager_attribute) object_id = unquote(object_id) - historical_records = history.filter(**{pk_name: object_id}) - if not isinstance(history.model.history_user, property): - # Only select_related when history_user is a ForeignKey (not a property) - historical_records = historical_records.select_related("history_user") + historical_records = self.get_history_queryset( + request, history, pk_name, object_id + ) history_list_display = getattr(self, "history_list_display", []) # If no history was found, see whether this object even exists. try: @@ -99,6 +102,25 @@ def history_view(self, request, object_id, extra_context=None): request, self.object_history_template, context, **extra_kwargs ) + def get_history_queryset( + self, request, history_manager: HistoryManager, pk_name: str, object_id: Any + ) -> QuerySet: + """ + Return a ``QuerySet`` of all historical records that should be listed in the + ``object_history_list_template`` template. + This is used by ``history_view()``. + + :param request: + :param history_manager: + :param pk_name: The name of the original model's primary key field. + :param object_id: The primary key of the object whose history is listed. + """ + qs = history_manager.filter(**{pk_name: object_id}) + if not isinstance(history_manager.model.history_user, property): + # Only select_related when history_user is a ForeignKey (not a property) + qs = qs.select_related("history_user") + return qs + def history_view_title(self, request, obj): if self.revert_disabled(request, obj) and not SIMPLE_HISTORY_EDIT: return _("View history: %s") % force_str(obj) From dc3a34eea0a65bbcb3b64865ab500c5896c34e65 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Sat, 24 Feb 2024 23:33:32 +0100 Subject: [PATCH 04/13] Added getter for admin's history_list_display --- CHANGES.rst | 2 ++ simple_history/admin.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 36bcfd412..b6db00b76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,8 @@ Unreleased it will be removed in version 3.8 (gh-1128) - Added ``SimpleHistoryAdmin.get_history_queryset()`` for overriding which ``QuerySet`` is used to list the historical records (gh-1128) +- Added ``SimpleHistoryAdmin.get_history_list_display()`` which returns + ``history_list_display`` by default, and made the latter into an actual field (gh-1128) 3.5.0 (2024-02-19) ------------------ diff --git a/simple_history/admin.py b/simple_history/admin.py index 30a5104d1..dfa338874 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Sequence from django import http from django.apps import apps as django_apps @@ -23,6 +23,8 @@ class SimpleHistoryAdmin(admin.ModelAdmin): + history_list_display = [] + object_history_template = "simple_history/object_history.html" object_history_list_template = "simple_history/object_history_list.html" object_history_form_template = "simple_history/object_history_form.html" @@ -54,7 +56,7 @@ def history_view(self, request, object_id, extra_context=None): historical_records = self.get_history_queryset( request, history, pk_name, object_id ) - history_list_display = getattr(self, "history_list_display", []) + history_list_display = self.get_history_list_display(request) # If no history was found, see whether this object even exists. try: obj = self.get_queryset(request).get(**{pk_name: object_id}) @@ -121,6 +123,14 @@ def get_history_queryset( qs = qs.select_related("history_user") return qs + def get_history_list_display(self, request) -> Sequence[str]: + """ + Return a sequence containing the names of additional fields to be displayed on + the object history page. These can either be fields or properties on the model + or the history model, or methods on the admin class. + """ + return self.history_list_display + def history_view_title(self, request, obj): if self.revert_disabled(request, obj) and not SIMPLE_HISTORY_EDIT: return _("View history: %s") % force_str(obj) From 9d232a6ef07a2120c51ca28eafb86e86aa4d48f9 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Tue, 27 Feb 2024 02:22:47 +0100 Subject: [PATCH 05/13] Added m2m_field_name util functions These should help with often having to check which values are returned by `m2m_field_name()` and `m2m_reverse_field_name()` - since they're both undocumented. Also, if Django changes the mentioned internal methods, it'll be easier to only update these two functions instead of all the places they are / will be used. --- simple_history/models.py | 5 +- simple_history/tests/tests/test_utils.py | 67 ++++++++++++++++++++++++ simple_history/utils.py | 24 +++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/simple_history/models.py b/simple_history/models.py index 9ff107a98..03b47440a 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -684,11 +684,8 @@ def create_historical_record_m2ms(self, history_instance, instance): insert_rows = [] - # `m2m_field_name()` is part of Django's internal API - through_field_name = field.m2m_field_name() - + through_field_name = utils.get_m2m_field_name(field) rows = through_model.objects.filter(**{through_field_name: instance}) - for row in rows: insert_row = {"history": history_instance} diff --git a/simple_history/tests/tests/test_utils.py b/simple_history/tests/tests/test_utils.py index e7da5af52..7db701d98 100644 --- a/simple_history/tests/tests/test_utils.py +++ b/simple_history/tests/tests/test_utils.py @@ -1,3 +1,4 @@ +import unittest from datetime import datetime from unittest import skipUnless from unittest.mock import Mock, patch @@ -14,9 +15,16 @@ Document, Place, Poll, + PollChildBookWithManyToMany, + PollChildRestaurantWithManyToMany, PollWithAlternativeManager, PollWithExcludeFields, PollWithHistoricalSessionAttr, + PollWithManyToMany, + PollWithManyToManyCustomHistoryID, + PollWithManyToManyWithIPAddress, + PollWithSelfManyToMany, + PollWithSeveralManyToMany, PollWithUniqueQuestion, Street, ) @@ -24,12 +32,71 @@ bulk_create_with_history, bulk_update_with_history, get_history_manager_for_model, + get_history_model_for_model, + get_m2m_field_name, + get_m2m_reverse_field_name, update_change_reason, ) User = get_user_model() +class GetM2MFieldNamesTestCase(unittest.TestCase): + def test__get_m2m_field_name__returns_expected_value(self): + def field_names(model): + history_model = get_history_model_for_model(model) + # Sort the fields, to prevent flaky tests + fields = sorted(history_model._history_m2m_fields, key=lambda f: f.name) + return [get_m2m_field_name(field) for field in fields] + + self.assertListEqual(field_names(PollWithManyToMany), ["pollwithmanytomany"]) + self.assertListEqual( + field_names(PollWithManyToManyCustomHistoryID), + ["pollwithmanytomanycustomhistoryid"], + ) + self.assertListEqual( + field_names(PollWithManyToManyWithIPAddress), + ["pollwithmanytomanywithipaddress"], + ) + self.assertListEqual( + field_names(PollWithSeveralManyToMany), ["pollwithseveralmanytomany"] * 3 + ) + self.assertListEqual( + field_names(PollChildBookWithManyToMany), + ["pollchildbookwithmanytomany"] * 2, + ) + self.assertListEqual( + field_names(PollChildRestaurantWithManyToMany), + ["pollchildrestaurantwithmanytomany"] * 2, + ) + self.assertListEqual( + field_names(PollWithSelfManyToMany), ["from_pollwithselfmanytomany"] + ) + + def test__get_m2m_reverse_field_name__returns_expected_value(self): + def field_names(model): + history_model = get_history_model_for_model(model) + # Sort the fields, to prevent flaky tests + fields = sorted(history_model._history_m2m_fields, key=lambda f: f.name) + return [get_m2m_reverse_field_name(field) for field in fields] + + self.assertListEqual(field_names(PollWithManyToMany), ["place"]) + self.assertListEqual(field_names(PollWithManyToManyCustomHistoryID), ["place"]) + self.assertListEqual(field_names(PollWithManyToManyWithIPAddress), ["place"]) + self.assertListEqual( + field_names(PollWithSeveralManyToMany), ["book", "place", "restaurant"] + ) + self.assertListEqual( + field_names(PollChildBookWithManyToMany), ["book", "place"] + ) + self.assertListEqual( + field_names(PollChildRestaurantWithManyToMany), ["place", "restaurant"] + ) + self.assertListEqual( + field_names(PollWithSelfManyToMany), ["to_pollwithselfmanytomany"] + ) + + class BulkCreateWithHistoryTestCase(TestCase): def setUp(self): self.data = [ diff --git a/simple_history/utils.py b/simple_history/utils.py index 321ec147d..a5bafeafc 100644 --- a/simple_history/utils.py +++ b/simple_history/utils.py @@ -57,6 +57,30 @@ def get_app_model_primary_key_name(model): return model._meta.pk.name +def get_m2m_field_name(m2m_field: ManyToManyField) -> str: + """ + Returns the field name of an M2M field's through model that corresponds to the model + the M2M field is defined on. + + E.g. for a ``votes`` M2M field on a ``Poll`` model that references a ``Vote`` model + (and with a default-generated through model), this function would return ``"poll"``. + """ + # This method is part of Django's internal API + return m2m_field.m2m_field_name() + + +def get_m2m_reverse_field_name(m2m_field: ManyToManyField) -> str: + """ + Returns the field name of an M2M field's through model that corresponds to the model + the M2M field references. + + E.g. for a ``votes`` M2M field on a ``Poll`` model that references a ``Vote`` model + (and with a default-generated through model), this function would return ``"vote"``. + """ + # This method is part of Django's internal API + return m2m_field.m2m_reverse_field_name() + + def bulk_create_with_history( objs, model, From 28afb4dc8486a7c4563070e25139a107036569b3 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Tue, 27 Feb 2024 02:45:11 +0100 Subject: [PATCH 06/13] Refactored ModelDelta + ModelChange as dataclasses This gives them useful methods like `__eq__()`, `__repr__()` and `__hash__()` for free :) Note that they have `frozen=True` (mainly to allow safe `__hash__()` implementations), as I think the vast majority of users are in practice treating these classes as read-only; modifying them serve no purpose within this library, and should therefore be an incredibly niche use case (except when comparing against expected mock objects while testing, but then `dataclasses.replace()` can be used). --- CHANGES.rst | 2 + simple_history/models.py | 20 ++-- simple_history/tests/tests/test_models.py | 110 +++++++++++++--------- 3 files changed, 80 insertions(+), 52 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b6db00b76..80889c21c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,8 @@ Unreleased is used to list the historical records (gh-1128) - Added ``SimpleHistoryAdmin.get_history_list_display()`` which returns ``history_list_display`` by default, and made the latter into an actual field (gh-1128) +- ``ModelDelta`` and ``ModelChange`` (in ``simple_history.models``) are now immutable + dataclasses; their signatures remain unchanged (gh-1128) 3.5.0 (2024-02-19) ------------------ diff --git a/simple_history/models.py b/simple_history/models.py index 03b47440a..9b5d6b942 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -2,7 +2,9 @@ import importlib import uuid import warnings +from dataclasses import dataclass from functools import partial +from typing import Any, Dict, List, Sequence, Union import django from django.apps import apps @@ -1006,16 +1008,16 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None): return ModelDelta(changes, changed_fields, old_history, self) +@dataclass(frozen=True) class ModelChange: - def __init__(self, field_name, old_value, new_value): - self.field = field_name - self.old = old_value - self.new = new_value + field: str + old: Union[Any, List[Dict[str, Any]]] + new: Union[Any, List[Dict[str, Any]]] +@dataclass(frozen=True) class ModelDelta: - def __init__(self, changes, changed_fields, old_record, new_record): - self.changes = changes - self.changed_fields = changed_fields - self.old_record = old_record - self.new_record = new_record + changes: Sequence[ModelChange] + changed_fields: Sequence[str] + old_record: HistoricalChanges + new_record: HistoricalChanges diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py index d24cb1d2a..bc0918edf 100644 --- a/simple_history/tests/tests/test_models.py +++ b/simple_history/tests/tests/test_models.py @@ -1,3 +1,4 @@ +import dataclasses import unittest import uuid import warnings @@ -21,6 +22,7 @@ SIMPLE_HISTORY_REVERSE_ATTR_NAME, HistoricalRecords, ModelChange, + ModelDelta, is_historic, to_historic, ) @@ -697,11 +699,13 @@ def test_history_diff_includes_changed_fields(self): new_record, old_record = p.history.all() with self.assertNumQueries(0): delta = new_record.diff_against(old_record) - expected_change = ModelChange("question", "what's up?", "what's up, man") - self.assertEqual(delta.changed_fields, ["question"]) - self.assertEqual(delta.old_record, old_record) - self.assertEqual(delta.new_record, new_record) - self.assertEqual(expected_change.field, delta.changes[0].field) + expected_delta = ModelDelta( + [ModelChange("question", "what's up?", "what's up, man?")], + ["question"], + old_record, + new_record, + ) + self.assertEqual(delta, expected_delta) def test_history_diff_does_not_include_unchanged_fields(self): p = Poll.objects.create(question="what's up?", pub_date=today) @@ -720,11 +724,13 @@ def test_history_diff_includes_changed_fields_of_base_model(self): new_record, old_record = r.history.all() with self.assertNumQueries(0): delta = new_record.diff_against(old_record) - expected_change = ModelChange("name", "McDonna", "DonnutsKing") - self.assertEqual(delta.changed_fields, ["name"]) - self.assertEqual(delta.old_record, old_record) - self.assertEqual(delta.new_record, new_record) - self.assertEqual(expected_change.field, delta.changes[0].field) + expected_delta = ModelDelta( + [ModelChange("name", "McDonna", "DonnutsKing")], + ["name"], + old_record, + new_record, + ) + self.assertEqual(delta, expected_delta) def test_history_table_name_is_not_inherited(self): def assert_table_name(obj, expected_table_name): @@ -759,8 +765,8 @@ def test_history_diff_with_excluded_fields(self): new_record, old_record = p.history.all() with self.assertNumQueries(0): delta = new_record.diff_against(old_record, excluded_fields=("question",)) - self.assertEqual(delta.changed_fields, []) - self.assertEqual(delta.changes, []) + expected_delta = ModelDelta([], [], old_record, new_record) + self.assertEqual(delta, expected_delta) def test_history_diff_with_included_fields(self): p = Poll.objects.create(question="what's up?", pub_date=today) @@ -769,13 +775,17 @@ def test_history_diff_with_included_fields(self): new_record, old_record = p.history.all() with self.assertNumQueries(0): delta = new_record.diff_against(old_record, included_fields=[]) - self.assertEqual(delta.changed_fields, []) - self.assertEqual(delta.changes, []) + expected_delta = ModelDelta([], [], old_record, new_record) + self.assertEqual(delta, expected_delta) with self.assertNumQueries(0): delta = new_record.diff_against(old_record, included_fields=["question"]) - self.assertEqual(delta.changed_fields, ["question"]) - self.assertEqual(len(delta.changes), 1) + expected_delta = dataclasses.replace( + expected_delta, + changes=[ModelChange("question", "what's up?", "what's up, man?")], + changed_fields=["question"], + ) + self.assertEqual(delta, expected_delta) def test_history_diff_with_non_editable_field(self): p = PollWithNonEditableField.objects.create( @@ -786,8 +796,13 @@ def test_history_diff_with_non_editable_field(self): new_record, old_record = p.history.all() with self.assertNumQueries(0): delta = new_record.diff_against(old_record) - self.assertEqual(delta.changed_fields, ["question"]) - self.assertEqual(len(delta.changes), 1) + expected_delta = ModelDelta( + [ModelChange("question", "what's up?", "what's up, man?")], + ["question"], + old_record, + new_record, + ) + self.assertEqual(delta, expected_delta) def test_history_with_unknown_field(self): p = Poll.objects.create(question="what's up?", pub_date=today) @@ -1922,7 +1937,6 @@ def test_self_field(self): class ManyToManyWithSignalsTest(TestCase): def setUp(self): self.model = PollWithManyToManyWithIPAddress - # self.historical_through_model = self.model.history. self.places = ( Place.objects.create(name="London"), Place.objects.create(name="Paris"), @@ -1963,9 +1977,26 @@ def test_diff(self): old = new.prev_record delta = new.diff_against(old) - - self.assertEqual("places", delta.changes[0].field) - self.assertEqual(2, len(delta.changes[0].new)) + expected_delta = ModelDelta( + [ + ModelChange( + "places", + [], + [ + { + "pollwithmanytomanywithipaddress": self.poll.pk, + "place": place.pk, + "ip_address": "192.168.0.1", + } + for place in self.places + ], + ) + ], + ["places"], + old, + new, + ) + self.assertEqual(delta, expected_delta) class ManyToManyCustomIDTest(TestCase): @@ -2246,24 +2277,19 @@ def test_diff_against(self): expected_change = ModelChange( "places", [], [{"pollwithmanytomany": self.poll.pk, "place": self.place.pk}] ) - self.assertEqual(delta.changed_fields, ["places"]) - self.assertEqual(delta.old_record, create_record) - self.assertEqual(delta.new_record, add_record) - self.assertEqual(expected_change.field, delta.changes[0].field) - - self.assertListEqual(expected_change.new, delta.changes[0].new) - self.assertListEqual(expected_change.old, delta.changes[0].old) + expected_delta = ModelDelta( + [expected_change], ["places"], create_record, add_record + ) + self.assertEqual(delta, expected_delta) delta = add_record.diff_against(create_record, included_fields=["places"]) - self.assertEqual(delta.changed_fields, ["places"]) - self.assertEqual(delta.old_record, create_record) - self.assertEqual(delta.new_record, add_record) - self.assertEqual(expected_change.field, delta.changes[0].field) + self.assertEqual(delta, expected_delta) delta = add_record.diff_against(create_record, excluded_fields=["places"]) - self.assertEqual(delta.changed_fields, []) - self.assertEqual(delta.old_record, create_record) - self.assertEqual(delta.new_record, add_record) + expected_delta = dataclasses.replace( + expected_delta, changes=[], changed_fields=[] + ) + self.assertEqual(delta, expected_delta) self.poll.places.clear() @@ -2272,18 +2298,16 @@ def test_diff_against(self): delta = del_record.diff_against(create_record) self.assertNotIn("places", delta.changed_fields) + delta = del_record.diff_against(add_record) # Second and third should have the same diffs as first and second, but with # old and new reversed expected_change = ModelChange( "places", [{"place": self.place.pk, "pollwithmanytomany": self.poll.pk}], [] ) - delta = del_record.diff_against(add_record) - self.assertEqual(delta.changed_fields, ["places"]) - self.assertEqual(delta.old_record, add_record) - self.assertEqual(delta.new_record, del_record) - self.assertEqual(expected_change.field, delta.changes[0].field) - self.assertListEqual(expected_change.new, delta.changes[0].new) - self.assertListEqual(expected_change.old, delta.changes[0].old) + expected_delta = ModelDelta( + [expected_change], ["places"], add_record, del_record + ) + self.assertEqual(delta, expected_delta) @override_settings(**database_router_override_settings) From a5439d2d1e274b3b619abacb2700ef6a95f921d0 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Thu, 29 Feb 2024 22:26:59 +0100 Subject: [PATCH 07/13] Refactored parts of diff_against() * Refactored building the change lists * Extracted the code for M2M fields and other fields into two helper methods * Derive `changed_fields` from `changes` * Refactored getting M2M fields to avoid querying the DB (1 or 2 times) for a `reference_history_m2m_item` --- simple_history/models.py | 78 ++++++++++++++--------- simple_history/tests/tests/test_models.py | 18 ++++-- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/simple_history/models.py b/simple_history/models.py index 9b5d6b942..004b6c85b 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -4,7 +4,7 @@ import warnings from dataclasses import dataclass from functools import partial -from typing import Any, Dict, List, Sequence, Union +from typing import Any, Dict, Iterable, List, Sequence, Union import django from django.apps import apps @@ -945,9 +945,8 @@ class HistoricalChanges: def diff_against(self, old_history, excluded_fields=None, included_fields=None): if not isinstance(old_history, type(self)): raise TypeError( - ("unsupported type(s) for diffing: " "'{}' and '{}'").format( - type(self), type(old_history) - ) + "unsupported type(s) for diffing:" + f" '{type(self)}' and '{type(old_history)}'" ) if excluded_fields is None: excluded_fields = set() @@ -965,47 +964,64 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None): ) m2m_fields = set(included_m2m_fields).difference(excluded_fields) + changes = [ + *self._get_field_changes_for_diff(old_history, fields), + *self._get_m2m_field_changes_for_diff(old_history, m2m_fields), + ] + changed_fields = [change.field for change in changes] + return ModelDelta(changes, changed_fields, old_history, self) + + def _get_field_changes_for_diff( + self, + old_history: "HistoricalChanges", + fields: Iterable[str], + ) -> List["ModelChange"]: + """Helper method for ``diff_against()``.""" changes = [] - changed_fields = [] old_values = model_to_dict(old_history, fields=fields) - current_values = model_to_dict(self, fields=fields) + new_values = model_to_dict(self, fields=fields) for field in fields: old_value = old_values[field] - current_value = current_values[field] + new_value = new_values[field] - if old_value != current_value: - changes.append(ModelChange(field, old_value, current_value)) - changed_fields.append(field) + if old_value != new_value: + change = ModelChange(field, old_value, new_value) + changes.append(change) - # Separately compare m2m fields: - for field in m2m_fields: - # First retrieve a single item to get the field names from: - reference_history_m2m_item = ( - getattr(old_history, field).first() or getattr(self, field).first() - ) - history_field_names = [] - if reference_history_m2m_item: - # Create a list of field names to compare against. - # The list is generated without the primary key of the intermediate - # table, the foreign key to the history record, and the actual 'history' - # field, to avoid false positives while diffing. - history_field_names = [ - f.name - for f in reference_history_m2m_item._meta.fields - if f.editable and f.name not in ["id", "m2m_history_id", "history"] - ] + return changes - old_rows = list(getattr(old_history, field).values(*history_field_names)) - new_rows = list(getattr(self, field).values(*history_field_names)) + def _get_m2m_field_changes_for_diff( + self, + old_history: "HistoricalChanges", + m2m_fields: Iterable[str], + ) -> List["ModelChange"]: + """Helper method for ``diff_against()``.""" + changes = [] + + for field in m2m_fields: + old_m2m_manager = getattr(old_history, field) + new_m2m_manager = getattr(self, field) + m2m_through_model_opts = new_m2m_manager.model._meta + + # Create a list of field names to compare against. + # The list is generated without the PK of the intermediate (through) + # table, the foreign key to the history record, and the actual `history` + # field, to avoid false positives while diffing. + through_model_fields = [ + f.name + for f in m2m_through_model_opts.fields + if f.editable and f.name not in ["id", "m2m_history_id", "history"] + ] + old_rows = list(old_m2m_manager.values(*through_model_fields)) + new_rows = list(new_m2m_manager.values(*through_model_fields)) if old_rows != new_rows: change = ModelChange(field, old_rows, new_rows) changes.append(change) - changed_fields.append(field) - return ModelDelta(changes, changed_fields, old_history, self) + return changes @dataclass(frozen=True) diff --git a/simple_history/tests/tests/test_models.py b/simple_history/tests/tests/test_models.py index bc0918edf..39aced23d 100644 --- a/simple_history/tests/tests/test_models.py +++ b/simple_history/tests/tests/test_models.py @@ -1976,7 +1976,8 @@ def test_diff(self): new = self.poll.history.first() old = new.prev_record - delta = new.diff_against(old) + with self.assertNumQueries(2): # Once for each record + delta = new.diff_against(old) expected_delta = ModelDelta( [ ModelChange( @@ -2273,7 +2274,8 @@ def test_diff_against(self): self.poll.places.add(self.place) add_record, create_record = self.poll.history.all() - delta = add_record.diff_against(create_record) + with self.assertNumQueries(2): # Once for each record + delta = add_record.diff_against(create_record) expected_change = ModelChange( "places", [], [{"pollwithmanytomany": self.poll.pk, "place": self.place.pk}] ) @@ -2282,10 +2284,12 @@ def test_diff_against(self): ) self.assertEqual(delta, expected_delta) - delta = add_record.diff_against(create_record, included_fields=["places"]) + with self.assertNumQueries(2): # Once for each record + delta = add_record.diff_against(create_record, included_fields=["places"]) self.assertEqual(delta, expected_delta) - delta = add_record.diff_against(create_record, excluded_fields=["places"]) + with self.assertNumQueries(0): + delta = add_record.diff_against(create_record, excluded_fields=["places"]) expected_delta = dataclasses.replace( expected_delta, changes=[], changed_fields=[] ) @@ -2295,10 +2299,12 @@ def test_diff_against(self): # First and third records are effectively the same. del_record, add_record, create_record = self.poll.history.all() - delta = del_record.diff_against(create_record) + with self.assertNumQueries(2): # Once for each record + delta = del_record.diff_against(create_record) self.assertNotIn("places", delta.changed_fields) - delta = del_record.diff_against(add_record) + with self.assertNumQueries(2): # Once for each record + delta = del_record.diff_against(add_record) # Second and third should have the same diffs as first and second, but with # old and new reversed expected_change = ModelChange( From a842b98a2e36c6ab217831769a1817a28cd59f60 Mon Sep 17 00:00:00 2001 From: Anders <6058745+ddabble@users.noreply.github.com> Date: Fri, 1 Mar 2024 03:25:08 +0100 Subject: [PATCH 08/13] Added foreign_keys_are_objs arg to diff_against() This makes it easier to get the related objects from the diff objects, and facilitates prefetching the related objects when listing the diffs - which is needed for the upcoming changes to the admin history page adding a "Changes" column. The returned `ModelDelta` fields are now alphabetically ordered, to prevent flakiness of one of the added tests. The `old` and `new` M2M lists of `ModelChange` are now also sorted, which will prevent inconsistent ordering of the `Place` objects in `AdminSiteTest.test_history_list_contains_diff_changes_for_m2m_fields()` (will be added as part of the previously mentioned upcoming admin history changes). Lastly, cleaned up some `utils` imports in `models.py`. --- CHANGES.rst | 5 + docs/historical_model.rst | 1 - docs/history_diffing.rst | 103 +++++++++++++++--- simple_history/models.py | 93 ++++++++++++++--- simple_history/tests/tests/test_models.py | 122 ++++++++++++++++++++++ 5 files changed, 296 insertions(+), 28 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 80889c21c..58b3d3320 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,11 @@ Unreleased ``history_list_display`` by default, and made the latter into an actual field (gh-1128) - ``ModelDelta`` and ``ModelChange`` (in ``simple_history.models``) are now immutable dataclasses; their signatures remain unchanged (gh-1128) +- ``ModelDelta``'s ``changes`` and ``changed_fields`` are now sorted alphabetically by + field name. Also, if ``ModelChange`` is for an M2M field, its ``old`` and ``new`` + lists are sorted by the related object. This should help prevent flaky tests. (gh-1128) +- ``diff_against()`` has a new keyword argument, ``foreign_keys_are_objs``; + see usage in the docs under "History Diffing" (gh-1128) 3.5.0 (2024-02-19) ------------------ diff --git a/docs/historical_model.rst b/docs/historical_model.rst index fbf931c4f..32447f185 100644 --- a/docs/historical_model.rst +++ b/docs/historical_model.rst @@ -554,7 +554,6 @@ You will see the many to many changes when diffing between two historical record informal = Category.objects.create(name="informal questions") official = Category.objects.create(name="official questions") p = Poll.objects.create(question="what's up?") - p.save() p.categories.add(informal, official) p.categories.remove(informal) diff --git a/docs/history_diffing.rst b/docs/history_diffing.rst index 3e409068c..7e1edc5df 100644 --- a/docs/history_diffing.rst +++ b/docs/history_diffing.rst @@ -1,24 +1,103 @@ History Diffing =============== -When you have two instances of the same ``HistoricalRecord`` (such as the ``HistoricalPoll`` example above), -you can perform diffs to see what changed. This will result in a ``ModelDelta`` containing the following properties: +When you have two instances of the same historical model +(such as the ``HistoricalPoll`` example above), +you can perform a diff using the ``diff_against()`` method to see what changed. +This will return a ``ModelDelta`` object with the following attributes: -1. A list with each field changed between the two historical records -2. A list with the names of all fields that incurred changes from one record to the other -3. the old and new records. +- ``old_record`` and ``new_record``: The old and new history records +- ``changed_fields``: A list of the names of all fields that were changed between + ``old_record`` and ``new_record``, in alphabetical order +- ``changes``: A list of ``ModelChange`` objects - one for each field in + ``changed_fields``, in the same order. + These objects have the following attributes: -This may be useful when you want to construct timelines and need to get only the model modifications. + - ``field``: The name of the changed field + (this name is equal to the corresponding field in ``changed_fields``) + - ``old`` and ``new``: The old and new values of the changed field + + - For many-to-many fields, these values will be lists of dicts from the through + model field names to the primary keys of the through model's related objects. + The lists are sorted by the value of the many-to-many related object. + +This may be useful when you want to construct timelines and need to get only +the model modifications. .. code-block:: python - p = Poll.objects.create(question="what's up?") - p.question = "what's up, man?" - p.save() + poll = Poll.objects.create(question="what's up?") + poll.question = "what's up, man?" + poll.save() - new_record, old_record = p.history.all() + new_record, old_record = poll.history.all() delta = new_record.diff_against(old_record) for change in delta.changes: - print("{} changed from {} to {}".format(change.field, change.old, change.new)) + print(f"'{change.field}' changed from '{change.old}' to '{change.new}'") + + # Output: + # 'question' changed from 'what's up?' to 'what's up, man?' + +``diff_against()`` also accepts the following additional arguments: + +- ``excluded_fields`` and ``included_fields``: These can be used to either explicitly + exclude or include fields from being diffed, respectively. +- ``foreign_keys_are_objs``: + + - If ``False`` (default): The diff will only contain the raw primary keys of any + ``ForeignKey`` fields. + - If ``True``: The diff will contain the actual related model objects instead of just + the primary keys. + Note that this will add extra database queries for each related field that's been + changed - as long as the related objects have not been prefetched + (using e.g. ``select_related()``). + + A couple examples showing the difference: + + .. code-block:: python + + # --- Effect on foreign key fields --- + + whats_up = Poll.objects.create(pk=15, name="what's up?") + still_around = Poll.objects.create(pk=31, name="still around?") + + choice = Choice.objects.create(poll=whats_up) + choice.poll = still_around + choice.save() + + new, old = choice.history.all() + + default_delta = new.diff_against(old) + # Printing the changes of `default_delta` will output: + # 'poll' changed from '15' to '31' + + delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) + # Printing the changes of `delta_with_objs` will output: + # 'poll' changed from 'what's up?' to 'still around?' + + # Deleting all the polls: + Poll.objects.all().delete() + delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) # Will raise `Place.DoesNotExist` + + + # --- Effect on many-to-many fields --- + + informal = Category.objects.create(pk=63, name="informal questions") + whats_up.categories.add(informal) + + new = whats_up.history.latest() + old = new.prev_record + + default_delta = new.diff_against(old) + # Printing the changes of `default_delta` will output: + # 'categories' changed from [] to [{'poll': 15, 'category': 63}] + + delta_with_objs = new.diff_against(old, foreign_keys_are_objs=True) + # Printing the changes of `delta_with_objs` will output: + # 'categories' changed from [] to [{'poll':xABS<}xaOCZ<1R9)5A*Y*PM0Sx4RnK&8jP-f$R|r!mQLoSkM^d* zo^}abZ@jIFw8%S723@kexa3(!KhI(2FYsj(u1~|4CSL1_+UtdlByhw?X3W)@1@Ztb zHEl3_grb7B<$q!l$Fq&Vk=Q`W6o@K}RaMcTzQqJaTU&UfEl^YhG=@W9B4{J!t>Cui z7)-<5Bz@q+!=c8;xP>iiqlm8Jc3I@^%L^+bnwT4rHBnGVbT@{}nEi52qT_CCIX7F| z&{s5mOT``sn7x)Sy{PhTdy{qS@TK9;S+UmmbO^s;9mHzs^1mfPd4A}cy#C2z48&u?OQ_r@fo<6pgI#JHrY<*KO0NT5~?Y<$qGnVXUM z3>ZG7Sil ^{PXePG)D%EtwQ zrO3~#EF;df%QTWtFNWhi@*)=E36@Otg~ckvdwY;xaf?&7x##Pero#0)_og3eFJ~Zh zQu2ytVlo$U&WycyOpj_r9YvDb-FomEDMD^Y))mKV=IPW6sejp=z<{2Vs9GOzxDW_3 z5+7Bz(W`FnJ=Le51lxO6GRbFmJX~HmeQJ(=Xw>@cSDxxNfosIus<$&uTSXGTwrzf& zw<=2}ai(GUDkAR(Mu*XrJE3p&p&GQA_jn%SDHq&o(4J06z!&N357r=O?GqZt*O%QD z`4>IZ99@P-C+v->j~cVLWIOcIzeew3@s2SY=9^-LbIZ5L=+3#+7x_`J$hY3&z2soj zl}|s52+G8e#D|X*7w0m_Z>W#^;#3Y~>vcZ4Pr!MI+8d$8f@K=sB#O*iUYdu~7J9RU zUXUXJch6qnz#{lf?;mD@6N8knJ#?Gi&!O~{6zxjG20Q*o{EwSA-OFA2l(Tkkz@dAu zPseN$t+j@zC}aC--!dw8;;~uMoc-|(VAnq1r3bcJrm@&$2GlHao*rBq{2WV0rMg8& zlzNfvZ6XZrvwZJRE2R_mt|b&^X@U2F&_Kuf5NnPrKI9N7xntl*?8PwG4aJMrcSYOL zbN&Vg(6X5{os%3|WAxEo`nNPOzs>uZ3HL9qou4qv?sNgiy-ofpCYvCw9u}TdAyD~0 zs-zb;iSx~`xUs@~l(7Dj*$_#Y7a1`Sz@MsSBzu3U)JkdZW*mpQ_t6Y#{Y!p+5Ob{m z^RDqR*g@ng$XVRa10$SwD+Eo@hUCFTn+DBh?kk6laWaFH*pAojqfGmz$O4AFR8Mw& z$Vm}(rPtZqGD9=c1#{koV~>h>HGA*t$QBr-xUhy1vq2|=;b&i3WqDv=tB6ZVev_jN zBdTfVE&0me#+xo6j=%Kw*3I90x+`8QD*F(0KR85N8`}PuSmeUAdmZLAv{@O;{ZKd& zjEgx09l{0r{m>Zt%sV0*&QAS}M9e{$OdSw=Fv1Ab?xJVvx&bLZw+nqJ$UXvf<=q&p zoteY12KPO%RNXmqGCwKHYDAczjF`z{sQ9K Q%S3}Kej&WM`dDa-ePyHF ze4}mUA o70oERp&(LJH zY!hiFJibVIWA29JutHH?cT%hXCddK}x9xcD`}@^)rc>5*$l@-81IgJ7v9HTz^^ma= z0yXbK1L*aN&KYG%(rwpfOUmCWCeM=bv#nC$1T`_k<_KStgs9h@$YMZ-BGwZ{(<){* z0u%A>HJcuuyeFIg(5*&P9vUwKhq-bB^DDmA8fK%x_bJhb%~Z#aAtzUIrmqkRl&}{+ z5;GRr26mfW2vm2GDyDyu6Bo<;jN@M6xT<`ZILAp;x=~r -N~9+lCC5yL2}2uC z2|v9DIlX2iUwL{^{U@F3&u1gZ^wN6AHLcV26JOUBPna);k2D|RS2l{`(nUj`-mla= zt9>+f%a*5gO(@FlJ8jkndfKMqJMvhv?S&L`bNFRe)815GXVR)us6s~Xe$MjwtxAV4 zGpNZOiSb1DygScPMovox-{vN575Pr9@D(W8(5~EVt#`wrzc?J2PFq6?N5u?mRuAckNA(vEI&T~$@3~8x0bl4v{xBTc~ zdvLh<-_k4n&j#)@t8(_9s11+A=B`a@A)_wMT&bg9p^oLW OXC48JLtQ1a6iWQ9+*`;pZ(@5f}4&<6)?& $Ggh3GD_y)0Si_)&M(gS0Qi&!Ok^Fl?AmmY(2^+2G z>X8eC?1}_)gL7(R-bD@q=``l}h9Ou9nr-s4SlD7uG80Xcu~voGz!BFDc7_UYrPnEd z@Nz!en1Q;kN>bjp5JI0^;FxJ_A-ErZ*}8&jc&{6JhX;oz3F qZ#LpbDgsb@sx|Ncw|;#T9MEY z!3k6ePL;qQm%850H?kYY&m_BW61y#<{9k&5AN3jcfe^m-N1x1+dn=%j)I+!>1L2nA zxixpY-AzA;bC8A6vn7TuoLJ5Cz;Z$Hr<1|=XXYaSbvS 6qBv2w%= zu9zMKSX^1D%=vnOW6@=(!!%ayiso5U7CLRT`Am&|*;ly#oxQ>#Aw}~;w_aj&d}^OF zF5)BB-tT0NjWU*eW~qiEtPeGJ`$}lc@$R+_dLw-AcDsaRQ4^+&4Iwvj?A3h0=J`kX zX$hx=>(GgXRap0}k((Y?CjmC7#Ha)(h^BZCP|UfvDwzF;`4&uT_lSkQ#E}G@RCeen zC)l33V+J%cV2&SZ=M$4OM6HZQ{RDTDm6b>13}}`3gR%oLq2^%_a(Q`~9+bM>^pWg` z4D#2rBWH({A7qh?2c@x@cS$s3%3OUB`F*{L6U88me^1pfeSnCpB3HBD&dyG^lSL}p z`@zBiv!mk1Ee(PVd1)s6DA{~C|I)cKUV*8)Tc5>*0EA#AN2~A>+#_ ;O`&dF&6Z(VvT;lTU*<4sN0!V<5A15O`DO?l5*g#D~|MYB6&AD%* zG|5lq_&GRxZU)2C0Ivw7gbC?+h&=t>be@BPfhd#+kNM==m>CTc)?+@AR4%UHBY(Mx zK*hR?+(tGZyp7t}W(Q T<0 zNWoI!Hz9bZog}Nssb4)Na;dELHUWoJ{`}`2puhhB`k#B>LH{|LZX6jbo*HKL6@@qI z`=NBCi@mQk9u{q3>y$@FaZ}2tD#Dg2f$C1wxQy%5wZp>%f#a-)j@RwRhnL&2&hST8 z*P~)Eeb$GPqQbhl8RD7B%FG~bmiJu0v$Tk9KF7RdzDzJ*NQSwaijM~k6_hks3Y%ps z h^)Mcxf_bVCIcnfK&O5>@ve?5Ak7ig_7g~vsWFampecIHurS=)ta?s`jn)p zG7P7Ee_?N3CeM!vr}?I5v89uV5b0Z4#ayjs{CINSz~l7aS9b$LEx+s1S)irG7x KK+(`u{5NvAU#)d z_{b5 Ythy5ZW6GnQ^gE{$f+ac~ z2p{nB1ycNd^h^FP`DxZpkG4Vh?d#0Uf5+=!e=({ED__c0h3W~CST~&fHn!TS87|jH z6YIZV1J~I&<0fKVCbj%<;*WEXcy%`0)XHC)N(4EuYdO+Vf}g#H3ud4fH=@y*zrVVl z^{F5{d2-4->`-6a7|InN{A!?X%S7RsB?qA9gxo6Hd|k^{`InB(JNG{yq)$q6sDxLy zPfmSYG=;Uf%~yz<4nZ4{OzznNM#BJy;^kF$h_)>wR6XG&vT%67%xKBID6R|^6o76@ z+x@aRA?=lVOA###w0i&!#5QN&6eLw{Bv|%mcK;G @xU;tDU3ml%8fE zujc&93(ZlcuT8I`g)R}HHd_2Hh?w(0=fXk$fcfv!fqBB*{+v^D)1i$h`m&Rdq$)AS z*%8ak4s#ca2c3667S|qM3J;5- aBI7c2o5a{G*YZeV+3cH;$lMWpz>%~4=&tN* zu?yaZm9Rm3Y@G+yZqiAKk9on;hvjV+#ZkrSmfE}a>`}sYJ?776J*|9h?Xpu(MKdS; z#W~Y86?q<^CHixwt#fU!h25EW4lk_9HnM3G_|EXF;ED~^FV6eiy~f-sS~)uv#MRN< z$!xQCIOI53do!qRZ6oRHk+})yvB$)&@KBrd5Qn8=USyLkQ}IGLq11>Nm1ZF8 c;X#a%u*lbtCP|@(M@sZmo zZvG~b_voXCWAkvf+72X!H*=D+&{{}LI}~t!Tk_o+;B~F$F0%&=C$);Dz5JB8BLR1) zi;8!QFg;v!X8X-Zj92djdZ1`tXJUc_1Quli4!7w?Y_{Sfymr}O`lu#u=&0k$jaMOe zsF#rfKc`O*7|DZgB63|a3pVt^&KEAuAVB%+&AayUeNf}eIMp<2^(Ikwz+Jfko|0J= zxt(<_Ji=EmOdXg}6X7|&o c3?-l~OjI4fG(I z@SCN+uU1(3;mU!-?b?1#t+XT63=D3U>`El|Wigy&_KS a z!S05|gH^{`M-emkUBUx}eVTg6>Km6*$9^LDkJ*<$#?&;~j=5OK-Fuz{4iUIJ3YpKj z{vGJsm!z-nMD5KL)_t(~Ew%xu{KJ7D;FNCKfx1KT>i{LnL4I+TjB#Cs%5;`Hx$pk& zf5WH!E7!=Cz-i%3>dJic2?d2;4G;1a6rMBIDkvyfd~4i<7XsazaFBw6{M~9pi6Za* z`w*+32l?^|S~E>aT0E+li^44s{n8`G8Z}ANu4`+9?xd}+-WqmfI$e%>e^z$3RRGs@ zq1NuM+a=B#h3BLB6@!zmPPH4g03zNP!(cR>bz?R$lONjT1FW9z0HVE_oUC_}z=77% z%eT>}t&s`}jtzW%DBHs025fIGz{VTX?nQ56nf{zFRz>8hSs_@7Tyd;J6;iusqm;_o z51DL1(01XkT<&xyt7{7ltT8<`5%IE+SCqaXfsgEq@Qw_+w2{tI+^ZUi$4}U?Tb;7q zr|{TX*}gRjHX)-;Nz|xMhuulIv;+noN~QRTmob^N>eBHXZEI=zO6bDm=g(b4j0Yyj zZib8Z4i>~Dk98zI;GwXVMSg(@cD!tPvW+eK+#73cnUK3AQzuD9cIoHbRKZc#o0lHG z-(a>aQsLKtL9AdvCvtvydZq661a0G$RFqU-Sa&NlbL?ayTchSFWwvFd z)m9@lAm1YDFdni }?>N(kA0!PpwdsFffA670o9&n3lQZ!OD zI6xYhuz1?58@#+w9rl&Y0ZS$Ka?y 1VnF*i#mgbylhYnonu`uIaZCqKIJ;rR(n3RrLIPu7E zFgqXEvm;&lbn}uu1j`+sKz }m13+=%zI-_K$hVh?i zqF-H?{ZMq;(mrL #rSoV=ha1f7qfj37+gEXXjboyM4_|s39Ep>p>*kjv1 z*<^yNNPyX{M#5!amL-)1N)n7iH59}X($E2$U~^McZTvOqQpo_;b;(d!nqL`+7`U$* z5k}01 z@*v#7&Xr5Uu^(>ttr5S?OST_{ tz_SJHAHZkytEa^FtOv3HO`P<@U zzb>;otW?5sF%3DyN{0+q<0C*#@_tKt7P1A@)S&Mj;t(`kfG`?8uw#t=L^|z83_-_W zapBKtT^uVKy%l#)3Cn4!OW{^Xy`sDbHc_IECTZQ!P zr*Oh?8gQ9gMFo~yT~!;vh#<0n+{-i+3_hXy&e*MMMz8J7hs(N#v{fS~6F-&gh#tRC zu-rz0rj`!zG{X!Ie7(apyK=)&d@ijzC8YD9WJXysJ*3^5Gu!tQH_-eeF?q1R!|ct- z>_?}Hsv~?4;x 8+Be{%Ebh=!jN^RC$B()~PW&JIYd_ zSv`$6f7UnJdg?D@<=HI2tGsjjJdj7c@$0s6Cd&pW9F8{FuT;^#dh+ICJhM$VWCFGn zyO4fQP*FZM^Vw3_%(5y|;`0@CSO3 @yaJjJGp|aUAy?5?n=rb~4PA~(mC`#ENGdldo z+^C-b%ZX6!9h@IzdB(uOO dtgeAR)}ovL}1H+OSs&aY`jtt&|Rfw+`6bq|_r);Fec zgdf<=%2B5zAT2@`pIEI?_+20H*LG&u-kCQGN)pNN*k`UZT{_@5uBN1L^TiFQz_-h< zUmjiLX#?9b<@w5T`s*Z4$&sw6@as$!;8&}8RMj&JfRk7NjA{X49XM?7^x59bH(t5n zlnB5;DR6(02N!<^DDuX0AWHn#vCMxD@qZTW1b%KMLj8Pw4;(nqrm(G6bzhBbEwN7K z3DEgwtUucSa8Dru;p`l2@cc~p?K>5x!%H}PAXLiJ*4Ni3XBlr~#LfDyN9|}>bBr8g z= *9-O^BJt5FBV;DuutH|{2p_eVZ znl)4$n2M{6u>4(p8RVKvI{fl>ajSkzxJQ5GLiP?}XToxY&?10)a;73KX7*EIOb9fO zH>gCq#?qSEXJUNbra*mY+jd*T5S&!A-DrkgVN13qD}KT0@Kc(o+bbqtsyY2EYMDi5 z6=E)PO0=?@RSCGxy_XX4N(AaP`)A6XTW+7*qYS;s0X{qEdvsrIqm$@!#YUHKs!L!N zJ-{^g^_=J)C_Ip`?Eb<}6MLweiC=K+(l2W}%m*~>Am0X9r*C$#X>q>qBq }Z7=%? (Lf4XARI-r)?tEblO)^N}C%^ z5TqnoTNhzG@f?+fJ wb#URJG08!&l8}XczsTisj0~Bw z>H^=78_zrrtDv~sPDTG(xmV6boy9$CPKm=x5^Z#w2*^SGDw5J2N9|N{m9S%=;~o*n zv}xh7@E6-6fsNcMQbOvuUVchWKP2eOeeSK;OdKs_n(*`cR0g(*9;NgV!~ iD 7n3?L>9^Ng+3|Nv@=WMmyyfb+Qx8k{e)Pb-3p)>M}4EiBJZME z;|h6gjSq0Lelc5v+M%#Dl41AGb)5o1W9BcjPu85{Y>O=20yKcbSncQO>FMF|{`$?J zJ5ZbOX5)?WW4jwTw!n$Ts6AI5YTZ1j7Z032P~a!P2525Q0Jqh(10nA~PlXe|P1z}* zQfUX%*WdE2X~%Q8+ydM)0{s`8^et)nS9T336qYma>lG9NZv%b}P{#jjmJc)dALi?v zwi+q;ac$B3-`4;F>2@T7E-i`{r<9eI<4>v%E#Jx58lPkb`a=rjY}1WyN)3nAbQ{(H za&*=LEZ~Dy3`8wA_@kO4AYq%dfu&>Jd&N}uR*5i@z1Kt2Xoi^YZdr|TD9&KE=?smz zvF4=Sxx^*>%r?AAstLOOm*9^I$H+8&g7j8A;cZx=S(kWQ 6+FF74pEs>lAV_fs`R~24pQz>DA+ET?ZQj1e@J#PqC8wWevNu!< zvv1a?9FvgZ7U|T~p!} u%f*G4IQrtdk(0lz zxe2GqZ#(@mvIdVd5a1+)CTvSq`x~gmW?Rmo$77ykNFKt4Q?fvK0i-xJxb975!tMw- zjtnnMH}y>WY1(Wl;kFrGFY;)`?0ZBUC9 +&HX05Ou z5?cBZKr&sxH#d)o9p`6?;j&}bHSEegp!T47x0=?+M3!h22%>4AuZr^{0dsRLsi9$v z%9fZl+-4L07BeI74QkRWWG9YRP2}`h&K$WchV%gh+hO5szijeyaPb37aY=n4dd+i2 zp*tD%Guv@}e0CdVCmGVYT@O{dvhrJ$??vh?x?z)O`1C0;oRqflv0FGIo}r?I^_-nm zNec>LQfRN!%iDI3j91*S?2cU5-1DHDp6>iJVj-mm8S49X=9(DV(p-6?Bf*2!r$T?N zB9bJITOkUw-8r53yDtgU^f$$_ak)-31D_#W;BQNQm2k4!%I`3^4g!QBWd0-J6p(S{ z$upNLJo-;nDn`s_!FU**T&N;ak1ISP8lL@?(!tA -!($X zetAr`XAVj`EAT7hnOH;K$2RpX+Xzz448;*61cW~a2O<>$?vgV0bKJ8JPl!vlzU%5b zb=>D8I)REH3tl7PY);wl5Hvr9Il}J~&I#cN<4dnlMguX{OKB8vVMwAjf8y_=N8!uV z!7n9iMT _(hEJPTIyY_e7se6E}Uyc(d3gpW@u)s)cte?!T5* z==53C0#@qK{s?0W;q0T9Mj~J62`oVgYua!i9pxa~Ed`SCn{YhjvXn4%nE+=yKf1W^ zDhC=fqfD@**&+MxZxsdWIl{Ks0G}Uy>DvY$LABHz=5?oZ-hTP_@$p7)Cyq1633Z3+ z%qgT*mg%~X!UQtWg1^@aF7G_2b3bgmnN#)Y=!KTbUy2ynPUWZl8c9-m;DohTC&9x| zE*y8VUlMQzx^2UDV$E9*=#OS~NdqN?=mcIlqvF7auVxtBR&y8q*W&YJRO{aY-R~KS zp|8%8?H%n9x%o7iH@E2M9k$4-J1zPlAj1)_fCLz{w6c6wR0KxhqM~&L%;^oC55;dn zF3bTT>()qMK|~M1@>z%jgd@BR-A}+-UDs%v*(zeCE*tITcLnPB-JuRl3+rCPNv!kH z*%&Um2>62S;9MM+>a2PfEHV(Haz9pNu}TlHd$I;ds{N7Ev}e!&1P8p*ujiZzXRn-_{z9hf*HNgB*_%Y$a QrRP7YU~nW@JGORTI99D5s`;P_ia9BJJsf-DL#%H zX4k8U=7m#zxvbAloTHtT$UBypBqG1oc0gqguxnk2pSVF}2r*<~f(d&SN42KeM&b2_ zsJi>WRnlH&rO-(3lqqn!=m@|4uUbTKxVpLd!^(?3F2hqdAe|GaLPi5(%afEO%Svs` zhRh~IGZj&BZohp{pm@|DsU-0ngRI5mwL-!c$-S&A&rT*3E}xs`Fq};rvLb$9oFErM zm-#VH32Ez_r(iK~gzWvLAix_84UQ8%dl;LlanlX0w^o)2mIjMg$RK5_Y`V6ATq8k~ zhcM&6brptQ#_f-CkWbkbd5*(N+v^D7_XI_6*<2MRiFtE$U&T3jaHF{R*bws(?tq_l z@{$?9E%B fvL*0}$A&Y-3L#2LABOKv%cu&6_XAp@;r-Ngnx6Q&K8%lBy9#W^i8Z zCbSx0T4KtE$6AS7wrnA1@61L)zWG2U>y~zfRWF!LN=aFNY4NA7+PU@Cr-_Qdpn_bh zR(} ;a{fTsOb^n`wRrZHqDG%!k>bv=N&@Ydf)z1i^Ir< lZfr5mj3TzD}3d z7hcUZM5*F*OkADgMrILs4axg%FIkdQ3>?>lG*_<0+38-B_N>+zkvsS)Xg%x>eo`K7 zD_H;^mi1V3F#GnuvAhopUyi+}uG--tuBWTMc;-xNyvq-ZyHUF#`8-EO%OyPH7QjZ) z%5-xpS nU^LjAjF(-*XsJ+5vX;+A2iJ(6zsDWL5hM12BsnRdgk z-8{8Riys?Hq7FO_@0{k0crl3|mg1_w-FezM+a&!>am;dUzF3nUL$!Rva}FSb+%3Wm ztmv=wh=I7sojk5_ke~8!x&HC9v`RQh`0I@NLH_l>qM*82CLl|bHU4(LCE&TmWfcdO z@_fmKhRa`LfRf36>C7C 3 {bH$ z@590LS2-9#WCt8!dPg2rvSJC;aAEUH%Ys=oQHi+IH2uojY)d$)jSLfqlvGz_F}$;y zH4{e?@6ERoe4OfXs`#x}RYanX#P-*Rpk;&%*q{q=9VE@qu)K-;DZwOQi!>7u>dqToO>xUkoSa z;y`u2s(9>0%aqfkv||Y1U|>*qb)u+F%dK*=T(~hlDv=EynU4|WdX&NFIVT<0^>R(R z4X1T0OUhK)FVk?pvWtdQY$C3Hw`e{5>!-}HI{oHy`GJ%qRVEwB-8_=HEV2Fe?m`W- zk>;1*A=Twk^pHDr)s6*|z;}ZGQhR;YbJZ*XMk!^gr;t6axLfU3uv*P_xglE||8B_I z2T3FRq4O~f8u7=BH@g2Mw>^ gR>PGyxEUC9T$+}D?HaQ{4*m<}k%EHr3G&0c?yngTYvUbyfHdDtD#aP_FA#7k zn3=!+Md96R*Sa%#t1^*GMxOJ@8T)>%_U&>_{wo)pyomLOWTq+@h^o+lo&q-_6vcJb ztSz{!zOj17_6Gn3n@6zxJbdHgD7wr&Tdo~E0h|`tQ$W3;HC-cE58%QhR75vS%}E >v)a*s>^Z*e3jfS0AZ2bgUP#cFDrLtK*tHVg8Sb61_X@%2`v)WFcRn| z1yHlWD@OP^z#RdL(Xy1W`7OZtmarG`@}%@2Kaj!z*BH}`Vdyt6IN#U`tYm 5=qJK6O?a!;7%xGaa& ;=imC>lE5#OXh3R{Q1jc7gM_l z*F&C|E3?19OkN`Q`k$o`H;MY+iF0ZRA&)NAb_WU2rGuKQ8$ En_Cc)=qUF0xH#NQ%{cQ-h$1s*PS4+Xz#kF+XG#9i z{Vwi5<@pcN|Go{PJRlP RavivFaq0u0D&1r9xZ+!M@*ltdK$ODjCjY4ba*EchicbDZyY1`eH$Fa2 zx%m(J-4G{}@+vD$3U>o{t>^t;l9Pc7)b*+E{9wJU@z1XV%EYC9J3FiDFHEyc1rcs9 z?+31I-N=NyI#u_PQg^v}1j8Iw*IJF3zK2e$TDTfhode$ev*7B#hmc6I_D3yK6%O!QI^ 2kt)BXWutuFm_p8cpr_^nwrdEOtM}I z!EK0`sy+*{Jy*xKJ~*$kCCFi5e)xY w;NC8HFISbqqH z1u!UYSfev3ZpfflC@p>HbtGSfg-Pz+9jb*Lw+RBOWs?y>_Ex{o>(c#{fE}k4dH)gf zA`nv?z|SD$6N)0jxBl}ELH=6k6FU;m-Q$sP*?=hHmckD}LSN>&Pr{~;w0DNXui#hK z9M$}6dhzUKOA*VoigYsmP1-30*r?Uy-F8%fdV%hbs%7Calk>Sda0qy_Xe{I|co30X zGOplHIw^SA bzWe%GYn1Tr=j}Xf8r7~tog>k$L%n|d z(`yskx8g%SA`HLL&)VtU(iD7_L4e7{m5MbYn+=?oh)KoFukNr8?OslTB?7VO;j|NX zTgPxveCdkHHyh~MtiP`D?D^cLQLND!Hu1Xz?wes=f#AFgtUhu3>+T2&S{jS*;(7R_ z0?}a#?+6P9h^??C 0&=+9qD7Znn5ncV*maKQ6A1Rgmbd5T zn!-9c)N=fk!9vt>*cf`qIk0#EaJUMc39k#jzbCUev^@xF=b+s9j &KBbVTUnvT=#B9s24|CW5V>FTv{Y-71|7vUp zWl@ahrY?-ph`%i7b$R3~&Izpuhwq8`+y;_pltFJPe{6g)TSmj^emXb5MGG2vWwzP& zYQnpLhM&3)n*_7sEiW=RjdbX?eh(Y2)8}V ^KU7wg4D@LANPIv##{Y;Zt9~5yollfYglPa5uj-TiE?%{pQped>1`JmWR~VHw ztP!rO*y3kFvf k?(|h(a z{xGhu1a8HW;iL^v(+tE&&aAdE{DkrGUl|HhNFc-+r1#hh40Uj@pbjJDXT4{5Rr*V- z3f3BK>6cs;5{Iw~h%%X@Q2!1MjR$SB!yzwDOaWZ+{2Se*4|5MefM z(JNj!lic?lD6(H@au)Vf>W~?Q6SGv!8Jl0(e2L259WOOUx<@ }Dlj59Nyh^W%eoVhV@U+CK z_XWlRgYT8Z#rwqi)XmZ&znEpn#gE#iPx0P3uTG#@FmW(3rWfQfX4~Ww=da|o=1uLt zxikP0wtjC5&=VJuYZvz^9U3RD7@ER47@aAfwV7_sd(er>F{|vGNX=8vQ!j2*KlRgV z6ck+(&MY09%gog;_xb$N>|F`lWsoOaCfwFx=zol8hL%AXi$;vrg%%h@6_kn=MI>l! zfToR@NN&JjU~QUa>_mta@jk*$j7Mx+j4xs_92kxsAx=O;B+p&JC1||d(Vg^0hJ-x| z^b-{99&zU#(t|mQIT0-?LKvh&2 ~2HQz2Plv0S~JdfJas>s_5)=TSUe1w3ZFYU%nR1oi2eCDq@}qxD|+Uz1K* z_Nx}Z&aIf+zHbCH0vjK?lDl$)@i6QNpFIOsLh7)F#M21R2Udmz#=L4Dj$?u-`@$JW z7T6$m7 E(ce_@c3a`0(uX-bRdptfvTrbA1)=plW>Nd?M_C|6^5$EC0 zUU4Bh!PmgC!}*{cA=kY5jWmNOfZTwNi)i=SyYt)5mc5I8MsQPbzsPkkc;o(?nPXIt zVK!E_Nw&?`ps$i&t-4MEmjcX!=Yns(xQPyje V`PA1`{H;BlGBS)vUEt)~ zWB;w@dEs09*KyblG+;S2Nu3loz5!av j(m!IqsNf*+P+L_PU-kO zuELnf*r|rRD>>xUDocP_vpUe?zWzrfcA-2!`+&95YMw~k74-$p=?Ix|p0V}V?XRfq zIfw_ldopLT09=JZ{dzKsMw8M!6$++mNQN m=&rwZ`DIvsB{Lj2Ex+% z)6jYB9E= ulqiMCnfLFA>LmNzB+F#Sqw~F%W)?$rF(k^4TX!cXS@r WM1L#wU4Gb^X)_eN+nJ)wGyT0*&Jy(OfCJwBI?yR4u>t>UC8w8B!)sL^yG z1;eVxs!}aj<+OygQd8x^G~IqB|ElAVh)cuz *(pr$YJA?&8kfVo+lHB{$=a@q+s*YS$>vu zA5*a2^A*bJAgGj8eW{X4x8<9_6R;0`BchP|!nUnR)vfgO8q62KXX1Kws Lg7cGakEd zmVV*5t=$1lU3mE*yg0X+dCKpi-i1B4 8S+&Ddglh*wXi&&o;TQ#1MZ^{C9!+#zk_$9s`79uSl&bblGlPVXi4c5_ zxq5xmUI+7i8t#|4yN}OW^Ye35>eni6*iXEwj_oS1u~yN&(NwZasXN`*>F@AN-hWRq zu3EW;?k_@3G$c)BWnt)`VPqJ1SV9;CXb2Yi2*MKmcUTPeBh0J6;^AOmf-PX+|CvV) z`u=l8L!Uoo{_%bl8w7&{{q+|5xMjipS8l}StXKaPhRcAa!3ZmhNJ>KA%0><*Cbo{| zc21_^D7nyx*Y*;cjxaE|RDT{=Nu>{G(E8^rR5YA4WMz1b>};668rvC~FuB>-|EULt z-;Ea >NzUIH78sg+dU8 zjEs!m!Pt~n>5JGui$i}2P?$S8+4C|pySlnExw13aIhZlC^6>C5v#>FCxv~{HXM &*eF3@>=Z}CN51^R}5Wq&SMdFY>y|9Jli_j?`?hbzLse1?(yBCO&D zdz6ll^sWy#khx*+_sL0zE*yEchz=Pb9FrVEqWJsycXuHEc|a49Br?3KYh#bDOSP-~ z)vWc1byPXbP{xDTaGi>}`itsH?exBfU51`3zpI4#htF{T{*LZK-pU@2?*fL8P{F<> z`*$}B{70v6WYI9M|Lwgc0r#%i!ML${;@@}TGc@ZbM2YY4|MqtH0E?o_Q?oR0{_p!r z2F?2Z)qm1DPb^Ge#rp*tYmR?Eu4tG*rpSLMrayA@eJ}YPpdNX|#g+2!$0hL{fY 9 zj9KXwBSnV~#o_;m*RaOqUdOtLi C7^F~3%r5qIL0cT1x)sWbO#=eID7H&D7K|( zJQjg^DPAMxTlufVM!!Udr|Exb*uNc><6GVx8nxni>m0WhlQrrX`@he?dsO&H_iR+` zevt}2qWa2f;(kgZm#w&OVJJf(AW@TxioMRy?Y7$a9=W;&CmXe4gX4-+xf#I-k;HJ~ zwd2k)z?AeEhhSd;sEzBse>1s&@@${Utj)ZGtRm+v4#O)i`M=Mmy$t53>p&)SX6~($ zJwW18D$-27)fPAh(y{k8wuaPq|M9qCic4~c&|_ruhcxO?Si4DO()t>Tt$-4Iq-l1k ze4mYj*hM DIW~Jeer^)OS5=YsWVR{N}L(GC&oX<&vMmD<}J9q`4 zWtsoIKcC@vB0pEWZC10UBO@(w+ty-57t# $#9o-{=? z+nfiAuZbS52$I1J9+k|wXNG?|mN;>^@$LTtw0#{`zbatv=XT)Mo&I3JvA<`E;USny zufLJMI8q1M?31|$!a@r(*i3yM1+ #@XSa%`e`1?-s|9}ix zE_4OFi?-6B)wKQeE$k=lj=O6_ivE|?$CjV+lD2~*&}3rJ>KHAYlAOhE%%NmP^8bZ? zHgV`0f$xn>b=~iN*aLKVIt{L2^KkB9$p`UpLow;R1r#Vmmf*=x7q6gkH~rh9*AIe+ zY!6ZPaaVqno;`}*meh+h_Tc;>iXxF3WMVRLa9rcC!ql?hnJC5Zk?|lt!2*?gAc2@x zsnPOVv09tC0)eZ^RD*GHwOj2%K+U!kZfte!Y>Q|olaXAmTK*TuwoZhLp_GUgS=}P` zG#uKW`YCgaj8~UBrL(88#i-m?d{c?c(L$9@f)(H+K6?qVF%7&5-r82ce&=M9sibC$ zMdPq`+lX~--)=QFvmmETN~5C7(duq3&3(aQJa)cVk6}TUhu)3E<<~h=9H$c+#?Y~h zttU3u1+Ik#`W{Bh1d*XrA$tWeS$Uu1h8G)zrTU723-08Z(7~LTn(v@vhozRUhkD8N z#$mKZLRz!TlvH8vwCvPlYaf?fdq(rE2fpxBxNs(e-14oL8@kj8F4Hygq9Yd;YwT5H zuOCc_&3;U(L=mt%`hGZFvKz$h&y;@vdt+8+NjaHJf~jiwctlSlf`E2qa~?)U3RoT) zqNQ{DtsAV>2vRuAbc_eH;~qw!NYMI2Z$kB2ei&Yy+Le_=;3Sn38cB2@eVp-g=!b zZnv@^J@NCLz 2T|6x bcF;)cF&_-o?ljj}=+rp@#C4-Yu_(4;E8u#EGd} zzdHO+Dt P$CjLbY^Sj#nH9^!ZV?)B~PHdv|A(Nq1rux#zyIZV>6_f}qi zf5$Pcp|-}&N&odoh5Lz3 #H{N;aV(5SoVQY>+)TEV3 zyhe@lS}Ik`Lnh`%EPk34!mm_*Dacbjz*MZyDmqAg5alQMea@0k&2E&rYxi9|)nV=~ zI=$j;mQzWi_JNv2%;L842gQp7#-$`ORL))eXIX7U3COm<2X*Hw@9%w#Oz$~&l{e`I zf^@f)9`e8qsO_lzF%2>o8QHSf3GZf+tnx}D?DW;noX0eDlAYA3oqtNEt5EkZG>{c% zF=!={DRZKks+zoX2d?@swWX(h)R$v8Q6twqI8EqH>9cw1;D?+@oJP%?&Rk2)A-2fM zH7SW=^b4;vTBr+}D18nZW1^PkeurF=s|I?vKcTUdZS%3z=%ajwh-q5+g4XACi%(YZ z?YCOh-QGN<`8P;Cq=HwbR~n%%f;Tw~{wO^d67Du;-o)Ba-I#c_1Q`I=BW%kPH_|># zJ;!~GYRx)!p)DUa;8YC>8b{VH+kCybLU{kRmMsu8cQ>KIEVT9`!d0?m(Ia0cCxdT| ze5z3X<>xcTY 5cT+`KQ@akA<{iKA!{5!@Xd+}4Ms*dvmeyRkAnF0f%u$r) zE#*b^7}#CT*qG+ig=nwVW|c%MkD0`d%&~;_!m{`lCILC#()qWeWO+}!J?Ey}gDy(n z_K#X#(A4>>h>txk7uA#szMQ=f-5kI_cnC56)bA}v`B{&uo~fx1bgn3Hgz0#pN&V!S z%YVM%l>SR5^{QMgy9mr%mCJM4&K!~0Q#vJYxdcSIwxwIqR{Ob6m$LG5{jPRFU^M~t z$@}POpY7{t(D(>zxe#Ag`E=#UlaT#Wf`G%Za_%ea!3IAm6Ar(z^z `q|^3 zYt&^4q~eum);IJ%7wg~l14bR2)$pq75Wk+6(1<0^pvo1|ddZhtCFqhQV7cTS@qOeL z T(@xklju|l~1sHPR{cZnq0xr5Nxrs0u`up-)-Hn zLKe=xO_oYagRaZ4w<6UARbeg4Y1PXC1nyT2^l*K*jQm`ee3W`%mxp-B*t9#)?(x0K z(a-A<<6n0ISF1@hW8QDUmFkIVz|j-7+gg2S6t+`ZivzH4$DzJ~liX)f2j*Jj(oJVf z$ZX-@^ZEGU-gsWcvSW{??^6selh+=n@~&=Tdhw09mQN5#t??DGRa^*V<)}rk$w1Kk zrEN1J`mWz&%5!KwLRA$Fc)5srNpcOZ-Qs08C^!|>9-eN?{o^<_%A1I>>-jPR&o{jo zRUTuH(xkqkG}Db9x 1*fb;T<(pZ?~UI$0icr4Ca?wc29#KlT~Xb?V z_B*Sr7Ct=UR0>)E(D0#!Dc zJ>*6097o&>&5n&L@26bLZV>P5<3fwg);K>|wyI|?jPMUZwr4y=BZYOme*2$J{ct#s zR4yEJ3Oi40@hBBztGcm(j4$aA{X`g=oH5B9YdpMg^A(I@hrE|o-)C*|1*iwe1 SJ-E5KH`t$1Kg(LVkcwM9Oe5ze)CH*#TloIXrLUy@smt+26j&qL4=7HvWf+ zwi={<*WV49TI^FDkw_a2BO`;53?ns0=I&F+F^$uT=cK8HzF8F3C5y{_W|gSc9NJ64 zYq|6Dq_>cvaKrn)MRHKhkXi?Rm@%d55%4B6R!qimXoLSnV8(;}fOzHQW_i8wRV|P~ zl#g^VsQR1H*vRB((FKVHQPGm9pIvS^D!LwbkPp6}@JC2q+RBD?H=o2uTSsZ8=`T{C zn*hMKQ&|pe4EP=2%)F;Uwpt@P%H!}r(#6#Ho5UgN(hQme+V;O0nPl9UzliQh_X4iG zaeT@`zE(c8(Wgqch;4^+Pi{v=PA9&fdM*^&gP2ZMTR^uDTkUklN9f49PZ{rRFfi_e zf@7{MLsj?u#3vlN>y_;iJ0W+O(#E%WoHdSP-ZpX1?GmC5znfoH$(8-UL8B_Zr7xQ3 z;d67TYa@yIbX6N}A#8m!v{XKp#LV6?)PhZbnX@0fSobE2f;Vtd@1&z|PWE${33sk= zn!O2YmD&jQPtj|?Ev=Hd*tW(blq2safNn+x*x0+J2j!a_qB 7g<%oep02>Q=i~nzpE; zneBFVC{HdP;;^E8T2i<$8v>Y 8!Z9k{VSEgD#) z%*UY|!&QmqcH*^J?{5SMWC~%xB+(&qnB!!#>mhW1p3;X)-MBJrEhsbabGcg}xiSdb z(%d3(bZDV-n7WJDN|rah2fkXYIrH?8L&!zRmp%~SmkW|l%fGR&S?#LdCd7FacIX|V zc$Vg;xj~)_z@xrhYknZy d{I8_=OrCu8G x*$!mn>MPXS=4aMEdMmhxumva=YB<)TB5E!hl?JOh<$%# zDo}VRU@rP~`eJU22hzFQ-mIFk?bf&cV%wzA`mMy=XH5OvwL(yvyC_)Ed4Hjz7}%^9 zvqm%gQ@!o6%gxKA$IHL^Vp!HIPbpamG86Lp{wTwSe`k@SEFaF3dvn5h&MI@Lrrv@X zOCx)zrc(f6j0;%`-AYAm!zb=>H!v!k3<){=DK0C+lauaeylpulAr@jrn!g@tfe{yk zzX+9k)`L {LwrjF@8Rc*xHT@it2?l*DaN%e^XcsEjg zZ?NBS*k-M|StgraYV6JUkGn|cR&Mi*m;7)UMR^jdi2$a@36HQ)VjC-uzRgc=93AxL zeHmcW$+j5aqY7Oz*ZDIDMM9`3dduvj`|=&mj8=XqwTBX<$V{G`k}&?G!{^ozCHS@3 z&OqG&_5RN|A}Ba#&+9?K_=`wmRBp{2B|j-}rZ*80$JpsH1F)Mdi-MQZ3++(Sh=hQp z)7m!GK>FjwRCT( mr~KDWeo&_w|7CEUd9J6S!!k33N9@z14$xh7^E!Qu0%hPHRGar0 zgw)|To}JaW ;>}u1klQ~CuvZEFZaBQ)9mVLyJjGC_w}L#<-gKGzRP0ka#vLcIjb6TLhQZBUz(ob z(f4o8MOHXee%`#qw&-Q%>Alhmc=wpOnTyoC{}OX^_@#!%S{Qtg)j0?9e-002hJFXl zj)eUL#H9n{nRq4mRs2P(YJ^ ssJoFkxn-ol|({aP*947b1HbMrAnL;^!RQ}`PY>}O`p%@k|DwM z++-G)sSCpIWdxdOw34050jur6ueJUSfhg*g-U8~OI80 ZdIZhhLLrP4kM+ o=y(S1P#15$%k+T(ltE}+GWjEhUDsks(`@V3E9eZRR5u+@M zv~40lv5R-qq}6S>^?W@7X!k+-g=Ve8R=?#Wjl@B$5Es(#u{QYOYS6yiES1_}NVKqD zdo3!O^!bV(NhFY|5v)scJwGB=<*ssg$u3jossMYk+@gQBL_F7EHa%5U6N$CV{BZ5z zDCLbafB3$H#0~RO$4a%mfPeawzcm fo! zwA)jI6IF#qjt0QuA1=Flt&6;dh=sm!V(Yfk`u)3klzY?KotTRI)8WQ}#eqPaA&A7o zv*}dg39)+94||co_cb7{p2iNW(bb^F@f)wR>&j~@BI{6uXNkKyT-Ytd>v>PM#(ahs zbi6HpWHHQr5{&oa0b1uz6nJdL3aS0w2HF5;IHZIIpFZD4dGa|pjPs;g2niKDp=F zY8o`H-`gwRy`N?>?YiH-%FN zDkNQa?!azQ4zv%#@au#Hp!sYqUPR1atuN^#fS1_VaD(rg zv6r6mPbyqwm%(LqR5IYvL5MwB>f=zLZOsBTq9Dq02VRf22 zK^5+bbH6~?dA?%*8<#R?!RDHXoLxxCuGvQJg(7^5r`)w4bdQ19VZGjm6K-G>6$V{$ zwlz9=a?>%6MJ0sr;ahf9hP8> dr4yrp|m3uPl zXdNP&Y;}}smvH~m+Mj5S^~|Oi4W`@q70rII{o)cWH2vyeTLq)&2(vk(1(ZIpJ#*^w zVi`cVE0k57emR=OWV%ndf^j&Wm$p3B2m+rU^WV9b?UEb1 ;|%-XCP@wcze*oUk|qT$Y*&E72$I$*fBvlO>mACEd^qt 0~F(RUM5AG1FA?Jr$(*D!q!Tmq3+cC^;QFM!}#);yLua)G7j~tb3_NQta z1Oo3yi^dV6+F=PhS=`rP2g=50N_;~p=hAi$d+hIu)3-ZGj{T{hp5Sxbx{=)Wmy57Z zDm|})9|c#j-)fbL5BRHRTJf6z>uk$zdfo2M)aB306|2j@{8g2YtX~se)8fI8Yx;^0 z^7J*nd0TsA>sIr5_)erXvyFsx!uq=9jrOXycq&=shopI4!@GtXG%)@KC&9q_h^pKL z 6K}fSJcI-;ItHZlxr;2)^{%m$%J`*F3UaVO(tF_@s&(uYb?f%eN6@+8?`@ z_n);~PthJnVF$>2KBaQRtjg~JyjhV^Hvnd{YGW1JO#D5UMEp$#)jCl&%70dcAb$C= zHHTs>KQiQw=*0bzmI^L7icbCO! Hm<(hU}((%^-joA$T!q>_8Qpp|b9QwTH!r(ABJ z4vH&`ow!WU{#)#l>YG=Uc|+|x4LwYKj&}2`__R*h=*Krfkt3Pun3>m}5teERg| OL79EPHwv;ClU5`OEkS*gUP6(}j>KOe}FT;X;-2Bv__hA4UWj5IvV zh?EFh;{P)9VKQ a0al-A)8Rc|znomVyPaMaeo0l0y$p@yn^_KWa(?=_qA2DQ7jxz-!f zMxm0Q7teOhz%JX`yUDo2{vMiHCA%=z8eneY2xHWbA!d?8a&}xBHF-iltmEyy`^T|- zhlZWJ#gbKnIRWf6Bvf)jtdjF!A9ei7dQM>5_ ?h3nv&y_6`GAgG zRJgo)djQ^bqm_=_NlOlhw%JBKROh+BzX~u{xv4&6b%2iOA<_z3uin%W3uKx;--d2s ze1pV>%f=BV&Rae1rG>9p_8<0$QjmtR<;NTCR#xN*a9EdE7v5Z8hfv7cruwI0xFNK9 z_o)iIH}rpx|YX4KxxOb8N^*0j^8!HN$n(TK71^{Zwf8yOS_D>b9juw6F9> zID_o%kXv0cfiEM-=g+kiBBw}toSnu{+H~8uaS{yw6i_k6QMib{n^83PmZvbmBO8iI z?NvLT*zrIXZ;Yb7`|2Hz2Y$}NWE{^9PbhCq0Mz%{yyMur8uKrSazz6u_*VsC`eDvc z1cHW-yk0R^kCC=WI^Hr$B)%HcgXdpZpFqT@4Ww^f%a#D(fos?Ew-cgfg|5BZE@sAl zZV-ALRIOF75*7W5?2rJ= 6bq_!-RVhm-GL~&XHp*d zr&^K>Tdh>MO!~>%LSWa7lh#pWqBE!F%_$V&RbyMt_U)r}RNtN;yzsyQoD`A6yn>8R z1hRX6G;0?reT=c7uWVSr)6p?H J1~8PlCBUa{E&q7xtb4;h*qJzi;X7rp+KAxHordx`Y>NED`+BcUjSAN*xxI?_}L} z`TMC4I{a!&VJS!^weaw`h3`-s5-jtn3pOQe_ua^VzZWLoNn2U+4nHlkIzN|`h><(9 z4`A=0Yy%@koPK&;_cC%Pqy|@duM?7wg1UINxfjp2_0X?J*`Qa4nlp`X^Mu2dws_d+ z%I5eUc?!AKZk>|fu!l_)e$@t}olli|>;^!TGn_lu5NqlzHH1DE8ACHIV!7R{yB|=Y zO60QtrRc`z^Lrz7zv;5*G%XkXyi?!S)Dkl(QDu0;-#f PwPWRH~m3kJzp&Gs4EdiaIE z 45K6J?wLro*x52hHTgg#AY Ou!Q=fF7k<>ePw*6cpjIh_ z7 0%%>(0Sl#pa z)Ebi(?m5Fl^rtpB?9-LSl=Gy#yYA;1$)|6%xC4IEk{*rVHRq;>>i1#eGn$Uy1A*7; z^SUYid+DUl9fn_q5}BjWX3To1iKAZJC$QVPh!yk%K)(+9CX`M3U44S-aq9%M*0NY2 zN#ycNGsSua4SsPm?1$9H7(|c>AYVihn|j8}5uWz2XA8xvPGZ5FRpI_2m$)5KkqS1U z?8DeNKFQMz;w@Yp&K0nNFivpUmzIq2YFr1mtnzGf8gdG*34YzV(jqNY5tqer=U(i~ z3~!*&ZX!LY*VI-u@H4^;7hTyR*!hI#H-Z1zJCDbER9V}mC!{z1KktUZ=jLyD!ki!7 z+F#kOB!leA%<>Ky_h%!vD;Np;i4)VXKF~ctcLN=+;2m~ g!N;h3z;)QSE+&h#cicw^jZ;tVnA2j0`CzwIBm66 zEs%AEMZ^(L;7J*%2*q@eD`sc7XpM5@5^bXE^|OX2^qigT>yH~Gti|iq`qGGCTrk-7 zt|$kKjA8}>78!uapt5JFW>dNB9&{62rk~Vpf7Lw4{-viOMzNc#F{5lJ6^pt*`O>Wf znGgzi@wH`6gcWItM$fjO>)H)upHxj5!D*J@nBimWfaw65ta^#g*Mm(*?Cc4K&@KC| zmgO6!`B#Kujz5B?(9HUo)eFJ96P)rQ`X{qhnc291fgZIzK0#8t)q|`~u*~c#36Ku8 zl9hZ+=Kk7tvwlC!kNH=JgwM7gSgKsHan$GtzS6hLnYOc4<$7{k24WR0ma31p9^eFw z2l>e8l Np7A|H3COO_% z`%Pt4enxwK`a9K)o#sOSK&Akbv@G{L%-Cr2{Wj)e^ z`iDTKFA^d$&%uP9AD#uMT9e*tjp_zhT|Rs1zPg*pxHV|TO2!<-Ld-tU8i{D|4(sEE zPGhs6M^g(ARMofg3p++1V<77y(|mGYrKZ#|6oS${rOJbDjbB%SD!LuOfA~IiskoGU zIQLFdrq!IUM<^y*dnJ0=;b4a)oJD r%q3JY+7S8ok#s(-}Xnc zjV5ln?vEIIgZ`e>0(RqMbQUw{m}pK>cp}wKm?RHR@I_UvOd4DJse-a@RU@$C!-yn~ zC1xaI+vT^t9jC+D7A$g9tY(&2RR&!^khxqb^Gin7s;8dYgv53=GZg4(2P;j^;=I|Y z8G2VGsw{A0T*t5GOBdB~>iEGgWr?H>!J%kp(|$C=c7z!aRdnTjs6AK$nCqc)0^Kd9 zl+45Xj?`Nmbl?&aGS61;%o-(5s3HHvR#EWVR*fkYzLKIYBc0X#)|kIj2&kwceNHd& z%cNoYtuf6iBhW^+7ob&UgxQC~5UpR?nEb59p)#9+nOUY6zh?PdFaLb@#Yx-8^dwa) zelQuVkI#)S )OpXb+e$Tix; (N8+a>=HlW2(Y@!%wWgI|z#R_Ca G+nuS|W4#slA*_M0GHpvIZG+ TLF9swU?kE zBSW72xr|+CH#enBN#X*Hyv`mg)Ogm-)sq=G7 B^qHhyPo z%QXN)9j@djVFLB7{$Y*KE6+1ef28-;LlvG7-%z(W6}OoeBjbKUoao=Jqi#gl7Bblt zP^DemUUT>ynkd_}D+wOf$oQMvM -=5)aJMDE;9_AAzHr#5<;v(hc&>OC{$Z< z@+1e;aVnU1XN{tel2C*8aP52^n@K?JAIfZ1W%rHLkRx`aA_G8u|2KlkUaPL`C=0T6 z^}LN&ic}*mPR~vegby*p3VFk Gl?0R z$?uuFN|e$ici|+g@p|OO`0&SNx{$u0#Y|QZ@EsTXDmULu*TKo0AV!F;_Z*g%K2#z_ zW(wxS6SJr7HGx3TPp;K8%qmLF{og$@^I#~o%>gjTt8vk1@Y=*{#x(4;YSg#r91^%+ zZEWBbc588*c^Dh14oc6acy*5jnjs4nW AdsQH5GRzshi9Gun$TT;_tmzmf+~hmY=Q7**YvRn_uMnFbOqSI7RwWH+HeP=) zQm0&TM+%A0Um^8< >7ik?=l&i?a &p_O_}MD>&t`9pQiHNy+9X}?EVlS$i_Z# z6U1$=vxo0=STodk#iQF?Tr|Jiq3+eXqwprf5!2=6!Vgc7lX7!9W9(_PA4KZ}f@g1o zlIkdYTZPRxWf29_YlKRefXI3Y65WRI2lVP9hXF4oEs-X28f8}WD kCnO`tt*IjO-JvVJ0qx;en|k( z2g%B|iJ@eBz)*LCGu^b@OmyUw+Pm1h0}u|a4CP=8RD-^&OxnzNHIz)C{7G=uMeKK? z?u|OwIwt~FhdJVJ=yRa-ML7_@N&qU5%jhk aS1!zs>0k}rJSbZpZ*a{-jH6EaG#|5Ceb-N4g~STZ-6qq?+` zV@hDs?sf#fnSc2kFQZSc2x O&n_l zC;@ZYLJTZ$bOF-}L*e5lj=SLVkxc6M1YnFdLRQ7q7k8C$i+|ut7-tWqJF0P-{WVYH zpe25tR{YRVK1O@NO;F 0dAh*18YP4W?Im6V4ygu)S9=OyQq>0bD_r$eGa}bh`*LNIy6&u()i1 z!^xbyZl--|MfXBQvjWt|9#V4A?#erp(Z7W}oreog>&o_YQ(1ArLw+wf{u~FpH3^{< zX`l+5R)*SF4KH)wO*=wL`|MPOzZLH&%V0Ad^PJl_#*n`w|00y6zt=g%%MnL3dPuuz z${b6x`m*}7mUN;wRUr7V$1KXZo!!CiHbS^jjzRmB!A)ChW0KLo?fLh4Y6KFw!&3*8 z3tr(pb`Tr239NYWIunV8u{s1xTtw&=xcAxmY%6Q7V{_qd=Lqy!>-Iso11mnZ9s(CU z1!xu9GJftjzY80WmlvqxonE0Vw8R&dN$E3OfPVSK?b;Qm)jb>t6nNbg=Fnpxhpx$$ zlCPND$HRE#&A*=2@2Mpv(N!BA9AODK$~+A`%1||XpnB`0Q~s6GhwR=2fSVk$d%E*u zAfbaRdEKVlm7zZk(JZBA8JrT>@Xctlsu7BF&*G@W<6nvhJ&@`0 MwT= h+i``6OC8OC%pJ*EbUfOUPuOzq8^nB2N~|1v)0NON_B9vG0Iy<$fzeQV#XAm^e|2 zN|<$!t+WN&9ldj7)qU QK(v4nZO3{jz+33Ala dflO#d*N zt-+0@Fb{1D@1@rQP;}j|GQ4gwtgbQ`$w(jXD kPj!wx49s(0gp5&Jvit+tu28u;wTv3ZX{k#*yN<6+1hAuQe(3FXxzY-^ag zP=^&oG1*k+F;7U`0`3@oT_=R{)Y}M*=TA3>6XJ)daiRcg)JADwywfIpQjC%xmZ}h= z)?c&RGfx0L^ggiB`TWNNr)5EXuk#k)^U$RC)unG^+hp~jz|c2^ymK``>+~x4(9$dW zDNE=1?9dm=0vT_uB6B0I+{b7iT}#U)>i3O|uhngwFx7_Jmm@D7>|hS{f09+Nw(ai+ zrOuUB@Gvq*%X^bjW}=fOR%p4scd=L{`_0a_=*Xa(-?xqk<$QRrVk4KfE_d{nH^!?x zp$9EqF8x083m+Jj9I-c4kgftNROt?^XU}YqHxj_i@>vLlTo 4br05tDV%ZiY7~j{P##U5g7K68B5BTR zvd6c%--7;*Gk&;6op}vl!#Tg5-mPcDBWF)on{TV?AP%b`LPAXLK?0s1O?8dP9&fG@ zC+H*%k=mGyzh8;NGqYY=u7t7GV>aRRMVH*JGzm`RId9JHv`CLTaI5;JaA50B&k6si zRI8#$?pJ-Rp(w;Er0^U{k={TIp=NLm-c?b)UHV){4v_i%PkK_){On hdd zH4=4Zi9zO)B|W@?(d)9=z(e^Bd|+@lS6!)&B(uZdnWWVg)s+k?P6Zj@r<9BWMtDxj zSYK-8LIb<#iMLU#+~uj9I!Pz~prDsgia1)uO=zE(B-GRR)mL=(gNZ S8-pM8C;Fj4 -2Xr;-%-@h+)^_+|^pYp|<|q#49t{ z6-~64(4mLST1!6Mw~}}>*)SJZsLFzR_1x)hFUY->31vssh;95n)dJ>-T%BU!4saGu zCEecM2e+fz#NkL#kLnuouP9v?j%4cOkiP(}MoUk{WhXq`0(dwQ+Fs){YjKxRVd!~P z1dXO<-_=is!LDd?#rk^5p+cfjY1qh+W~2!_tCR@Fawl<`$3N5W-3Atx74@Mhe*M4) z!0V+@5G>5bCEwG%=w7P%mYSH-^I7)k^T%H`RM^|0;%gjV&;GC|&j53Aw~b|}R~aux z+H4f&_e@Z66q)Plb`+R=$)%tbKO^}85j1AY`|<&s8Zv<4gX|VcUNakC=%E+oBa=k+ z$BS&8?TNp+3L7+?(ez9~8L-B7>;IkGwCP+;j%TGJM xSE4EITVU(qhlO55dttsn}l7C zWfM{}Uw(lD3`)PqH5MXWCaB)C!CeQ|nvYf6_Yq4kL7A9+*#K>;47%U^cSf4Y^>j86 zft2x?mjFgn0BPZTT&%cvN`^VaSa=;$YkuclV`)g3^NxD#tG>)frZw4tv~g#Xx$?={ z0VZWqaLsx;P8ije(1+7YZKG9u%^E>rlG|n-hG2w2V=Da+Cf5usQfw4m--_rz7%aY& zrkYNz)T}qA{Mn!)jBf 7NfC@lU;yvmQptfIGo0_Z|+=sc`2XkO7we?Sw1?JqS*4e6uKwW+ibmN zKN|+%i-kB~wgKxZjKMR}QI=)EQq2qOW~j>)3;7~h+EaG+gWj`4BqIrz(*}5ZX?m(d zngHu#S~H8%lCOe4a{c^E8Dj4%n{8gZ=MKiO*URJwBY`ZjEbniQ3Tu}s90GyUX=|+o z3y9=pr@54}g5^++`A%h27dF#lHH~2?4vZHk63BW88|0|Yu$lhAeHH4V+WMQ5vX7QIKws7EnMMX&6eRQ-)6I29Xp59FT^gYe+%5bLbvG8itVm-JWyy zbM_Pa+57+I|LS}`yts#1Gi$ADUF+)WI}YcRM*GAXJg?h{84J1+`Kdl)qo^1uu7|qY z;*Jd4b?dH02%PG)I4FKu^qUZ3bR%H&jYDESQsu?X48=52B*u3?^_i|{r(*gqv#|>E zjbbrF-neW_G@jUaaXZ8A(5p(HGx`KZDQjN=?{M# uE+kMZqso1!O#r%KX%9m@vi@L6>hlC2pDH#Tr^O0)=*sH^cUQQ-&&{ zrtHUABa=Wr{6(%dpTyj8Fzeb)?N$2=!(x+Fj)}5&r9~FQoLlt7`{Y%h>7ABEvBxV* z^v-!kr7j5 U)3jdHQ?MG= 0P;QmNlP5aZco&sc~jP$mW0sM)3v5)?&0 z#P`_qrQw|i-RtZrpp+tPxi{o}1%{o@AeTjE!2RrA#T@FJ5yO@Iy?gOpgGLVb$ADHT zr?E>Cvliu6{m948^yqMfWTsD94eCd|>JmwD9W3iFD%zhoYpQXk=5ZKWq#nUPXC3Wz zcOMq1aeb5}6CL5$Ub}vP3|OB|{d!*Wro1rctS6q#!|$=mBuFXHkDaK`Bv(-jSpsru z5uFziXC#J=!dO45#%r-05ndb }!2|o5>aOORt6r>5An#Rs#`G!j>Ia z%Zf>$4P+P%i|=>Ij{_!E88EoXO-0+^61-tUG)4m3+qv%~!!%%uym9`-Wg$I$a|;^( z9`;I-DxD;ZO%VUIu{A5vMLPRiAgZAvLD^Cwn)E{1q2~=mf|C#BBbc&M2WW?>Kb#9m zRbW f0x%1AIdeNouaqp7;47va$ zQ^0 PEvM*GoAAeGbB?ZF?AGZ}0T+f6r5 z2ez6t2H*C37?2}Mjnc0s)MA`s&*TSmYi8Cc1E0Ma*caXrzr^LXSYJsLj=PGVCqBN; zj8vKMs(Lpp;AZAA+F6!CT3c9a5j&ZPn8_oM;j?3_?+VrD_##yIAsIb>{Xoi}zGd1S zf%@PZ39;Sc^QfQp2~09Ulh~Z3ipLDZ^gBKS3W>!YkPC?ud&`kl*a=%jeNrpRG6yGY zAMu>R?Jnf$gO7v@QU{byxEswszvGR*MxzP(6hzNCfrab#b9J+TMW!Fu-F9X>+@iRC zqm5@~2J*__$8{?!qyc4{;DP N5+11tWA`ei^=(Gjpvz6Lp~Rk( HU2>$)y8W?p*cCo_EKn-V7Z@?a8a ziW|G(8EZl-wJPZjZl`!7f@7$6&^TA+lLtx`*1Ebt&rqvOD*pH*GKuPNP(=93LS{eO z@+_Om{6U82<|5$HQ+%^o^c7vy-gcN_WAHt%=E!)r<<>Y)x?fMPnlTBfr=!&_c(E@? zb&io-Z0K--<3rX1c&m@7y^43The;qz`yyW1e8p=`nEc+ZY81jOIs_4qon6KoLH9g8 z-?_Dn9=u=tPC#?uh-q+;>Y*)GU>rI5q+5D1A@cdRSrWi=75nZx;cY}~wNR>qV`zM! z<$ulBl;SNcP_wEkUUY6Agb6sGK^AdAl=_TG4p(;rKWhjOuk%VI$NgY!l+c@BH3fpF z55N_kijbzizw+ImQ0azS`>dO=L=Jkx%rRL$eiN}ySMFy(U5?{hBc}3n&>mopSMij1 z))wX>TOlSpTR&3zvxV Vsuq)mNfO^<#V6z<1Zq-9 zmtbb!DFKU#rUwdvM8_2Vjb__?r3Mj1;yg3uy4=dVPJ1gX%8w*Kqq!v)wh{y-pXM^< zfhNBc$eq%UY05`IELXVhi^&MHj@+dW-f=
X0e-!L7sPn zsPn>oqE5~RFkn0!2n3>I@7$F3_Ko6xgPXfPqs3rFzl(BuJ^q41V8IKCvTYVY{eg@_ z>FB0R_=D_yYiNn%GKCtA7TqGf$IVBV*DTez_(fASsxNDG*hPLeyX$_eC#|9 d|}NZOtgZa;ZAX4v&$C$*OK(vz_sF+XQj9`$J@7{Av>+x^|5>>%qSX_*8p z4Kg4&4k;!TdX!$8wl1-qUB74SaHT7sdyb)PZm|{0ig*6*C3mgjW35Aw#Xn`V56kX? zu?)UE|6MidUBYK`sFagWO{C7QMO_ntO0C9rAJ2?fRGyb~<#KoZ&lWS@Dr;F_J;oPb z$2eGcwhlWEGa?9ve&BdtDJ&Ii@KuWBJNXnmK4QD&Mdgk!P_qPt&hkJD(`j?JpqCI@ zjfYqk!B`JJq7Ti}P4Feq+75079DT<(8zUL&4Yvw&OqmLeHqg*9q~5E4o+naZ-<^5( zLG<0SXF)MNz6nH%tOlrY)1(`3{h)2ty=avcIZb}_&O9=Gv>~v|qCLi&=U_F0@H)Ms zO1>g+pJFCgyudD^^Mz5$$Iu~tPJaHzC;ciP02lFu?7Mi``)je<{GKMn?%}Oy!4Ho5 ztjLWYXXT#gH+ht~L;t`6$TAFoxOxZ92YBn1)oLtV$_9%|L~dmC#Bh@UHZP_@9TX^| z1Jt8{>Lv;rtn&8NR?AH`_DeoQY3D5O=i2sig?Y08CQ-#E|I1b+* zZ#TbODloOXnL+iqx^!CEm qnvMo!#37&(v2beD5Vd@H# zbE4{gmc`{`%&lNa_viUtK3mHO%E>|NYH1G31~Gy$iu(Ew*{n2uc+CV4jZ{maZ`4xg z`j_7m2`X&s8MdiZrAfgJsk<_|7Da9o_yUr4Ror)*KBt!AWv}v+XwpBv&`YR4~A^UU&r3b})>8_wG!PWk-zi!)Nh z6`Ej6_2zanM^ZdiRwfKp2yKTNnZ$TBk17J;0e%Sx_N9tFO#Q<3u(7JF*;zW^a8Iwo zaJ~wEJx*`ydR(it-s6PNf78h9s?#X0sYO*g-wfU8bNk6wo9EN`ki^#sJ<7~QS@||V zNFCc?$=Re~*Qo~UGgp|w(gk45Rk+&tg8h~k+NYkCA41>jz}$TTM33xrg}#_Fjko%7 zwV7?_T?t1Z{17fhTEpJa zabx>GX+L?@28jIJxZkHKrCm%7Q$ynp6G{n-eG6o%9%@TARnv(3Nds9ykb%hQdSdeJ zQ+92ad6q e-0eHq;6B14!0UVd|yzc6)EoS}eOB^6ySuu>3 zguy?hDUH|phM;h1pxP(4GYrS{9=`>Mb%2`88iOdp5mM&9|7nL@y}XK-6oL(pPyCgW zc uVmA?RDK-?eDD*VS3+P;o>Gf z{WWV;I2|M_S53i+UC{ebkLTKx4}vXA3@5N9t6pTBcMdRWmq*tft+`@kn FmipjMgCa71qBP|phNQ*Wy=_Ir<#tq7 zNacLS$bRwKb`7Ki%XOn>nqD(8{G_V=#=tm>#=4y`fxsQ38DffkUk2lLQ_l}KFq}K! zAo_Z-B!MF;Tc5O#dV4SKu2>UDOXWsK%e;hzT<@_Z^_vwN?Z*_ctrF%WiD$R$6jS>l z{a(96uX$ve(m{P&<;1(*r*kKKUzrA@%PE{hQXY|yNfhN@ ?Z Lh}01H?|$b#Z&hgvLRP%8BfulO-SCG?s)6#e9w|4)Qb;oApX3RzC2y zgVO3T>o- 3Qjg+WW4vc_fPYEs7d$I`}WiJ7foE z>0a^;5gmQRJ#R>Gz$>`&!0&k ~bFiC0}QTPlOz~kRJY7pN0Eer8i->JJBc(>{D+BC{B^bZ+_L>+fInS z>V}}kat5Yk6ieS28_`ox9ORi zH$n5;)&Ml@g;jWOg&X$0NtGO|oZo)0F_ZK&d9*$;q)p zis1PVCgc7!*$bmz6;+N_uVJGY2=u#8&02syHCC5nd%12uIp_{dQ+Q&Q0YiP$qEeeI zq4tDQaHo5tHf{C~*WfQ+E`%875&k4Pe1YA9qgHnBej~aMo+!Z(&oECkT7q6+ohn zUu`~dCV#$y{ H^tg)b+BN2&GXh(hif#_x`y*D72 z-TK`(k)&*N9!G0&^URlPOW&krI}n%A5-oPJFHNU0ehJBE{is-*4tyE2mv|2oq(d7w zTn!Dnz(-%8`|#c0hk$=uBE}O(MQnp$q9cXrm6!c*dkU3Aq)NrgVlWR!!-KPvupfTF zCZ)Tt$)>3xPs0BA{AfM#Yl+CoBJ`m|kF#fy@MwiO+N%Y(=NZ=~R}Xf3BA!Ib`(K^E zo!xso4ZUsC&3WC?|9G*+z{~71gIJxd+%bT!7$h_4u*lvgXp8N(saARISWC9ifT$8! zzX%=DVO%9E3GZ$@eQ61~CtLt?(XM#VCv|4_cHZIY+^$b #R`Q$lQ-A)-_~Wn%%R6 zz%+tXkqhlCzcr)c_W4vG;qkQ5Gsyet7FMB-tD2-&Y>&{e*WyoRW11Id-UJi&FQP!m zP!BQTsU#@G&?cK~%5`GwN!yPkR85ODGMc^eBtE3e4;jokvF%Xce|^5sW2_)W;P%4N zrgtu50}i;E<}dEHi0_Y7g*X=*b?kVa)G`Z~H^EDFpi@bL4m3aJz7#s=YwL17*r05D zv;Jdk3_qUCtmkWz{@JqL-UJNFSERO3bz89CNevyl$gmi$r$%!q4E&+Jx_+7T?fVY{ z3pcJybLbS^qKckn(vX$DQK|;34P%?pl}FZ&{KAHpcU3e2cQs~bJ9`7;#7Ic7wL{f* z`K3bsGw@hJ$-tITWm4Z1TG1}YqQfj%vxv*8Jwn9%#`;s;Tf5xG_N&g!#Tv)mXAftQ zKX|{$j-`$kYN0Wd!8Yddh|c1ZANMR)YLo9{xh;-V;lvVv&_$*2o-KIiM3N*?Dcf76 z3co5{t+i5!i7?M@a$!l}8)6KTv-EPkAvIgC t#_Dfa(oCY_cA6 z{Z#8|I+aRU=zfo$Lcnc8X1c;A*4bKhptw}S-=g`3 G8Yoqa4PTZN`Y+H323g^4`%u_3dH(ryYa+;2`e`WeT=lHMdV4$(N1vsiu zh)*t- z%=-unCZ!_eUx=bT!=VxpoE43Elpz|DE*4wFysKFpiXO&li5|`u3Ga0ni{1Gu)M=F_ zocPtM+Nrazzo`nS_~Sv&rfA}{pTzai<*~0KK0sN YR-qv5kz!C z1!eOgEMC+By)pfIqu8wCnN?9pp91NQbXc`@fy0+r)!3L3q;?0__Za9S+4y|pj>IUS zNWDp`^o`YO4#qM!L&%5pTcPX{A?NYAM4s2TX31tRy;iOCfkv^Q$V>C9fYyQtD~#_j z68Rrro%xAg V|62N8QfT zpsRDVH)>izD2i??inc)8n+$HUhz;W6xe}UYv2=IG%*E#*bROH&Ufc>-g{u|?Vi<}} zJ>dQQ`Hz4JuraaNNFhbu;g4k2Bz;p|Qj4s|2E~3fQhXRH42wYbdVSi5M-s|niI;mM zyquV2h}LaqD*foqC^?adJ1 z4APSJX8ewTkA+vzy-! zn1v;AsKnKgl 0FQT*b;xTa!+ zWen9CRIg~3OLt$7Evs$WI9k9=Ym-Io>xV;146`U0w^skxf*8)VJNiXJldgLwZB%cr zG&vIU>)&E^2UAmJ)%A}I>k^Tc@Mq=rkzalgM57T8-(*Oy>iCI2Ft&Dz#^|a*#`?BX zs^Ta!hn|ey`K2{yC+VR7oMhU`f|QZ8?MbChr>;ye)G^C+J|bsrMLAj#{9*RT_Te`O zqp3+y;IHc@a0X*I$ A zi99iX-=;_qjx#R&PP@)YpFZ)nSm?W;a($2( s$2=&5_It%m RlzYzC&`uRh=X sc7CNQsB6LFR2o! z+PREE{1B_5bFD5$g!k3afRb%6;`QbI$gsBdFF2|UZi5XOf;p1OlmAlrt ?R4q1S@vKLc&Kl zI|miW0`NSXxDGMtyQ_cnP1;PAJh`jZJ)b;c=Kg$%-76(?ICyuX+JR2fIm^8POId5P z?eRXv*6u`|wJtGcMX-%vpBoX`eMa(S5+}97x(Y3dHvzN!iUEG2)-`;SSL(alTCQ%N zUX@Fjv&AzPIJ7jZ1R5EnSl&3zHGT;{kOMgQZ8Y|;GU3vDSxaNeP;QK(rkQcx)#2b! z%#L>~pc`I`V2#(@2U|1)bMyXY+TXu@=f0qNlTB68+ZSH}>g1FslA3RB{n-`t1Se}B zDWQykAqv?ICU>)MxVRhlUEr--uQ&zNC%W lc(`9 zYz(O2T%6@g; M#A$2%-Wj=KYhNO64Glw#? zO7%Im$Z( z99n;EK2xa3^o};^>~Io%eEY5Jmj2H2i{SPFO1nNRApUKEMx+?B;3&_!>JfZl;yz7F zgEHw0cLZY$!L^&5j<%^z4-CuibBZ1d_CD8u?fhhvA#V4L5((3ovd3+k&fGdj$A*)o zwE7)xgg2w$2cbm?Dh{^|*Vfg}o%#xU#<1zuPVB^5e6Ei_EMKFIoFuaxqi&i>!*mv5 zrr~4&1ugzW&XU>~DdP|c6BtsaJQUA2q)x#&LuT=)rsL1uBXd3wy$UJV#EY95`k=D4 zQ*+V}jnzmnY&J=I`(iU P?(E5dvlJI7XH9MuqSs-u?c8Z++9U%+>sSP zLZT)+JT yt02x~085f-95YPf7;w|KGHD`#ZED63uP#aeV>*>Gys*wO?Uy5Hya zb-i2I5Nz`8fG7N9uiaCcglTAh_)cV}7ll+xX2+#SgfL}cnFc?9Kj)DqJu#%(sUuPz znVQi>HpAzZ$h00x (Kie)f`_u!6X@Dg*d;C#t9 OCQViQTFa8V1mLNWa?)RsODMk`^^DK6ltji3|l`-Z6MrO-*$GpDk?jSLBj zG>6vS?|l?Cx06VPMn6C4fQ=^|c8y`;lGv<-ub)RBy_9%=(A}$1CArhTb|&-Ox6z|$ zo&?5qeiQ5o`gVEm(d-ZQ?G%u4F3 qw|A%azxxU^T9q7k2-!IIp@D8AmRRx^<-j&B0P= zQV<`Jcm9XQR*?8YCL8-F 02#?ij{|DU! z_ief}#}vCcYdIYWF6_$}IQ7TJ?ySN~n+PRJ6P+AHvgToO88Sma%y zu~=qY9q92Ew>A11%<;Vm)1-Bwy@qzhENJZF-N7uWgHebfA!CEcc&W8i!3s?V>}`n$ zX>bx4-oOvFhZ4;$UB4Bxnd8xMZ70*?{Nx@Cb>A1l$U5~6!3Yh17j!P7Z}j5%rg{qN zU~u0pu|4SfY%*k(dI*dWkxx6sJv%w1k#F#sWjs|z=Da&>I5Do-75D}}s&?-FLh;^i zhf-GOWjB{7mP%EK}NrqJuZVO~q{tL3~rXTj!pj6DRZY`4;n(1E~3vH3o*3Y~%47 zC=*?9$f1a`SS7(Exq#JS>U(YXXL0BY0TaDRT!;rVWD9x+A%&XFU&lv8qq#sx+kv#; zVSfzzuw+7+!PA?Y_h;@68r;}s1`mp1lln&6q0vTlEC-`8(*#EkUTp@v2l3{Mi6~}p zh>{g1aM)Y7d2Hn8hbHqb^ MJ6)x0s___7_$_Y~~AH= n=_?u$Xlt>+$% zm@%JeENP`MUm|y&Q>nnWYos#7n3@kyg ^X%gug0hePh5iE3PLBFTEA-SQ~p6 z49y%jwM}i_wJ&MH5*C|axVMhQqf-hwPvh90L5 CkXh$A4K u>R-ph9H)UE4rKYO@Wv5URtT~YtzSU@lU4T`&rDAqlEWGM4&AvSQa#{F`%X5d-| z+BjJN=}*chqrG{_GmDo{KE&5@#G%jnsEgIP!)u6MkPelAT*^dBG}xqKF(S)v$dIE? zV=1@;cy|`CW^c7KhcGa$2d;N9a3?JLf-VQcfWG9FT^sF9EO^rJ?rH7Ft{ SOqv&BNyB@fT5zz5N^#^Fu1UxW#o;@9VQ}6YSM%YP1Gr+(HTng&%s<=gNM- zI9+52Vh_qAnh`c%=)JS<_$XF?D8F8LO0wy`yY({S&J~p5#Un#5$rP!npx`2<<~rev zD}akNmPEVfPhuBX-D?C&bw`3ogWZI;V%} UMwQoVh zjBy5FF=oS`R#`n`E)kwfbh4PXy`wIY3zp$5D^orx3HW4Am|djGPmM)IxXSOxa=5z+ zft8-F*c`}N7HyOo6=={chO>t~6iI1y!HoyuPV|*jDMOj8?RgY8zm1_d*tgSNnXL4Y zz~;)FpQ=M%sQqwz17@S#rwMyH$;B yVN!}T93wq` zVWkjK @}CSmt>1X*2jnE*>R_h!UY&ro7a#%%HUm3^jY%WIxAg zpOLN8^^0Z4cP(l-uTH1!FdF#fUq-8Kl_Wio$!AKCuCqHl&9O7xbYn OS40$mF2$Si4-q@BuR9@2alTtEm3(r?g<@66ZTp8^ z6uj!%&eiC1>opGkxLi`)q8fY^Yr*$mr!YUmOo`u!1ZQX?C+6o4(-B>el_0JxMmz6( z$WLCX<9jr^rw++27W8YjzCY%Mku AeFjFIw^RyCoTCCv01Sk!!;fGYEc!8^zs(-ZSFT{V$K>4R`pE5StxQ4kT`1( z`mzPH){&+n9l$csw#0i9gb`_e0F~-)mebO1r=l+=z<~r3+&tB4f2(Mp3 h?xxbDDvO~R}3(j)e0?1u#?-+UHr;=?zMnOi_gJnQG#R$q{$^p!yG{#0eyM% zE3qcVBJDg}qOBSUEt!0gUraPbqhgfRm}zp=8i=~`I795#dN9jzJ%U!N@r+epy@{1m z=>%E@rzssK*%w3S#^S`DB5 9Fm3{xk- zN-U?*cs8)_aU8L5Ux_5pN`vR(p?znTVe$Y(R}rkF-tp?n!?DMo@U3YT&S=?`6n^ zd1IIln1&wZ(uRXK=zYK$l@yPZ#&ijh$-zP!H9`S=!#QU 1umX|Hi6SU zWuqiwy~kyHhGTB=RVsW#Uoxu;H6@kDL^aAF4CbkqVulb_hO>u358W*VM#%0HY)#-M z-gg)|yZuB#fVc7hHn$y9%^3@Xh5^Cxa-f(H#zj++6}vMPwoxmXI*hn6l=cp)<#7)V z>F(eUs?_n5onN) |d9wDqe32H`s|uX3%`T)jhe>j;aMzbSG9B?=lXQl(XCNW7GzR^9R*SW5`N0 zYz3J!CeX~BNZ_KoOT}*mIO>;`j#m!g@`m<&kLO2a*pb69NOrc$s(&S)@=AQoBj7w7 zOE!RihA^@&AbeFv48~;==6C55Fcy*@n2aXns23|)>(GvAV6XG-Fp`gNJ<80|zLksq zGLv^E SaodSX!e0y`Ea8_c`J0sc^@ik<@VDvN;ofJ1oSVJ0 zj*aCM&0NctQkmY++!IQHYH{wJ%tP;3SBDuf{0Teoo_$+2dI( ((b^S)` sKxy65lj+(r5C zhv%GkCAG6heTbUUv$qcihGiOTtKaco4)jkW5+ZN;*R X `y2r_qx6x3q^oz+mojgucOzlo0XR*t} zG&mNFG0-;V?CyS}^33e%0)!W?N2TnHZZHqmVL4r>fa9IyI)(&Zz9#Y IrQWMx`ENa{Mw z{~-I=r> aa7e9MG?YQVyzunl&* z`DAgIiXZ8007lvYzbuKIhOIJe_zvtrFf-?H^6EfvuJ&XD_|;uS-nFHx8TUZwaZ~y| zw*3~Ii7DzTi)RRpw47>bmNTAiWB~N3 tfjrvczqCc=d+Sst0!ycha41t@plcrmt0cRP!Z&v(Lp7MDO z)0#B9Nx)f6NlGg#Sn7$M3-ZS-;Xwtlm}flmxz;O5KB?Hr+c44%t?VCuCt|+vX`RCy zT7;wjv(|j7V8s5)g0^Xn=cS0x5iO>0sp`{e<(K|b)^XR1-hDQmqE>a`NR07@! zau2^JKo6xlb&9Kzy2(Yg`BWQ7(8{Bf`gjNn1|s{^vVYMsuPbpcqw)OyiD!?e-7tT~ z8EAb?#67laQi4rRLfLMeqHjY+oXUdl(!TxJb
}`k;ruhNeH1v}!J$p&* zth5GFaUD+FwyhW(dC~0EMd&P6jHzYFwdzl=3yyyKv?<%yMU5tArt?xv*xO?Fboa +UfzL7jD+YiVAbC4&K4n&OH!C;q3s=PExF4~p$(`S!=EwS1NuJbw-c zjluP^&y{sb463WZUM{3j2gZ$(+OF{?guFraGwB14-JgnB{Y1-Mk+% upJE!4Y7O&`ZZFKBiiM^-`)+Xdcm-okyS~$j zeFCCTd8yqNNXAe9Qa^62@9obrze_P+&*{ec!|58-U|GStZ&?ToVXN7xUz`!A1@*p+ zeAimw-D)IM@$qIa?k-5AKE$&c1g~!fr^lWNQ!x*3o(Ss-B!RiTXAddOE)C&ErDjAz z$@A9=i%z`fv;&PIpJeSs*^#F^iyG+bjK^}jL5##o7?blEm*3f~m!_L+f3EIvS&Q!1 z@0qBE?(??f!FFu&Ksp;#z55h0`|thml2aDPcrta?X?FO(BHV|5>B1~L7^4%ENn|r( zA5ext$E(+S)CzzE*gF~WR#cB`w1v%L8f }`8LmQU;sX!iE5sAyPWDO zn0@nEZa8tPVpq_h6e-KpZbU~vx1WkNd?g`2^XuG~oqU=8N%Q?W{19x#A76?ZSRS>P z&d>CVO%#cFMB;8el%ZJJMoL`IgTATRlbewjyQD5xI%Tu53ofXu9rP(cGm7N5o+xpB zV971AuPgCJRsp)Mz*VOeXYBOc56@)^I*d1w(6h=*tHWYFQ*%R!-*_`KU}RA%n4z8+ z&k{MuM we7(9lBF`X0T)eX2i}i+O&rv^` ziyCjgj(vV^&Z9l;XzfC&8T!E^__3YpM;Ainw{qPNEkqt0Ce@Zu`<9kuZ8uL}<(c1n zJQft}9d)|LzOme8VI&SO^J?JVaJp2QEa}4&+Vk(tb-j(85pKW7xYN@p^~oq-TVX ^|4s?fsyR@deC@2gY9_(ydBm{oYUCg_tYPU=J93e>Az