From 4c0683c4849ecfd41f0eeb823f8c36eace9ecbef Mon Sep 17 00:00:00 2001 From: Erik Kalkoken Date: Sun, 28 Nov 2021 14:48:49 +0000 Subject: [PATCH] Add blacklist for groups and ignore blacklisted roles in Discord service --- allianceauth/groupmanagement/admin.py | 59 +++++++++- .../migrations/0018_reservedgroupname.py | 24 ++++ allianceauth/groupmanagement/models.py | 46 +++++--- allianceauth/groupmanagement/signals.py | 22 ++++ .../groupmanagement/tests/test_admin.py | 108 +++++++++++++++++- .../groupmanagement/tests/test_models.py | 21 +++- .../groupmanagement/tests/test_signals.py | 21 ++++ .../groupmanagement/tests/test_views.py | 3 - .../modules/discord/discord_client/helpers.py | 41 ++++--- .../discord/discord_client/tests/__init__.py | 4 +- .../discord_client/tests/test_helpers.py | 20 +++- .../services/modules/discord/models.py | 37 +++--- .../modules/discord/tests/__init__.py | 1 + .../modules/discord/tests/test_models.py | 26 +++-- allianceauth_model.png | Bin 280473 -> 310621 bytes docs/features/core/groupmanagement.md | 73 ------------ docs/features/core/groups.md | 92 ++++++++++++++- docs/features/core/index.md | 1 - docs/features/services/discord.md | 24 +++- 19 files changed, 483 insertions(+), 140 deletions(-) create mode 100644 allianceauth/groupmanagement/migrations/0018_reservedgroupname.py delete mode 100644 docs/features/core/groupmanagement.md diff --git a/allianceauth/groupmanagement/admin.py b/allianceauth/groupmanagement/admin.py index 44750aab..0f38bfe7 100644 --- a/allianceauth/groupmanagement/admin.py +++ b/allianceauth/groupmanagement/admin.py @@ -1,14 +1,18 @@ +from django import forms from django.apps import apps from django.contrib.auth.models import Permission from django.contrib import admin from django.contrib.auth.models import Group as BaseGroup, User +from django.core.exceptions import ValidationError from django.db.models import Count from django.db.models.functions import Lower from django.db.models.signals import pre_save, post_save, pre_delete, \ post_delete, m2m_changed from django.dispatch import receiver +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ -from .models import AuthGroup +from .models import AuthGroup, ReservedGroupName from .models import GroupRequest if 'eve_autogroups' in apps.app_configs: @@ -70,8 +74,7 @@ if _has_auto_groups: managedalliancegroup__isnull=True, managedcorpgroup__isnull=True ) - else: - return queryset + return queryset class HasLeaderFilter(admin.SimpleListFilter): @@ -90,11 +93,22 @@ class HasLeaderFilter(admin.SimpleListFilter): return queryset.filter(authgroup__group_leaders__isnull=False) elif value == 'no': return queryset.filter(authgroup__group_leaders__isnull=True) - else: - return queryset + return queryset + + +class GroupAdminForm(forms.ModelForm): + def clean_name(self): + my_name = self.cleaned_data['name'] + if ReservedGroupName.objects.filter(name__iexact=my_name).exists(): + raise ValidationError( + _("This name has been reserved and can not be used for groups."), + code='reserved_name' + ) + return my_name class GroupAdmin(admin.ModelAdmin): + form = GroupAdminForm list_select_related = ('authgroup',) ordering = ('name',) list_display = ( @@ -209,6 +223,41 @@ class GroupRequestAdmin(admin.ModelAdmin): _leave_request.boolean = True +class ReservedGroupNameAdminForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['created_by'].initial = self.current_user.username + self.fields['created_at'].initial = _("(auto)") + + created_by = forms.CharField(disabled=True) + created_at = forms.CharField(disabled=True) + + def clean_name(self): + my_name = self.cleaned_data['name'].lower() + if Group.objects.filter(name__iexact=my_name).exists(): + raise ValidationError( + _("There already exists a group with that name."), code='already_exists' + ) + return my_name + + def clean_created_at(self): + return now() + + +@admin.register(ReservedGroupName) +class ReservedGroupNameAdmin(admin.ModelAdmin): + form = ReservedGroupNameAdminForm + list_display = ("name", "created_by", "created_at") + + def get_form(self, request, *args, **kwargs): + form = super().get_form(request, *args, **kwargs) + form.current_user = request.user + return form + + def has_change_permission(self, *args, **kwargs) -> bool: + return False + + @receiver(pre_save, sender=Group) def redirect_pre_save(sender, signal=None, *args, **kwargs): pre_save.send(BaseGroup, *args, **kwargs) diff --git a/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py b/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py new file mode 100644 index 00000000..0349b9e2 --- /dev/null +++ b/allianceauth/groupmanagement/migrations/0018_reservedgroupname.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.9 on 2021-11-25 18:38 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('groupmanagement', '0017_improve_groups_documentation'), + ] + + operations = [ + migrations.CreateModel( + name='ReservedGroupName', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Name that can not be used for groups.', max_length=150, unique=True, verbose_name='name')), + ('reason', models.TextField(help_text='Reason why this name is reserved.', verbose_name='reason')), + ('created_by', models.CharField(help_text='Name of the user who created this entry.', max_length=255, verbose_name='created by')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this entry was created', verbose_name='created at')), + ], + ), + ] diff --git a/allianceauth/groupmanagement/models.py b/allianceauth/groupmanagement/models.py index c023c643..9befa8d0 100644 --- a/allianceauth/groupmanagement/models.py +++ b/allianceauth/groupmanagement/models.py @@ -4,8 +4,7 @@ from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from allianceauth.authentication.models import State @@ -181,18 +180,35 @@ class AuthGroup(models.Model): ) -@receiver(post_save, sender=Group) -def create_auth_group(sender, instance, created, **kwargs): - """ - Creates the AuthGroup model when a group is created - """ - if created: - AuthGroup.objects.create(group=instance) +class ReservedGroupName(models.Model): + """Name that can not be used for groups. + This enables AA to ignore groups on other services (e.g. Discord) with that name. + """ + name = models.CharField( + _('name'), + max_length=150, + unique=True, + help_text=_("Name that can not be used for groups.") + ) + reason = models.TextField( + _('reason'), help_text=_("Reason why this name is reserved.") + ) + created_by = models.CharField( + _('created by'), + max_length=255, + help_text="Name of the user who created this entry." + ) + created_at = models.DateTimeField( + _('created at'), default=now, help_text=_("Date when this entry was created") + ) -@receiver(post_save, sender=Group) -def save_auth_group(sender, instance, **kwargs): - """ - Ensures AuthGroup model is saved automatically - """ - instance.authgroup.save() + def __str__(self) -> str: + return self.name + + def save(self, *args, **kwargs) -> None: + if Group.objects.filter(name__iexact=self.name).exists(): + raise RuntimeError( + f"Save failed. There already exists a group with the name: {self.name}." + ) + super().save(*args, **kwargs) diff --git a/allianceauth/groupmanagement/signals.py b/allianceauth/groupmanagement/signals.py index 99af3d5d..60bbeb80 100644 --- a/allianceauth/groupmanagement/signals.py +++ b/allianceauth/groupmanagement/signals.py @@ -1,11 +1,33 @@ import logging +from django.contrib.auth.models import Group +from django.db.models.signals import pre_save, post_save from django.dispatch import receiver + from allianceauth.authentication.signals import state_changed +from .models import AuthGroup, ReservedGroupName logger = logging.getLogger(__name__) +@receiver(pre_save, sender=Group) +def find_new_name_for_conflicting_groups(sender, instance, **kwargs): + """Find new name for a group which name is already reserved.""" + new_name = instance.name + num = 0 + while ReservedGroupName.objects.filter(name__iexact=new_name).exists(): + num += 1 + new_name = f"{instance.name}_{num}" + instance.name = new_name + + +@receiver(post_save, sender=Group) +def create_auth_group(sender, instance, created, **kwargs): + """Create the AuthGroup model when a group is created.""" + if created: + AuthGroup.objects.create(group=instance) + + @receiver(state_changed) def check_groups_on_state_change(sender, user, state, **kwargs): logger.debug( diff --git a/allianceauth/groupmanagement/tests/test_admin.py b/allianceauth/groupmanagement/tests/test_admin.py index 5b32ba10..02c5a848 100644 --- a/allianceauth/groupmanagement/tests/test_admin.py +++ b/allianceauth/groupmanagement/tests/test_admin.py @@ -10,9 +10,10 @@ from allianceauth.authentication.models import CharacterOwnership, State from allianceauth.eveonline.models import ( EveCharacter, EveCorporationInfo, EveAllianceInfo ) - from ..admin import HasLeaderFilter, GroupAdmin, Group from . import get_admin_change_view_url +from ..models import ReservedGroupName + if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS: _has_auto_groups = True @@ -396,3 +397,108 @@ class TestGroupAdmin(TestCase): c.login(username='superuser', password='secret') response = c.get(get_admin_change_view_url(self.group_1)) self.assertEqual(response.status_code, 200) + + def test_should_create_new_group(self): + # given + user = User.objects.create_superuser("bruce") + self.client.force_login(user) + # when + response = self.client.post( + "/admin/groupmanagement/group/add/", + data={ + "name": "new group", + "authgroup-TOTAL_FORMS": 1, + "authgroup-INITIAL_FORMS": 0, + "authgroup-MIN_NUM_FORMS": 0, + "authgroup-MAX_NUM_FORMS": 1, + } + ) + # then + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/groupmanagement/group/") + self.assertTrue(Group.objects.filter(name="new group").exists()) + + def test_should_not_allow_creating_new_group_with_reserved_name(self): + # given + ReservedGroupName.objects.create( + name="new group", reason="dummy", created_by="bruce" + ) + user = User.objects.create_superuser("bruce") + self.client.force_login(user) + # when + response = self.client.post( + "/admin/groupmanagement/group/add/", + data={ + "name": "New group", + "authgroup-TOTAL_FORMS": 1, + "authgroup-INITIAL_FORMS": 0, + "authgroup-MIN_NUM_FORMS": 0, + "authgroup-MAX_NUM_FORMS": 1, + } + ) + # then + self.assertContains( + response, "This name has been reserved and can not be used for groups" + ) + self.assertFalse(Group.objects.filter(name="new group").exists()) + + def test_should_not_allow_changing_name_of_existing_group_to_reserved_name(self): + # given + ReservedGroupName.objects.create( + name="new group", reason="dummy", created_by="bruce" + ) + group = Group.objects.create(name="dummy") + user = User.objects.create_superuser("bruce") + self.client.force_login(user) + # when + response = self.client.post( + f"/admin/groupmanagement/group/{group.pk}/change/", + data={ + "name": "new group", + "authgroup-TOTAL_FORMS": 1, + "authgroup-INITIAL_FORMS": 0, + "authgroup-MIN_NUM_FORMS": 0, + "authgroup-MAX_NUM_FORMS": 1, + } + ) + # then + self.assertContains( + response, "This name has been reserved and can not be used for groups" + ) + self.assertFalse(Group.objects.filter(name="new group").exists()) + + +class TestReservedGroupNameAdmin(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user = User.objects.create_superuser("bruce") + + def test_should_create_new_entry(self): + # given + self.client.force_login(self.user) + # when + response = self.client.post( + "/admin/groupmanagement/reservedgroupname/add/", + data={"name": "Test", "reason": "dummy"} + ) + # then + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/admin/groupmanagement/reservedgroupname/") + obj = ReservedGroupName.objects.get(name="test") + self.assertEqual(obj.name, "test") + self.assertEqual(obj.created_by, self.user.username) + self.assertTrue(obj.created_at) + + def test_should_not_allow_names_of_existing_groups(self): + # given + Group.objects.create(name="Already taken") + self.client.force_login(self.user) + # when + response = self.client.post( + "/admin/groupmanagement/reservedgroupname/add/", + data={"name": "already taken", "reason": "dummy"} + ) + # then + self.assertContains(response, "There already exists a group with that name") + self.assertFalse(ReservedGroupName.objects.filter(name="already taken").exists()) diff --git a/allianceauth/groupmanagement/tests/test_models.py b/allianceauth/groupmanagement/tests/test_models.py index b8179550..dcf9a1e2 100644 --- a/allianceauth/groupmanagement/tests/test_models.py +++ b/allianceauth/groupmanagement/tests/test_models.py @@ -5,7 +5,7 @@ from django.test import TestCase, override_settings from allianceauth.tests.auth_utils import AuthUtils -from ..models import GroupRequest, RequestLog +from ..models import GroupRequest, RequestLog, ReservedGroupName MODULE_PATH = "allianceauth.groupmanagement.models" @@ -284,3 +284,22 @@ class TestAuthGroupRequestApprovers(TestCase): leaders = child_group.authgroup.group_request_approvers() # then self.assertSetEqual(leaders, set()) + + +class TestReservedGroupName(TestCase): + def test_should_return_name(self): + # given + obj = ReservedGroupName(name="test", reason="abc", created_by="xxx") + # when + result = str(obj) + # then + self.assertEqual(result, "test") + + def test_should_not_allow_creating_reserved_name_for_existing_group(self): + # given + Group.objects.create(name="Dummy") + # when + with self.assertRaises(RuntimeError): + ReservedGroupName.objects.create( + name="dummy", reason="abc", created_by="xxx" + ) diff --git a/allianceauth/groupmanagement/tests/test_signals.py b/allianceauth/groupmanagement/tests/test_signals.py index e9e47f36..efa35dda 100644 --- a/allianceauth/groupmanagement/tests/test_signals.py +++ b/allianceauth/groupmanagement/tests/test_signals.py @@ -6,6 +6,27 @@ from allianceauth.eveonline.autogroups.models import AutogroupsConfig from allianceauth.tests.auth_utils import AuthUtils +from ..models import ReservedGroupName + + +class TestGroupSignals(TestCase): + def test_should_create_authgroup_when_group_is_created(self): + # when + group = Group.objects.create(name="test") + # then + self.assertEqual(group.authgroup.group, group) + + def test_should_rename_group_that_conflicts_with_reserved_name(self): + # given + ReservedGroupName.objects.create(name="test", reason="dummy", created_by="xyz") + ReservedGroupName.objects.create(name="test_1", reason="dummy", created_by="xyz") + # when + group = Group.objects.create(name="Test") + # then + self.assertNotEqual(group.name, "test") + self.assertNotEqual(group.name, "test_1") + + class TestCheckGroupsOnStateChange(TestCase): @classmethod diff --git a/allianceauth/groupmanagement/tests/test_views.py b/allianceauth/groupmanagement/tests/test_views.py index b90775e1..0c723ec9 100644 --- a/allianceauth/groupmanagement/tests/test_views.py +++ b/allianceauth/groupmanagement/tests/test_views.py @@ -1,10 +1,7 @@ -from unittest.mock import Mock, patch - from django.test import RequestFactory, TestCase from django.urls import reverse from allianceauth.tests.auth_utils import AuthUtils -from esi.models import Token from .. import views diff --git a/allianceauth/services/modules/discord/discord_client/helpers.py b/allianceauth/services/modules/discord/discord_client/helpers.py index 859b3d01..50e7b7dc 100644 --- a/allianceauth/services/modules/discord/discord_client/helpers.py +++ b/allianceauth/services/modules/discord/discord_client/helpers.py @@ -1,4 +1,5 @@ from copy import copy +from typing import Set, Iterable class DiscordRoles: @@ -39,7 +40,7 @@ class DiscordRoles: def __len__(self): return len(self._roles.keys()) - def has_roles(self, role_ids: set) -> bool: + def has_roles(self, role_ids: Set[int]) -> bool: """returns true if this objects contains all roles defined by given role_ids incl. managed roles """ @@ -47,13 +48,22 @@ class DiscordRoles: all_role_ids = self._roles.keys() return role_ids.issubset(all_role_ids) - def ids(self) -> set: + def ids(self) -> Set[int]: """return a set of all role IDs""" return set(self._roles.keys()) - def subset(self, role_ids: set = None, managed_only: bool = False) -> object: - """returns a new object containing the subset of roles as defined - by given role IDs and/or including managed roles only + def subset( + self, + role_ids: Iterable[int] = None, + managed_only: bool = False, + role_names: Iterable[str] = None + ) -> "DiscordRoles": + """returns a new object containing the subset of roles + + Args: + - role_ids: role ids must be in the provided list + - managed_only: roles must be managed + - role_names: role names must match provided list (not case sensitive) """ if role_ids is not None: role_ids = {int(id) for id in role_ids} @@ -74,15 +84,21 @@ class DiscordRoles: if role_id in role_ids and role['managed'] ]) - else: - return copy(self) + elif role_ids is None and managed_only is False and role_names is not None: + role_names = {self.sanitize_role_name(name).lower() for name in role_names} + return type(self)([ + role for role in self._roles.values() + if role["name"].lower() in role_names + ]) - def union(self, other: object) -> object: + return copy(self) + + def union(self, other: object) -> "DiscordRoles": """returns a new roles object that is the union of this roles object with other""" return type(self)(list(self) + list(other)) - def difference(self, other: object) -> object: + def difference(self, other: object) -> "DiscordRoles": """returns a new roles object that only contains the roles that exist in the current objects, but not in other """ @@ -94,11 +110,10 @@ class DiscordRoles: role_name = self.sanitize_role_name(role_name) if role_name in self._roles_by_name: return self._roles_by_name[role_name] - else: - return dict() + return dict() @classmethod - def create_from_matched_roles(cls, matched_roles: list) -> None: + def create_from_matched_roles(cls, matched_roles: list) -> "DiscordRoles": """returns a new object created from the given list of matches roles matches_roles must be a list of tuples in the form: (role, created) @@ -107,7 +122,7 @@ class DiscordRoles: return cls(raw_roles) @staticmethod - def _assert_valid_role(role: dict): + def _assert_valid_role(role: dict) -> None: if not isinstance(role, dict): raise TypeError('Roles must be of type dict: %s' % role) diff --git a/allianceauth/services/modules/discord/discord_client/tests/__init__.py b/allianceauth/services/modules/discord/discord_client/tests/__init__.py index 7ba14d17..48be4f00 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/__init__.py +++ b/allianceauth/services/modules/discord/discord_client/tests/__init__.py @@ -6,7 +6,7 @@ TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY' TEST_ROLE_ID = 654321012345678912 -def create_role(id: int, name: str, managed=False): +def create_role(id: int, name: str, managed=False) -> dict: return { 'id': int(id), 'name': str(name), @@ -21,8 +21,10 @@ def create_matched_role(role, created=False) -> tuple: ROLE_ALPHA = create_role(1, 'alpha') ROLE_BRAVO = create_role(2, 'bravo') ROLE_CHARLIE = create_role(3, 'charlie') +ROLE_CHARLIE_2 = create_role(4, 'Charlie') # Discord roles are case sensitive ROLE_MIKE = create_role(13, 'mike', True) + ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE] diff --git a/allianceauth/services/modules/discord/discord_client/tests/test_helpers.py b/allianceauth/services/modules/discord/discord_client/tests/test_helpers.py index e19140cf..6a65cca2 100644 --- a/allianceauth/services/modules/discord/discord_client/tests/test_helpers.py +++ b/allianceauth/services/modules/discord/discord_client/tests/test_helpers.py @@ -1,6 +1,14 @@ from unittest import TestCase -from . import ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ALL_ROLES, create_role +from . import ( + ROLE_ALPHA, + ROLE_BRAVO, + ROLE_CHARLIE, + ROLE_CHARLIE_2, + ROLE_MIKE, + ALL_ROLES, + create_role +) from .. import DiscordRoles @@ -143,6 +151,16 @@ class TestSubset(TestCase): expected = {1, 2, 3, 13} self.assertSetEqual(roles.ids(), expected) + def test_should_return_role_names_only(self): + # given + all_roles = DiscordRoles([ + ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2 + ]) + # when + roles = all_roles.subset(role_names={"bravo", "charlie"}) + # then + self.assertSetEqual(roles.ids(), {2, 3, 4}) + class TestHasRoles(TestCase): diff --git a/allianceauth/services/modules/discord/models.py b/allianceauth/services/modules/discord/models.py index 321cd183..33389845 100644 --- a/allianceauth/services/modules/discord/models.py +++ b/allianceauth/services/modules/discord/models.py @@ -6,11 +6,12 @@ from django.contrib.auth.models import User from django.db import models from django.utils.translation import gettext_lazy +from allianceauth.groupmanagement.models import ReservedGroupName from allianceauth.notifications import notify from . import __title__ from .app_settings import DISCORD_GUILD_ID -from .discord_client import DiscordApiBackoff, DiscordRoles +from .discord_client import DiscordApiBackoff, DiscordClient, DiscordRoles from .discord_client.helpers import match_or_create_roles_from_names from .managers import DiscordUserManager from .utils import LoggerAddTag @@ -109,11 +110,16 @@ class DiscordUser(models.Model): - False on error or raises exception """ client = DiscordUser.objects._bot_client() + member_roles = self._determine_member_roles(client) + if member_roles is None: + return None + return self._update_roles_if_needed(client, state_name, member_roles) + + def _determine_member_roles(self, client: DiscordClient) -> DiscordRoles: + """Determine the roles of the current member / user.""" member_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid) if member_info is None: - # User is no longer a member - return None - + return None # User is no longer a member guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID)) logger.debug('Current guild roles: %s', guild_roles.ids()) if 'roles' in member_info: @@ -128,10 +134,13 @@ class DiscordUser(models.Model): set(member_info['roles']).difference(guild_roles.ids()) ) ) - member_roles = guild_roles.subset(member_info['roles']) - else: - raise RuntimeError('member_info from %s is not valid' % self.user) + return guild_roles.subset(member_info['roles']) + raise RuntimeError('member_info from %s is not valid' % self.user) + def _update_roles_if_needed( + self, client: DiscordClient, state_name: str, member_roles: DiscordRoles + ) -> bool: + """Update the roles of this member/user if needed.""" requested_roles = match_or_create_roles_from_names( client=client, guild_id=DISCORD_GUILD_ID, @@ -143,10 +152,13 @@ class DiscordUser(models.Model): 'Requested roles for user %s: %s', self.user, requested_roles.ids() ) logger.debug('Current roles user %s: %s', self.user, member_roles.ids()) + reserved_role_names = ReservedGroupName.objects.values_list("name", flat=True) + member_roles_reserved = member_roles.subset(role_names=reserved_role_names) member_roles_managed = member_roles.subset(managed_only=True) - if requested_roles != member_roles.difference(member_roles_managed): + member_roles_persistent = member_roles_managed.union(member_roles_reserved) + if requested_roles != member_roles.difference(member_roles_persistent): logger.debug('Need to update roles for user %s', self.user) - new_roles = requested_roles.union(member_roles_managed) + new_roles = requested_roles.union(member_roles_persistent) success = client.modify_guild_member( guild_id=DISCORD_GUILD_ID, user_id=self.uid, @@ -157,10 +169,8 @@ class DiscordUser(models.Model): else: logger.warning('Failed to update roles for %s', self.user) return success - - else: - logger.info('No need to update roles for user %s', self.user) - return True + logger.info('No need to update roles for user %s', self.user) + return True def update_username(self) -> bool: """Updates the username incl. the discriminator @@ -171,7 +181,6 @@ class DiscordUser(models.Model): - None if user is no longer a member of the Discord server - False on error or raises exception """ - client = DiscordUser.objects._bot_client() user_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid) if user_info is None: diff --git a/allianceauth/services/modules/discord/tests/__init__.py b/allianceauth/services/modules/discord/tests/__init__.py index aae30b3f..ab6023db 100644 --- a/allianceauth/services/modules/discord/tests/__init__.py +++ b/allianceauth/services/modules/discord/tests/__init__.py @@ -9,6 +9,7 @@ from ..discord_client.tests import ( # noqa ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, + ROLE_CHARLIE_2, ROLE_MIKE, ALL_ROLES, create_user_info diff --git a/allianceauth/services/modules/discord/tests/test_models.py b/allianceauth/services/modules/discord/tests/test_models.py index 332b8cb3..bf23f573 100644 --- a/allianceauth/services/modules/discord/tests/test_models.py +++ b/allianceauth/services/modules/discord/tests/test_models.py @@ -5,6 +5,7 @@ from requests.exceptions import HTTPError from django.test import TestCase from allianceauth.tests.auth_utils import AuthUtils +from allianceauth.groupmanagement.models import ReservedGroupName from . import ( TEST_USER_NAME, @@ -15,7 +16,8 @@ from . import ( ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, - ROLE_MIKE + ROLE_CHARLIE_2, + ROLE_MIKE, ) from ..discord_client import DiscordClient, DiscordApiBackoff from ..discord_client.tests import create_matched_role @@ -294,25 +296,33 @@ class TestUpdateGroups(TestCase): args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args self.assertEqual(set(kwargs['role_ids']), {1, 2}) - def test_update_if_needed_and_preserve_managed_roles( + def test_should_update_and_preserve_managed_and_reserved_roles( self, mock_user_group_names, mock_DiscordClient ): - roles_current = [1, 13] + # given + roles_current = [1, 3, 4, 13] mock_user_group_names.return_value = [] mock_DiscordClient.return_value.match_or_create_roles_from_names\ .return_value = self.roles_requested - mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles - mock_DiscordClient.return_value.guild_member.return_value = \ - {'roles': roles_current} + mock_DiscordClient.return_value.guild_roles.return_value = [ + ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2 + ] + mock_DiscordClient.return_value.guild_member.return_value = { + 'roles': roles_current + } mock_DiscordClient.return_value.modify_guild_member.return_value = True - + ReservedGroupName.objects.create( + name="charlie", reason="dummy", created_by="xyz" + ) + # when result = self.discord_user.update_groups() + # then self.assertTrue(result) self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called) args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args - self.assertEqual(set(kwargs['role_ids']), {1, 2, 13}) + self.assertEqual(set(kwargs['role_ids']), {1, 2, 3, 4, 13}) def test_dont_update_if_not_needed( self, diff --git a/allianceauth_model.png b/allianceauth_model.png index 09b93574357068f678b8cd6b33c8fce50be0d84a..e3fa337fab52e27d166c13181fd9a7fcf9bcee4a 100644 GIT binary patch literal 310621 zcmc$`bzGI();7E_z(5p1I+PUYZZPN;kPan9x>>ZM(x7xBARtoGASFt7Nq2X5zGJ!1 zKIc5&`|S7M@7}-tTWmIW%{k^6*SM~0E?)&X@f+8Ot|1VJ8f)2mA zY;fZ$e4y(|i;E!6QU83ZOb%`O^Kh_>_|KDz0S#w?4}Je957~f9dZb z`iRS@TSap7=1o)YdlZB-)6-t%<>f`coL+r*SZ!_&r_UyR?_*#?6p8b%6Oh&~VGusI zh?BdCoF46>{x&psT;?%Fzi$0~**u@%;9%748g}v-YVEA8Uo3Pc^DpB`aqa<&Y;@bL+kSrt(568(D|w??cS9n;4h^#`1+B8#42V@pO9 z6tJ(Yt?{^&YVhM?HyJ$O=lQRh|An@PBhHwDxGbrnRTZMA*Ng%*TN2YJbtC z{a?RI3GD7LkI!a}mW=v+G?bC*&wqQCGguy8!%m~d`WRjdnIIF-7amTps=<$mjl?=Q zIG9*lvsh(;k!QPunjmDojbX>5@}r!zenuV3he;KOSIZn5oFAnor5UHws;JKH55>5T zvAp2oYt+Z-tJMA77mO~1 z-QlZ7C6nRqD3O`Ee4}-h_pNavvCe^q)(dJzm}H{Qt!{~*AN33+p~BeUn)Y|Q7j=Gj z{=_ybT-Vh5E(Jvh9V%5cviEHU&doH={aEdEc`Gie_%rW{_PANy%5IB{VGuIAhSk#D zU2VB7)38}7)Ai0{%Ae82udv64-O#rA(dz5#Y!2t_Bh0o9gL38bSLM#DnHNS%sWTnU z>pzgwTu`W#5p|E}@E#~tTYEd!z^aRea4uOASyzcK#c@u2<;EX1b!atrNo9RdDj}|n zV5ZMY#h&PC;U~)Zi_BZQQ-dU1!e|f2xwM^1QiImFICeynwWw zGkO`R!;$-pJpJVIRm`h;i$Q^{$M)3L=w5a6yp(SL73G8k0q9H5zRE3s4;*lHlFv8n zZa!T*7{Bm3Q?$pA82hm0B4gKEM{>B$SN4Ln)cUYLnWbT_4 z-7hhhl+zGlyXn>Cx&BD_X9`8r;)i(j+&&J*seA*HuVG58RhuVo`VxORz^9JY+_je@F8=;f5Yz=`VM=T^cWc(jtG1*mTUg z|Frb}b#WQJ9sY}@uG@UESnG0%{U?m3sEJ=)DrYmAlv#T}+OQg!hFB4BkJ_&~ee;fz zrY9@n`t{nIF*%(g52sW1>cS`*E1%xVoWmeSYgcN1&`Q1J>gz@mLu2D z<;6m9!>Gaau>GOHaG5Rd0?{aq>(ALwrfCH9;S9f8i=+=;#B=NJleI>!V}3T6OqM)~ z=teRr{@Bbr{!cd9%2%uQpfz9SG*a$3Gdn(g7;+jh;~wiiGMr3eUt;|$s@sxhG`85V zrORz6n(ek{xPIESV(R-0EsZj>yKMsHsjR8oCGi?-x*}|eZ_67RYU>ed99V{y#vM}~ z)k!j4w_QtY?%8^1*Sy_Va@YIR6?NhuJtSpn*4}M8m;!S-I5f1?^2@zltNiWYawKa~ zhxj6Y4Lsh~$kFME2^ZOjA*32AoL9QlzEHogv6~A2l$6Gn&o)i@%FIgGp7zHj1s3O6 zh-N(cN0m~)L&NsR)>?`*xd)Lew-64+#bI_2j8y7ha@fEKmCKW$eR>wp9b$g$kUOs_ zi03zE*8Q>C@<RZT6<9^CRA@y|o%R$3;(h?U@-#r&>WZ8e25mIT&g* z>$N_LewT}>uduDX4-Zl<`gi+u+mr}g<{1$=@0GMu(d#^E17{~~rcrwyi9H2K7r7I+ z2))bKafk@pEyefMM=*>VC=end;?~P&qg2N?4-=om(3Wz(Y%bk@FAyM%#jKmif#L6m zZ*$=;!-F?I3-=KP{K0tiGneAnDw%XEa1ojN7L*5Tt5mE*!-F{Aocz^^_Z%E>>6N2w zRnJ(>r56Q^KQsR)X?uS8|z1#h`z(WZ>T4if(BVg0zwU0{?rx`w}n5ol}`$Z^l6ThBx0znG-fRXy0@d_``f^2HNrC<+&0vn>NS1@}mU|ey`MijU zDDG;L^W6`zMzg9bL#1XT-@lDQG2p}jb}cVoH*W2RRoU+pn(UdZV+_@6@uQ6i&T z83om-^JMqP=$N7HR3jv%SQ*lR^!7?bZP?gIv5$ef>7GTZ;yRbi&fd>^rd)GwYzg1P zzoJD1KA{e4N@#nnHE4^N>wk9%{Rs&|eYJo}vp%Glfw03lZaI^>zqs~PMh#PwNB6Z| z$-m+MR(>Y{x{t=6Wh=RxV%fQ2^eegx-kDgXij>e?8m|m|(q{&fHIn>2-^iD|k(B_m zwzf{Ori{Z}drW4#6oZwOVKfihlgN-B?VEFlO3&E`Z^sV$Ni|%T@)>-u!+4%I=d;yl zQJwRY*Jj(~>C*KAjye#CuaOyzVeY@^o|O{NGR5i7Z~j8O6&1ThNE-jj)txrT>TD_1 z+wox_xf)St5vL+EuCz^WgcNKZ?5;}B06*9F_1QM z$+|B4ROVWxWLF+eFBLV#Q_`LeIkFF1N@_*~7z?)V{thwK+&bO)c0%><2dFSuLE-ks zmSmS6F2q4RkBp32fJu4ROM_u@jD@j6MyLp!ql@gEu~@5<>AI@jsn*hc!LfBo1+;^1 zTUX+ViY2xOAbRy5daprIx0!ahf+0uFy}G@nE%&>7_NDTwJ(IJgalbF+P8`q3$^n2T zzF^XycT30$bB28`vPamQEg4qnoLoetGbb32FY3Ggc42uY6<+t8#cE+i@@#pVJ$q_R)*BFMqkE@>jeVQz*TAYLI<8LlIS7i zT{1GVg=H@vA1dR+tyw20r_NMqd{b}eN}8ZM-Q3))85LX{{e>zaY@u(fJ-TyR8Q&yJx zlZ`fD=fq8Zvfg6XpJ!ET5cAvSMNI@}z!CDhNqMS&DOAyR&%M!?YOD5scviK~Ijn>Q z0z51(u{F6ciJy1(Wa^3tW06W9AIT<^S1*@C&t2n%VQp=l=Ww6m!X;W|Ib?z^Eq-?(4{P8+T!kp`j$7#?az!RI~Z#kT9AW zY>$t1v@F;jxVJUi+~%`Jj`(KQkgM?nmFo0F%kt+@6WX(km&^T0VeI7XNcncF-b;ld z8UUiGTZ*2chg!eK!WU_RZX+S?qhrHD$=mb{guL%G zW;1UQVYe4P^WPX-lg2w{a20+@HCVul%GWf^@uqxpfY~6&woS=8<@b~5l0$Zu*k!vz zDCofl>%8L|Q{_vIo_7?NuUVg}u*3$h%pU*tzZ!^H@`W6F;Mk?aN_WEFEy|yq2mMA$ zNq7D}h&_;NJYsX<}OJL;t!f%9T`Bc&+UxcBqL; z%e%{(eJo4!i;~T#Ot&^IoMth*)Jh`YC+X&$x4R(Jj7u;1@xnXAfYtN#I_v6ZC9Lai z$Cu}GPM*Mcm>n81-HDTC2jDn&aI}#ju8VHBAXeG@dy&Wa7Nom2_ozC}^m}B}@1J&O z+;0(+HC$_$e#x244CwP4j~HZyj{)C`91#C`pa1$P#5!C$BGozC50N!o9dCc^h)^h2 zy9$di;j`=1JB7vB%F#&E5{rC@Tf~Z)Io+aiz|X^LQO%b|%PxEBS6x8$g=dV16pL*L zJWK5csC9@5jT9_+UVRRt=%V)6P9H0yT-|FAv$vjo(e66A${4ZptHBbd-(qxTA{qzU zq1W-*P*@6)744`(B!K=8><RA&Eh%Z@puo9i>P>(yQ%-%nv?2^rxq4(!@?V?F4mK=rQ`RP=EjLL+7^nQl;crT-jCs zfQj$~Mu^n9!C&FGM`Lce?sH+O7j$L3IHAbB-hUD8rB)?ZneEJH-D3;MrR&Ip!mqc3 zL;))n@B~E-5WH_YdcP2bVP`Vpch9o=2}C43?dvzlEp=t@abL=~YUitefc`G3C&FCl zB=wv|J0s!UPp>Ri5(ZTieqBBT+%T)mTA|eUJ zvn`R+lanu?!JFZo|C{RkFh62C`(WF{Vl+c3kLb{5c2dXbzIgj-)5@;&;b|_k_{=@1 zx~fd~tZ(PY-H8C-D`#r9Jg(@tSd*0vNocn^^fz1ekz>*2>z}mS7@A@&iY*zm#k?i3 zI97NA;ZBQ%gF~!8WWhl+xYvO8o!eY`Kh0cyXPEDQuf$nGK$0mlY6pq^)|pSr_YS{d$5p_mBiSS zgGnDr9^h$jE?21Mn_qPN^_Wf*deoSTcS^6{^kK28Lx&%*xa$Ekkp#^3$8?E5)bmX0 zr?4Pf;C-N4w_DAEpWMrS|7c#5ApGMrF+b3Fm_FigQu@L?qjD^WH^c z6&h&*Ma`=BI4I(v37xDQ#rLe>wMAVxrvGcO(%G9!?=R&+p`APZ9>VkFM0@G>QuOrl z6R2_-I%yOz2DI`)S{1|HfzUtOZm*d`9R3X8tfo5jcqKZos>Ws`mA^{cd-^Xrd&e0{ zGLR&%mkjAqAUw39@?dqLifX>|Imd)cXYj@s2^&6x^qNdgSzjaRqX=4KCiJphAV|cZ zIcYrn;qO}M#N~Y=kNCi>O7`A=6aqK~cDRdcHRmgbiVb|#C0;otjVRmS`o6x^;Ff(c$l-o`$abu9U>P|s+{q9eDjyG zOX>xL3t5D0Cp_l&;Pa^O;hF5V8~15;N$dMkj!SXyn!o1tWGLrjviCDhdfSh=j=Heu zR0quGN~hW?QmR9Co8^26O>wiI(1D?8A`< z_DKSVoBZxkKPK+Rk;Z?~-h8&CiVW1;U)R&ue{%XD7aNc8b!9v$6qs99yZP$cpk9qNzU|0 zxCf#U*aAd*ker=G?}hUgd^K2&N!XJD4^sD(G;3$4OMC$l+Cb*SI!v4hR5k6Ylq9Za zh%-@X@2ijC+9e95jJ>%3^ND)3u+ZR=Mv7dFTc&CmU$}JrxbLf(3X_rV9+AMW$~cbv zPZCwc4i`w&*~-57FVCv37168AZVW0lB_yIh(@g9nHQvdsck8lFtqi7zH#j=riV61rbsBIs}X4V6(Q@lTXoW1U)zZ# z@vO5)ir&Gr*-Wh$r0LIDs<#0NcUI-_?!ujI#k4O486AsAOVOE4RQ6EQmXNp-6cnVK zrS`GFxPwNser${k0|Vpd&z}j_b`-%4AszP4%}b6C;RO#Jmd~_%IJIto-$3?Lm6)uEmCY+_0p;nfP#iwQp@WNYg7Vz{Rrh*U|LG)~@6hRZiHDyzIv4 zuy0WCy2s{YIU_?pPO}$M-^B!dj5_zH%ZP4Ydj53$b~CWO(UtgDZx3Df_q$8M*UW(D zsrN3<(*x(5Z-;!|n|EkF0YMZDY!pp-DB!&{^;Af{`$?Hz9-dnQRY9ly9~wqvX0T~t zz*%GaA{*O2V#*SeKVzNcJBNj6XS^nEyY2`28twtNsr&22cG3kF()II}G%;QuzI~!U zSTm0k)QbK~*pS^D`D^wVUNntd@q{JuH#1!DPVd(FPWEey(Z!K3)OFW+Kla=!H_`37 z5G1)IRhnV-vuEHZ1*Ow19DNm<7jojxK-%YnD3)#~+nDRge~z#9D` ztlQ7={`6%9^Piusa&MT;2i3fM-g~n6R=iHw(6l3C9ORxbW}R!A#F|(MyfwfcqTN?2 zyo7L(r*^xoD)s*)do##1K`Tz%A(qSdS;u2NNx~mYjy|>CiAzX&%9H*pziuCKwaIoInp1t#<yI|>8xTWr39_Hq8_9!y%iCU#`X2uGb z4dSYB5kE%19miGLyaq{XvhTDU0TAl-giD0{4^RnaS6RuorqRh`s&x|ebu!gC_gsin zGV4l&wiN~UcIviQ_}v0>oo~*S_7kC`lqbgWTVo}m*0|D_YtkUCn*vXEaO^N@F@IUNn0;4&3vifWwu7KBQ8rvN~3)TJuV9eg10j=|k7s zCEHOqQA(sGw^_S&E+31?^57v5utNESD#fEMksuzKx2V`aez| zf#&h?#45LA#T+eOi;>bAzdQ5<0#4tGeuc=GE%#+>RXT?Uk@B-yO+0izn8d(i(vmno zJ8f$o9HOaq;Jtg-lS0(*7A~%cfdLg37FHa;W01q@(9<*-!V4EJ3>2E+tXJ-nl~|4& zf%C+ufuD~r#yvsUL#WbaU#`3UaC_dQCk;Zd_Z|z&2SJy;&MG&TZe!| zMvnh6AsJoAnmIIVa{0QiJ=1hBeL{G2<`^bws2O~4x2c$j+)k$0OtwSLDEC(i1(4^Q z!w$>>VVl*hM$io_nQeDDBc?FX`_@M5hbo5|gqX_qLNkdcT2no7^=lZ~u=bzMjXlbtr3hhdo;M_GzT^K{CNUj^Yp=f| zfjOO)v)H2PufA?V&C?^~@X}(&aQEinwkk8L+1?E9vLwBV^xQ3y(`9)LOm5+QXqqL9 z&79}Opqp|G**0JyA}F__(Z_G+7#4D>zf{ow`^bUCr4nTSzSZ#;#o0ZPULl1hO^5g& z1A0OMNB0aCn-c?XUs_f+IWr@YC?r`O=R;L&HTf9fytD8q{K2y?I+c7FH_2v3c(?f( zs74q&Zv!hs1v4iX&xblCSEsTuf{}Q+KbMxP@5C3Urltn`RsmxA`Cd;ERaI3YVq*C& z`xXGx#gF$^(FXGL#Y6IsNN<@w(9F7YzIslFVSHrO1;iu%iQjmyDCW`tSd>y*fCav~ zvx!m#6JA#4hG({p1XrV$HOOJ}wIf9*J55eHxeB|!#gwR_lkWhl7iDKdFrk-tOfgC& zc}Ok{ErL_|Q-5P3@t@V6b($No-+gY5yL&^Ul^{VZ7+aDKvNfm%(q)bsUG6iUw2HYt zK2b_S%-ae%YE*_t0+l3oq_`jFMHeaR4(Ggr2lp$v@!bcdeoQf*Y)(H=35=Ipy_T!N zFTfr=XVreP(vllG*SQ&JWI`V2E112h{1+`L84q?gi8P>gU+>%?YiwCQJ3Y?QF2_SS zIywUA;X!UuA^hTYwW!I-yvc>NF!DMYi zd6Pfn#z>B^I0eV&PS1m;PJT=C+`0d9^;T#R+)2v8v^ibp9F@IV2?QSWF!KE6(EQl? zr?2ZV8LQweE-r>4ium;D?p#}}@#+UEobAocdu(hWT`A(R=tv4m$|i_7e|+Zp=PU}P zRxixV&C9GNFK}CoOr$5eODW1H2p|!q*3;++^WkD@&0@1G^@8_sHB`2;v*RG;v->eN zCNyeYY(9(!?ExO6#@p&@kFTf!?)y+DbiD_|?KP<6S%U?EsLl@|E7$h=9q2$8A3Uswlt;f2EqYc417{H#auXjug8fwtGV z!OJ+5tiL_eT8A$vFt|IaccxJo9f;%c8Q5RM3OL9>fkXS`m?;ORSudsUB4*m6WAiG} z^Bj+Ew=)m5wr5=51auXjZT{7|5@@X^a@PRpUjZ1@!Gl4sX(p7957aBD$ZUBX3Mh_3 zwIv8HmS;T`oW_;QFx(Vw$WIqVOO61}jRuy=zDBiq-`xDDpprm(FmZXgsVP*ypKB)m zSJf)sow7%;L6ZO$j<}qCj-<7>v`SPJWP=65LMiD})z2yU)0sXSu5yQ*fAIAYch@x?pa(%nUZghE{b)ku2p511{nDrA?Oz-INm+yasjU+S3>-f3vB^DwWdlA66 z{#JLH*43&{N=|!gO-fX+BY|e{8s)(lsfPOyhEFxWk?&7YbsoLjDdU7cW@GyY z?VL)~1ucJ@x4MkG*PukGZzGnrUmL&}RPbuW?m;AjKUfp_f=@pP@Y+D|#ymNreR*VU z`9D3l7%Zf!83u$#IodygPd@y<|Gdi8_%8Kze9mxbrX2KVriy=acUdFm+bB~TF5QPJR!IpD9nj9Bg4_-is+Ch6E9hUWFas0#jUiF* ze>47heDy4fBa=%!#!O3!e7TmNefPxYHnwnC4ci1&IpoW!^*aeQ#1}RiI(P{if zQZ`ZK)M@rd)8x}cbm!%8E-8R{6Cu_fTMrg2@HTrlNzbPd<(dZaDL+tu58jouYz+~p zCUcfLPx`qvw-e_`4~!P&--^AbB~}GHwCpg7yvyX?Tz-t)nubt#UGoTRgip(-GEqHo z$=Y}wPyB0e> zJcR)%PyUY_Ty%3e5X}@JhpD@Q4=Bf(LzU8AUe)+*aplMNQ~r<@dDjZm^II_mf~f~g zL=?#>QI+m=>qToKfArbB2i_eC&ROp1C6Kw8DQ64WX(4m9E^6#dhC_ofY3)6>J__3ARmb)D}^0fB+3T4gq2^r}Hn9o5I40!~5d{cDRrE1m^4 znSiA1M&*IN-t-x1Tv0(0YRvyX+>XZ|Zby>f6B+|w-yrLmMJ&)3KvMQ6tKpCvTN=v) zMlw1ERpP$Nmv4ighia{@0^Oj>2_Q+<(V_qU{@agX4=v z=tiRi!Xc5v;~M^FS2_0E$M?>0z;91lf-XzAKlZ_Uw_$R2Idn!+i>@d!W9(Q zk<5un-)(1`MV*|suFF;*uki~VEl_6Gp>O(snwpwwE#6)34<`3GVf|&F-Vb{u4pvLX zqc}`%tLGa??r-kx?O_se_)Pxt26~f>i1XDBZZepdKK7#aw!pE`aL9gX^lA;A$GxHN z6-~v$SON~(ZKl8Hx7QND$KUNfgJ{>StbMyLJT)3&0Z_s4VBo(wEbT@^s>Rxow~xZW z$*H_F+Gm!esNsIT%;ETWJyh6{j7Y^LUYj;JYL7EV2g4KkA`+yNYISA0dpeM%g<0F^ zhW_Cw)(Qmt64pJ3&C>zfB}f0^4v7_@ia?!ez~8P)vGHZ&$ejK8u!Ia8<~0FD(rm5` zf(;`6XMeG6IuMT^Tko?_`Fu@nLA6jl#V2Lwr-ysL)+0^af}0y13v2sUMw=%Hak}kg z9uJq1*n9x?90D+HT@L{+0U;Fz-{tm~)hMn8b&!QS$!uji2a=o0baz+y)ZFq0`-J-H zG;Jv@)AoJo`f}9O=7rrF${G2$L~qKYhy#mhZ|j2cz#}o6#{ChFsGI8I0<}3 z$ZexB=VivF-knd8AjLq zu~C8t0GXe))V2&?>6d@;pAulA>rLsQEb_%nmO^>@r*b4U=Ud~-OzII=-GubOgOA#T z;HXi`dKNi|cq>q0s(rN~8MIw&V*G~oX>VS~S@H)FPVbnVowi1s-_10CycpS>+f}Ti ze{^PP=VvV{=rafAr>$Qfm{G=ju#C6l;E2Ce*yRM%J*f8toF06WPdDWUhae`v?KbNl zd0_(lENQPlu!&BGdi{$LWV`JGQWO%Lpm?XnK{)vBFHB8aAXffzlbTy&(rsF~`Tuq) zR!p{f3ikfjH7|cOSSr0=zJtnR8Y-(}k28lXClt%jQB5N#vlZtm#)u1EUOaYtR3G;K zK-8A|)i~lj9=o^U8T7z`1d1cerzegGlo&cQk{GOA!U-;nzYgZ&TKy1hqLL|_PEOYo zE^DyC{pIdRMScW(NQT~3UW0G!4jy4mmi-uqYY`q0)N`60lN{t{ zRicMs7mektCXenk<1#*~b|7m#wZD8iR8DI`nO(|g(h{;+GXo``&8yBIf4dG}_Z9)c z%b^H%V>wVSu3WuZ)790Lbuv(=}l-T4)jG5#qE%Y*lp)zG>Xj*WsXh+cw7%yh`G#uG&T8N#i4m_ zX(^wnlI?VUx*yG|kKU6eJH6c@EN{aUA0HphYfEEcVSzX}JY=<*k&sIekj&94dpTYu zsFWsi&vL9HqqrI>3GTSQX+y*Y;a4?~vpW)Gi*jZd)V_J5S~j;ir2@BOhimvuGK2Ys zN@l{@eA3|$MDz6O2EN;8ikbrz-WVF5B&vmhMKUo_;|27hK!%fu&NX_~oX@Zx{%mfH zZU)Q6OTnp&VC9nQFj^((OT}l+%L7+c-IjJNV~%l8lHkeCvJ~hVO-I%RsObiJD+zjh z`I&)#^38f>1c3h#%p~F|uX1g>T=>i7NRsAjCo2T!o}fD(-DEW7o`6g_QsEMN(Ku*3 zyQ6|be_L>&0d`FgF7$c9!l8h+dnEXZ{&=Ft#`Jqb688gKCQtCb@wm}>s!B)BTMW3h z>y#mWpiJq|e1XC2?!n{_A8Ozj0owN9cNDDeuvRSzMmLEemuvRIX_kqDSB;~QKXJDDnx3?_e1|+53DWNVSmfq5!3rzP@>lrLCMglW< zU$dB@KUddyu{#x$n2TXBza@e(1&ZjUOP3T<#BMZ)(LI3}kL5C-s@xx&-di2cQp@|P zJl5C8)wYKU@>HqtFPbGx+T{+jKi^%)Wl-Dj-CajI?7TdYeFf3&S9x096J*+#Wl}6$ z1DzF}N)`d9X`jKsw--QvS)ZLChYC%e#Piu-!NfG^NDwTv98Ulp3P5w1-tc^Td6h2n<;Sbs=u~q^;8~sn!HG#Gz*A?7jgQ}u zDjoUdS*D7gAwzc0;9&GG@0&P;gpxq0D`lbFqzM(6j8hk^tgTs(^XVx}3V>$+W2bE5 zgB{m^-OUl#pW9US2Ox9K4&3f;M)To40c0BEv$x{1$bQ6naS=D z2sNvQEiMPTMzIM4;c+uyIRSvMbGrTocb~S;xfVmR`1ir56j5^;M=+i5@p!#1asfqn zoy$L1I_EU39pViwqn-+djtLp}hg)r`do7ipH=%N9wb`Kwxxb$5;sL}u0r*N&=F?qQ zHh7f?gc?rP611y`LX3zbcK+UXI%BQQMst<0#s6Q#Wk+@Ecm>U1eny}pb>y!@`VERy z7Y_DeBS|5Y%<(<&*zu9n>k;JP*Zxm-4e=_P5i>=KMDlm?(y}#6ZsX&NLu*Z^`1uNy zL1_o&5y@jM#>Aaf^x?z@Xb;7`ahvlo7KG#aUI=`+oSz<&C zr)fQ;L#U{T)uAGnH-fHbg3e@tV)`L$P_bF!TNu>ytyYIP5w31-z5xLS`)i{!E&J;| z_>LOZf@?lxVVL$bosCTr3^1j;+x{h zQB{eHt-^=CCM8th?=^;kCI3Dc!uym8Z01^{4K$_v3vU+_V{Xc$wm3)>#Zl!#GDpM- zxy3*>@4?9pA{VwOCci5lM3RyWmo7Qo3buf%)!=CUy=gq;(|`KRnX9JNTGjpk zDu3{?b-pr+RD1UO=*Q;zYp=Ur8gD#bKS`eW5MHfpBVj9X`HrzPH7Bn3vcju=4l4+0MV$POYXJbqic)XJ@VP{E@4}CG7S~$`0#evhMCe zPu^ob%1~C!)e+3f%HnZcBgx3faNe%4nY}jde#{8Yz1oHbrA!sZ1RzoK$LgdqVe#$& zwzjM~eBarrVAPS&2BeHywY#AGa^LK3zbjYsWly1K^laqEkFSAS8XFs7}V9(jYf~ZC_ez|YGZ3l4|3HkN!hJr ziVw@5v@qU%xyEo>v0T5(An@L?=q2W3Whxj$)aog-okxvwXfbpjeN`1bpCxf#_2`;c!5NdjmviXXyeyS z|5Wp? zZXdk=BKyeN6&y9)#7#tC5tMxHKxW4#wePD)q0ZoU%cEBKaXPS+JgIJQn%X3Tu z76d;(|JK$PnIsZ5nzx=k($h;WKIJeQU@0hZw6%Sbq!9IcPVqr_-H`dUcW)q)Vzi6@ z4E(L+tuHJ4XfX$zUM&&Kxt4z98t)u0RgOx7ah_)Eci)+DyPcwg0qo?iz4>;Wr z92y_r2K&v`DxEol1^1#HPFthdn_62%vQY^J@!2!bl{tE@2%eEfDHcXn1(4;201=yKAGwMH+3J(+&RN z<|`z^O9RRe?z}1@GB%#S-5{zucC^(U&UOF3Pgq!(-fmB)|D6uua{)-cQ{(4n+>kig z`Q^i<>Sz;PAD38+E_A2*kJ-Ha(r)1Db!+NrXmlw=tFp6xL(nWdBq&AQ`G(T%iNYQE zhAogrJ17qywC7fzNkQ>$7SM%xc)XCl=!zHK13i%EG5hDWk+RTOZc99uZ(H|M)6*lJ zK2hvSqUP(d&sW)gw}j=chrP_~_;tCJKdHDI+4H|Eax$SF9dsB~cToe5BqSufk9*D4)wQsN%WS|CxWPSok;|7a;{_-zD5x!uVq>Gk>e0-M0q(WSfb%b}d7R27DNM}Gp`#YMiHQlKH%lGo-D9*L zARBM&?$$LmU4`%Eppl9C(H6@s>nEw|CM6;92_~6`mlsULa*#S88S#}wey4?}x$?A< z(Dc_>rAuVLj>`=&!kWigw??rJt?m{Ha<29!d-P>%8vANza9xR1-j$G6lr4rT*Veupq0b_Rw3Ufa3O@AjrJnhGm*m)$l4NO*6*dt3wbIa(=* zM6g;xzg`l+gDqISnX18%t#SQ3;*QrKq!g>&-GGFVvgH%uWhs%%(JX17ILFcN7uH5{ znf3*MO06-6@VPz1cfehSbq&JoP7hEqup~#y9fP4y!6qdQjEwB3q?aFG(Sa9>FsOJk zdvBWf?XB5II#UnCY5UD+so7pXig!`*ac$i!RWetkz>)Bk`6SzeH>)`3b!?*gE8>~b z_*}_G{AUrGjkYQI!&hhG@Z&8ao(wI$+rfP>qKD9#sh2x2LX+0RBrhxbTTXuzP)!7r zwuV{x&!3M&LPMhkUAWxc-3u)Ms26e^?XP!C)%l~L6tPemS-st*-y;>sP@pYX+)tcr zY;2nC$wdeGL5bAS(SZOWkWn2v3XhJKR8+hT;n~gPvXqftcw-2#*Q>}_jSfX6B|>fs z63Dy|q}Bk|y?HO8(rS_2(T)k=YH#2kp&Dmt6g79>yDA7H3g7f`4W36PL4XkTB`3ER zv$E_QpHk7%nqn-?%*`dMWJ*d&C0mYHjeNKFhe(5#SFOP421v=&#g5k2UB#mux~Y#L2q~O_cW5$AkU-{T<4Zj@aFf>=1o9+SeZ{E8}{$^C>GR{DAI# zb7!ZQI3;6W2tX;yFEKKrRf)XX!jdlcYRMJ)2-zc#k?%rpNB{VmVJX~gv%_jHEWWqU@Iy|psqyG~?hllET*RM}Qov160h3EnE2B4*zppF90wYIYQ0j?0>953uwciSxW z(m|u-m7UE9=y@7;GlI<{XJlU~3?{>u(fl+9(kPr?7jb&DocrbXtKL_f%+)9P4rLj; zD+^sIc1yj!Yop~TaE8aArr;-t55e=b#O-+Rse-~oXz+4!axPuItj_e9}!FT z>f+>N&Bo-fyy9Z_S{DWT#qT#+c7>7`Ho50@?lCb1y?Jv19h1N_HkKHNMy8W678E-e zQGxANcBtxBu)1!WWMyabdYlQsCF>%H{n7z@qMAuSu%WQ5AXwz&{MG#w2Nbv{7^Wjz&2s{-Qy?AKSalqA`jn|yfi_# z#PzSIjveRAp0=wE4EP_eCfp$NY1qgy@mkIJdU92lMp3_hw@;N(zdLnIgQ4ii#YD z)rO7IoWfW!>T8VK$?UW-zi)zA*f1c@$1qk`F;!;7+YkpO3Q z9ptU;TWr4!Soo;Y4xzoKbHMa7-8@PCyG zHz*ZYz<>jAf-l#>8c949VZZJ}XWoG!{D8VSBCpuBYud8C^3D4^LO)o{{;9t?56&T9 zHVh5LU{i{HvtL#TosJfA<2yV&Y)uqS1ep3!RR<|*?3Q0#oc72IU8~Af-7+q1?D+HN z&rssT0CwF-!->$(l3V&pS+AqW>2Q;Izx7V67RD^!eN+hDd;5(6Ij6hV2UE74)y|Hxf- z!1Fg&m`_0T+E^K^`8==EE%O!TUc2YeWL#)@R>(DO?f!)4V3H<;+pVGNneNtFDPeD} zX2*oJ$$~~=b`rhB$OfbJx;IW|o{KL>5ow$ni}N*a;iEU~#QRpyMK9I3dB4#9tkm~^ zSbOhyF8BX`Ty@$*Lkn>-Rglriho8SFe z=RH54@9+Ej<9BYi_j%vWIq|w)*Y$Wl#(g}l=G3-g$GhSSxm?z3BORvJe~s?vHS)>q zlQsAnrSN_?HIMti9Q_lj)ZlAZZqe``eS7MLm6bfRZ+(LDLzKjc<%KbS<}j4pA8qEv zJLu@lE=7tfIU-DSKfgG)W&f#r^v<(?g@n*LFMQ_~^P!X0(~EgyT9M#v7;w3o*LvVJ z3aq=2Pd~t(Q@i|07;O@8n0j4a?hz2MueP>!10`j)^+jPf^eFh;ja#=i*wXg(^=YHe z1+~d|;zX2Yp@XWLn&oKw8PqMcY_luwjz$d$YtI@PF;k!mYi?~#12&0Yg~IeFx*aqM z7_2J2>pDB>aUM>awX16DHLe%`vWqHJ%HgOB&VAcO z@x9`1-qb+NU6d}XpCf98!(Lw&GzxA$#yzYly!EmF!P_4aipRW`gxGuO=)NN9Ixc+A zCi*(`wD>;n5#(Bp`HY_qH@)fp_Rr9y`d4VOuednx=7`7_qDU<(?zfdW{dA4k_>YIn zj(Jwb?iqz`HhsRY0yB&|f?TAxc73trc(Bm5VNi|RelTn%cCQj6)y+n$W720j5P~hQQOi*7sah zc}i#cTwf|>6>vZ?^XI{R1!Yf1pC>Ic-^uJ0OP|dg1|N1<+S0< zYI322KkP$$H&xu4c)!>FXN$gJdk*{a=g+6UxA~(54hr=Fz(fQcjY(=!!Kb|tt@!Ih zRh9D}@(+LP*|Fmd(8m4zH`&k&-@J8;!?^LpP-C*OMSfa>7IjP`2Vfy&ybBmsN!AHD zlU}d8X>v)c&JRQB2KkN2?(-eB3=h_mDhD`{&1KoKy}kX!0q@U{wynNx_9f5Izon(6 zYk0U~rqiVw@i_>v0rP;)$0tWx12@re;sb&B7~raZLjyl5xjQihNTAt^yOMtzZPbY? zc^OKU4!3UYqM@lMu%ASm-ivFZKy&gD)b4L%w4_qXr%O{KKi$Ut`hbNXG= zX7qeN%8#c|Ts1a+2sJ*DZ(h}GL#a16H%}is^y!S&zrzF_C0dafa4T`#H*DDOCx8>w z%GGPuOqZsE07L8(DlX-4qx5q>#* zM&(&yZN0e8imd0A6^|b?#VfQO6%`MCgqE8JIA|sYKH9wEK_9L&)RgR2Y`$004Y`H} zzZ>f|>a*DvvPenMc za!*|Qu$gs7(Sq^3z{)|2wr%eN3n;IInMqErlcu=Iw0P$y}h`Ct+&~CtyNDeRkn!c*MvVg6>W{EoCb9<5*#({ z@9L5_;g!?U(gIxga>)*=%QMyka%%UEGY5b&esyy~dk?E-l9iKMJ*$_er{~TJ038`F z+)C9RF*KY8+p&zVcKH@!-Jxf@?(v*4Hb7SQ;_fwmx>;7spmeLMs_KYy@ZCFiHf-9I zCU^cVl^Z2}V=g}fLwIwsepQI!`1_wg6|CH)v=y!Gj80Xr2ATQ|fan0F6* zvPcYRP9Ob#f%ISl+HqSKAaR<+|AxObhGfOE`GhY}usPC)4-2mP#HaPwUw@H(5@=St zdwPInQKBT*1-sM;>Rg#9o~o6^{18M-c^h3I1rBRt@Vc)6L}OJ_G#3{-SM&g@>UG*- zMkh%&&t_Q6|KzFHg%PK+W$sKYnCi@o-?qK|gwf^3foH=n82LT@d3Epw@8@Gni=~}Vf`14Xt@ruXr=OH`r zJizCNa%G@z$V3S;kfcEXRGPQGb43mUjVb}d0m}h<{)uRXkM}q4#THFg>9~jx)jS!G zp|V;VnU{AXBAzbjU1O>(KE#Y<(5J@xhx{8B)mLZ7Dkv3Pe&GN+l-mMxOiYc~;;bks zH0Z7RY8#i{H_l9dml&A$*;=IPt5A%r-YTHf8S34$R*s%-w zMYz%EqGKfJRepV-RzsFePgBumbbBAFD!bw>SG6mCS#vgAZoE!kt4E>Va-Hqao6AO(OUq4@U&vf}&qpZWO=5!&NnbEKQ zR^n*P*m2Vl$=~|=hdqY}#)PaGrZ;)DKffy6*_E_(I%wmP>uyP_ul>6YUlH`l*4OFt z-hfqHJ9t{#dfFEEFSZrG&gl&oo9PT$;#5{EJ%YO$AWSErVg_oPVl;b|-DvwqJQ(|t z)Uqw+N@BHjb(XfaC$pr6o|0B_`r}w3n^S$|foEbDy{Q~-kX_#gjD0NaIG zbIV=|M!8&gP%hs2P{PM3zg#$8Y3H6j>t1IXH=$$vnw;IiJb5EVCe$M_(KxXG_^(K= zQEKzg(H!hsfA%fwu?$v=RhWW*E?Il3~38O6G>Y5dEB*Nd?(-DrQnsr z&%3|Iubg_NC>z;k)}DV?d`as~Yq>`~&3@X2CH?PtT=@|OEgv3M$u80F&3ZSv#JZyU z?Fsio(-dYo_M304WB%}bVV7S;t3G`M$D%t_I*ej`;XViL+|uFAWm3_iB}b|D?D-M) zPSJ7uTyBS2g3?;%)&5m|g%cbbNBOq6be?qFQJmPk9JfhnOOTLP{F)0BwN?jiPDfL` zT(P;U%B@?E969p-ZOulJO`A8j z6xefc8P#9K(C&yq7PI6j2%so{P8qglFjWi8(5@_vxyZ*WVQBt3G$et39Pm?A^p~); z8G4mLuUA^1Ed1QDbLZQ!;uRUR&=WH=gcv?55KyBWwsYsso9KVf*bGs@?@4$av_@L} zKY!654?JXBeco<2jPw{HG_2!vgNolak!N=b-jVp&!0@i*v2*y)xp}*LH+dkETu%ri zW?v8Z8Fs7N4b~okLpaxYi5HOiK0tmkFiKHTQN1#4JH2z)u8-B#m{)l}1#xY@4T^_2 z8!%SjTaZyk|GB;Eqet5by#d(+@v>l6VLTmq3AR)MiGO%}hhb?+`d!@yVBCq#>dD1gqsExYxQ<0`@+KL-L@TA{Q`_;tJ(tdai{*n z0s^7Hm3Pt7V(Y9;bFqui;NT#HLmK_;_0a=!pH-^vZfT$=r~ZLQL~GGIu8~mgsh78` zrWXow=f{F;_3y6VqwaEyb+1p?m4__q+b3A}oL@|APK!_6vv*T-HV+o7TzS}M76 z;!NoGIb)_H-tx2A@9C5DzvN0c_sMQ-Ta520kUnD@GunT*f9=%|ZwHTL9hcU?BRQ{j z#%Z1tAV3Kky!v`2P)31p{Y01S&g~jE-leACJF(C)JKww~0;d5zATd0JA# zwaOhtB5UE)PT-=^7kubKcx^nypB`N`Am9qJrFOY+pzPijU&W~q>I5O&1wh^6nNIGB z5e8o`hwa%KY$0po!?Iwf#!E{gj6dqX!8`F|wre5CTJ`$Djzb1u?J!34%}(?HOzryq zJy|+dj!~+L8Vf&J)iqhdu_umYnO;8J9?KzfrapZ0+&D#LACit6SW72-i`@bh<sT`DRnN8G0$^ z6=VaftYeSAEyaCkdMu^#xH=`gnNHbhZ;at@ik*Rvj|TDny(#qkd)V|>oyBj=s6aPM zfCETpMmme>0TB4HsJmWHF8%T)6x5902Yn2U(C&RkP=H$;|M{~I#OfO;1VFNl|7337X(Vi3htnqxQi zT%3S&me$sPoQ+nfiIYDBlcKz4m&d6;i@Sb575QBLp`sFxc5bym71wLEJk|C3j=F)-*_TQk#%^nU~Q5+dY6=i=f5 zmGA}J@CTlr^;N=gWBY>CW#nCle73*k8GO2P{-RdQc3s(U=Ld7DTw6bZzlUOjB{2YU zl9%V5WI^n>$JZU_y)P`!)Z@Ca^?t3R{QG?XGMDFvS@8hN`OU99lQ@DRN7*nQKcZ+k z+>IGUPwyxNa21|4>$ZHYlNE{<^P;Bo9g7q!7VTLFBokE!+`FmPLmp;$n1;UN2PxH z+S@;HOVUrV4$IfCm0m>O!o$ax4iW}!-+dn+%7vM+vM*l>rOH?Fpe=`6@9P)4&R#Uy zrwm0{(5--)rJ{)BF@&L=KsJOCPvG~t1uOxLTd-6qCTu{=@UT&&)r zRN8sQ>Z4PcQ*TX}!j*ZGDEd-s-*9@d=jLjzv~~>d?k0%j*K0I{AE4Q}Q&?3s03AVu zLZW9=b-AMW%C=+5dvw%~ua*uAgQ^MLvh~2{zrawBw_8>3|I+wO=aIkv1ppd0 zFNbTRH*egy(I?70C+(+h`BGq9M1(Fx1eh-0LIUA&Il0eY%ee~&+_z6mVpldY-qEA3 z(DE=ZjT525_68LPhrFyWlZ@-Oy;<_Uka>COgJz#P!VKRj0ur6WhYvp+e9dqh(uc2O zZfl}j=qs~=3V!AEdvP+Q-Ulk|I-M@Sw~gax(oqnI)^`uh3pu|i7nKe2^5R0CmN-Sf z|M+%ce=V0K-+}^uZ`w1$H~}}i?(gM+HY@O(slI+}p_UN1WUzRxF2@cZrr=K#6dSCd zis*ji54p!4yP44|(D#UgaRg3+6mT(r6kJn=anm-ijgTjQ#PP@+Mp7W^Uodw36Yq9` zE;49>_`nN+N%oMfr+2)nKnopQI69Bt@`E=JEqIm6Jxf&;n*F*4D?M>!d!ZB60aw{N z4OwYfWhH@b3n!-09|D&I9El5PjUU>*X)XA;*@Xow)cwdZC6Q=_9k8$iuSK~Qt5eyt z>Hg{*p+o*+AZ%GKXAsE~D(vHs5R=-xh%KR7PW|V0`4l-MNY=#bODErojvpg+K^Kpj z0qa{O(6iukAZ^;C9`@|nJ>hW7+h5TzpcTs)=B)6QI0BwnDopTzuLMRaSKH9OY80%g z7;Y-Jzx5TZr;nT!7G4dB!#ijzU>pXz_OZOYR^8Sn-Mmu}Dn-j1H{O<%+=Y_cVzN(~ zqOj~kGyC+!L``3@Vck)F0fC3TT>}Gg+$-ycy~R{j_o4KML+FDgw-jhVzVrTnQ%R%@ zEW@i9-n`5|YxDg-)r0TGxc&mo~I88SfjN^T|f)8dM1p6iE0y2 z@eJLeiUq_+!OvD>Bn{)q12AE}C>9b8OB+dNk`6nsdu)l#<*D;N(yZ(|m~OF=p**N) zB0g_ERD$D@eABFg$lS9S+47LI<9tA>p2`Y-gdA!gkHwz|wm-^8XK55NeB{IBUK!LJ zgq{sd{k7S?H0H!U_q|sl1nrI{hl0_Mbm1%hG$Q;~o(*wW6L;+KCYO%#j$k zF!@U3sbTVf29415I|ngLpFuW2=K?{7NWR7-S2)C-Y%mfCF3;*feI#enhUoyjJ-%2- z$CPXLMH!jKZH(ycaQP%`z=T751wQtb?f1)2TU6lgO#59?&dtd&XcGw1!J#@2F=MXH z@Scw!Ki<24KUJ@i8Ye{3B;}0Ql{{NxuDZ3xv#ECF-e4H&nM$08R)wv_WX_30N z_&D=s#nE`_!v2i3T7h@3KQB$-KoHrgPN^rup+jpjFDHMe`P>kueNkEI&{jS=t5?fW;*{$JG0ye z!0+&xGl7uf_W-28x({(l$w*)hCo~K8u_X&B%%8d+$AcM!;of^L)7ED~%d>a0JDr15 zismI2N?b7R02&cRxV_5EjE3d`HpBa9!40#Cunss493+n)KYoMC!UJ3cc`*WH5h04# zZr-!cs|y_!A%;AuxjfJ%LTa@0?wM3xG*NP3ZAlLZ;RKEZ4p4~8Vh_C@eLgmZOupMC zD~Hw|k|7?a`3wYkO{TF5kvxHvla|ohZxG{9h~o)O{^z1zmyu?6^x7A~1f48y+|cfN zdw+4NP8M@@+t<7Z&tIurL`A@f%?!*BW_Xhufz*{t12?>$O z4OIlspV`7CVGbI}p3zb7!a@O%b88rX27mvjj^uPe$1@%}Pd;vL|GVoc6=G!$U}PK7Gb*=!w7e_j6#m3S^4=1spgrUSc9v)LJD^zs148+|Z8*Y)&v?%sVLibG_f;|=WX z+2-{41#nal%uP=2QI&P+x&nxiffJ?a#L=ff13`#L=%DxH2nCm+-|#X!CQ(*z`>JU9 zvf{iw*WXdOMsbWpWj(i|Uw8pm8&URRSjPoGoOt?{R(V==45!mhU-zjJL?a1A407F{ zxdoLY4T|eZyp`+Op2-?6L`kvaConm5s_ZH2)WF13`sU4FDEUP22$dkt8L7&k!=L`j zxXjz^-6c0%vjoc-!ee;{1b>=kkJ#mGv*e{sn!ltd>wZa546`%S)9aSMU;@N&s_-5n zFdISZQ%BY*)}f;BZqtL4gsgF)2VQ>!F&9hXEvlJ?#oq$yU)BxqvhsB`Pt#Tgs zoXkFyMpEKhflwW@8+`r~*Z@TdrHqtQoc6#XH7_M%BsQjzNQxrz*m~{q2a;kgUAok2 zijoY$!2#eo$kso`TvljaG@IY3`Rk@@Z7?6B$Wv#?Qr|n*DJm#f`(3#*NJ9}LKeaFL zh7^6&*zqpv`5_s3j1^}Z8yh|Mu}3tk#CyIciZeh8f!LXY!*>W<1U#y&%-{qdlCjub@h`QQgfmHCGCLha{&#EkLe4M}z!;J!O=Gi=z@WNV=f%AIz3 zU~`p8d&ytBcleoIHgR{UKRGRS)=UZ=X42*^$@`i0ZghiyClc6x4%Y_{ zHW0xEfXadP+;s7_jgy&%b*`uYaE58Kg}(e1D)b{g+~=!O;x0-R#-6sVj#txERox)h zefPHO4?6ISaF4x#@)ezxlA&HEbA@2=`WeZvgl2QhYX59C8MWq1V~wc3+c^ZJdWcpq z4lgrVmA^D9T3NpJkj_QgB_=b|CbdW!@&7%?-;GHY8H&;6$4>w45yA$aU#x-^qhD)f zelPT5bG53{n$=C{}H zlh`$ERW0Bq5v>ik4p^_sC{w84lbx&tfj2}ekS;a{*##K2b81GOcYG<7%LV{`(Uvxp z8Z$cS5-bV8#rVVoDXY7V$-KfCn3kTdmSe$S+M0ps_#;vex=5Y=@C>PB#ebxmdH@7a zFcGYfcz{2(W*Nk)UAnXf*_GMGLB|qlb90W67yoUT7BCT^ol0$WykAcHf0ndNo9PU( ze+MOtfFgk8)RZ-4eR4RC`+Mk_d-+Vug86#APr*AjoLleM#er04wDUbzq`NSDz%G-lpdrU z6pX(WAfo4VSCf z%+2vX=^e-avOP+rn6<0V7z_d;o&d6oa0Yw>ND6fkyTg>hDGrXUX6lVg1*CE^i_E+V zx@F)r6Y@XsAgQW0d|&jTmeWJZIEE!XK0-2H)La?E^dqu0u}KRipT0kh8;PQi;Td%M zg;8x6ezq5-)W)6!Vwpd6c`T=sLf^A1yTNS}$vRXOxQo~3rv`Ox7wPZ+BNlZV7Xo$$ z%3V_1DBsHzkuXZrA3Xu1FuP&xVT?F{t_vng!McB@xQ&9z4kZV&nizm!{Dmo(=2JDM z7WC0%Ac5>q07f)q^qz6?0mF`4S-ZY*mr;^));tF zkO2_!cwJZbv9Is@^lCv#;9F>Iqae5q;kVz(<|xI5)i@uB=)NIwFVJxv1pB%=6>C|6 zX(|fBgAf+~^!6|9?X!r-02o5iHALWoF(|eSknruqXUUJ*-`p3|7++g&M^fP_BRr*B zKSvkPu-k=`|K+eVE;+=fp z;jx;qT9k#wq^vOt99vl65d05q$4#^T_Ej&2>r5x0P)b<6o8GzbaY6oEQfGQ-_!S3^ zgbOYm3pWmc`!|?uqsqP;RngCq#`=~`XXz%WcGPCl*dwOQf7nJ?!-1uWT!FUXC=1J0 z+!~-HqF?fCwwkH@E6nS0C-v|z@431T74&0r(uUC%F1GW0k$&;^GZ=WGa$HLRfD~pM zVzta@8yb?WiT*#txd`Gsn&rFd`-mP%1W7U)#wqbY^{mK9ofcaD%{QS3$E*>9DVP9a zxU?VWqF@|QJzBORe=}fNkIegvO!awvG2Atu!$nr1DL9#aoAcDEQ^e)iEDk{iaMq{m zFE>13ntmIS;g=wSQ4biXD#R= z=C1DW^Joj{WMN?5B$<)@UoNNKc!hX4oZf;;Lf8iuMe04VePqZyJa#XR4r%Iwqa&L9 zsVOnL_Y!dK3J)bwhNVqL#K2L>UL#$ z`m0xuu=j^_w18ZU=e_-ebqh`!zfAG5GdMRgF~&mcHCJ@Y`~VA$ed>}0t)dFcy04b8 zts$4Q*W3m*myUl2fs!L2 z6Z2{{V|DlA%)Tv4DaqI9p5Emj7lboRws~0;TSm-1c@~fHoxu0p+)dxavJ~Q@Uc7kG zoCd^V91kK;Icxjw76?ZSaGw#!JwReJt$Bb~zu+wXsC)&f58F%}=X(7$@~qu4h)*C7 z!QQ?T-lcgtdn^*X(O-2SbokOs4MXd%gOQ$FnJ?WMM)Nv?vIJbK32zg$aM6m93VaJl z`Xgcb+H)rlJy6&n9|-i<{m=#P7r>QmZS_!63KmB5doL9My+qpp?a_}`g29HE?JlHo0EZG9c zSLiUtSrXCjwbx{@3hi#lGF8WFL^@t`D~zh{=%C0?Kt5jCfkL|p=r~ew3Ar)#aq=ze znNA!%a>&cit01mNRWg5sJr;dOPoowE71f?oy*;9zwrYu6RYNph>JN!q04>`cWp*u# zBZs!$M)sNYoVcL;=NdsW7Rt{7(1QXcsox?x++FB5(+0a*720C7D%SuK@alg%!D27N z_(}P(*3bBD5Xi$?_QzLjfz^L~dzIt-U@WevpW!h%Y3y*@!AD` zpfeTfPj|n@fxf+9{%-8IQ|m{wj*qWoOHkGAh$zhmZjXNv9Qy9PkO3yCXTQ|YF~l!* zm*I<3W#7bbUYQ%ZwW7H`n&r7_mmjeB!RESg9VOi-EJnfNyr`<-)Ob#J&Fj@56+O zj*jQAUP0u>P~Hqlq_~t+6lT_f@wb}+IgI!EYu$h`e0jphMGg{Bh5ls33-PL*KmQfe z-8=VP$IqLHR)+HJy>)9ryP`b5Xl1s?hav-#-1^>ZqXrLD&T;e><&ql8pPJSeM>bg< zm@r-DtiH+BFIB!)bC_e_?PpV*%TI4^y8Zv6jrqSRVEo#pQF2$m*4FZE`lPK^zc@Fk z_Uh`nx%G!W9b96AAd>q^bys<7r1Q&c>p=yG;zTf_LRVeZ(2yxL>ED0vtp*qYzir{I z3$Vf(0tdnfQ2=!vypF1uDPWKc!Mhh#eHLsDU zGcp#mb|tH&$7H2D0m}hc+WqYtgrV!na6Rq&jD>69#%JT0(AE?yX?1B%%I88@3^l7 zel7rJ0A^o_gtqh|3jVRT4vwoPFzlZS{L_bL8h-^n4dq7-|FzoNzyI0?Og`7RfimIxk zhYz1$IL68Oj4b;={v-Zbm|7JN3XfJ1%MWYSp9J~E0E zHmk$F{{Xl@R4_icYm&F83aEBgRt^*D41Gsy4qeE^@Hsf0N-)l&kZPFEmHtRljNJy9 z9qtgCFGBLUjtgOTKh0V*xx{Gv>ChputV%B#mKECK%;O!tIrb@ZcDFlOL%KeU9aH1c zV-`wjk(o_m!zuAdt>}wjmJ5gK$zYjNaN+0^x|JDz80&h5*V5Mw#-Bd%a7*^MCNbyv zMGvb57Q8-kFOC_ku+m7Nn{fE+nwlMF$2fRJs(n4ZzSQ{Yqc?8Ow@cC8cAM>{;B><( zoobthf+`%@-U73p@`wJ*PPq2uzXRoeEi5_x(@ppzL~bDcMy~`RVTqqgO3KqV7t2$! zC@ruYfDoO4?FDDQ-qmiSxoij05-1e$$+4(sk@W(tHkrMCr^4n zyuHKCn=oQ2e?O~3CXoo>+d$x-qe{Uf0!;G|k_SGRfb}@xtJ>*L?AWuX6cgM{lr^%r zNoZ;6>6-#Ar)g!9`7JQI6jE+?J{0Rp&{I~e&7%SVOPWC}NJ5gNh z`$O$+j^_3g4yOzwTE-e_9PYCqUIGIHF{$r>n1PhEe{4~P#Wy;8#A6h$4hncuQ&e5l zF@!x#Zu)E^BPDQ>vi!q`t2mAX8blun%KMGW%7WJSyQ4omO^F9!Z{$>ijnz*MNWx$- zff?lu#Pp)bqg&}7!VpTB-@66>B?ELvv$nK!D)_3U=mAo3g z?yE5`FMsNtD%N*qWIS=WZ(I;~nn(3Mq00r0{%`&kPllaNLKiVSp+b$E4CMhL#eWW?Hxvk9#`w&Pv;!f_{e}(uU zsr4hxM`YxIdXl{fP^z$@-s8s7JA~bH&U;=G$YCa-xIWK%Q2R$ZuhmdMQGXLPz0=eV z;vPbW0;z$4KaGPea^b(*F!d{O828-(54OBGDN6{048uB!BsgVtaU4)oo`Oh$URmn0 zT~Fy&NE67;Bth57a!${-@T`;<8yA^yJR`WW^6e6sB5$j$CF^7eOP0nz&_HJ z?L(Xqt5;jt*c@SG{4hYFHetcZYT~tz{SH~u2m>?3`!X=#0_$wG4oYDT_#ckPf61kk zO;Cr#@mQxY9l*8+{XOZH!VNsa^QWO~7nbF~N zqgJ)DW&PPVpLH9)Z2n#~Pr(h?Kax3L>#=AEZ@>aPy5y(nMW zD$=`OUDv=oFevtA521;bu0tRjbFIX+)kO#dF4{p-6e)nTc>zqr<$#hQJ^S~uE8wl) zI0kbWue%M79AN9IeHZY>5u_zeO)3lb^%N&Lj}(!fA5UbzvpN2dkp=< z`0Ql#TYTM_zMpmKfA_2>LJ~g>Jtywt*MWf#z&k345lc$y#OV?x`*)|I6#%g2Rcbus z44;-C4hWX7dBYaFGUKwMKNWpiN2*fsUjiQwkB1Kz;= z9T$iLSly~Gfh7&cn3+9bao@kx?9M2FW=l#SOmz76nd%{_WS#lv?8`#8X6W`R2MSp% zT3wZ+TDN=E`n7M*UfX@#P+6g3=W$WV$;C-7*Zr2L7ai?OTAR@AOsLgOk1OEqo1=F>ccgn`VVieggo6Wd^Zu7=VUDw8OAXQ(H!IrD8yL2y3iC%kg}uS^;`B$s;G7^b?O#ZrQkSrO=^aJmyQPPFV!wrcRd?0SegV z)-S8U|LPq+Ei1xb1{C=hkzRqAN7D?>?Qa@1q4$z{|KIsQU&?eUQ#&ND8>VURK z?rCwI2g!N!Cg!<$c{{17-hqTl?lWhpg!Ge>dMg0oG$wPL^tUxrtZ6mHN7RHKt1ivV zfhVg#Cn-sMrQcC~`2YuZ znd+BYV})+$b!7$TqH0u(lim|ATX}i8>Z_}(h)P*F<)rf0ss zsqkHvS5$a;XUC41#Q&}rHkF2S-O$l(hgJr-$f?zhmOeKyL(v+aE14dsN~^cl6=m;=a85pbqrW@abBXhtR_ zd0wD*svkJ6kwwjG%{V$bO19i!n~Hk7%CBTOPkhP9l9fPW!BZM|c_$0LP{LLW3^^km zotJ52oR785^JOp%eaYwc;s3D#F$}DSh?G&t-vcTnyYV1!4{6{WDRgaY74h~=&N6JN z>mc6?V-}GXj&ed$B`G80-IYuV!+Aw{r#q<`8TI3Gc2^*tF9GVivOH4^-l%b)IjB_M z(8bu2h=Swhg#(>kuu%oRSQh8NPk}%Sx6{$d096Cj8lI8y3=0t~p*+DCkj8CJj=45w zQ$vr%RqV)PXR!2d_wL=Ofj!``J~tZ$6KT3f#J;NUz|S*-obgY^OFwCV?xyioLK1-^ zd;a3Z2d2kq(1ar6$l_Otii)1l$oU*&ipHo1av{v-^#k{(yD#B>B2ISE(QQ+@G6N(s&8lAp z-&74c6mZRGJ0G?sq$>$RSr42Z4Eu+VAG5VI+!Oi6S# z*nM&ga|4dhA27({hJqbu26Tve*ScFD#~=EtKi03`1y(kKkV$-8ytksgOjy0{aO#iO1sW?Jo=J z4@>IDuB*GaomPhEjx5YVQ$n@~bh{tJ8$8&Etdx!I^U%akO%Zj(q{r`=^zicH3Q1?m ziV6%0+VmRji_r}u;Ri<%8%-sOVa_O0kPJ zF8uB8>5r{}bBF{rik0DrH90qPS4afya0tikrw1YfLbtouo!no}Q z6cKx^H)9LIdjPzHATbV!d0N~w$&{{uydTDY3$Sp153ETsF1XA!0Z#I6EQWMvZv4Tv z{NyEiq&~?~tNYulWcohci=)AA6EgFl7w_)x*ROlHkW_iRCC&DNIiNet(Vt&l;X7^K zAq9ecc%=QF>4+M)^3z=2TSEay5Q2L6*Ce`Ocfs&~m5UBkz22vQRj)(sI>eEcKgF;j zgKAQH5yBMum_Yy)0KiV0;w=Z)Ks%GyLl9Z4DEp_>4Q&uVm$2D2;1f$qr0_FnesR=E zsR7D}VWMUzYv_b;^%_e4vq&j}?YSJ30#lDFhi@dM{tj@?-C0v%--cg$^;2KeQr*xW zGhniGDSjjLBdb2yMcvyT!{qT6_huiTZBnF2{0@CDngr@2$P(V$A{)^a;Qc=yHT_|t z%5HftUQkegfDrTC_eN-Wf)d=he4_R6f`f_=&DGiQE=Nper+W>VvgVV<&t z<=#_8oxQ#CJz?=MSz#(I1!N#rc1cNzc$9HKFceKwgHbKCc%VhGIXU}oqXii!urVZaqYwu2K9o*5^Mz8kUGhPnDKO`6y}c7}H?u+)aXC`Q z91<>!%n-Bc;cf8KRM&Rv7qAYGrF1yuKc1NO-{vjWtlbbN#G}inHGiy9($J6zh5uBp zDS<}eBIlA(j8>4peA!5K>Z@l@ZdKKOm+A&3oe{fsjTv3D^4t$0f$aLlWfHea*{O%} z{Qdp07v$q329W{Kvrq^p_&W~id{W|Zb4}2RL5AIdC1LqvMUsw=1@E17pMO5XRcf2G zJz~PLbRjmqv(s#}u4ziucLUuIN~N`>+Ef6a4Ib;cj#XVO#`7$H#jjamsQt!+9$*Znrok zNkn%;e;S{E_1^Yrv6bygPCq6l;+x*&1EpfuucFPf8yUVW+k66i<#XXJJLly&eFAUe77z7GX4UT96JdYD(wahegC#OUsSmvm8U`d+T3VB4 zKVM%_^sNxdy?{IkOnKcaX?ZGQpuzsOi*SEkl$GT+Y0I-YjuZ%eO1k#@O~KuARQJhS zny~~FHNdO3&VZBA;|}Ts5(WZme^zvK^!{GuC6;x5-Q2n`Wbm&nPP)46rJ^D)IU=Hj z*R#9g`f8n#o924xS2nzc3`L&m{>oy%5XE8_rOTh}>^0q7cDD1?4Y2XkIQu<*d=9e` zV9zbIJdaTHTQaa!SJyD!Sv4UxrdgqsJ3Y2&Gy1-&KIr8bJ%o{XZYN|PXZE-w7P%^+ zmGlWno$E#AapmZ_X}ni(Hb{-ZayMwuVYX(18cay2^HuS(?2Q{~hh$P)T7k5~o{y@E z(Ul}&h#uFmsS7ribHJ7HXc-+6cBgVml+@MLnaqQ0HR&u`^5gM8q4pLw)SMn~zFBx5 zyy(83+FK2)3%(z;V5`TWw|K$036-70o(1Fo#N_0m!-wB777g{ML-H3nnLBq}Kl_U< z4LSq@mi?r1GkA+tMz<>HoxYG|dFLyU!qAH#2OPuB0U{8C6>lAOsv)j?<{7@#;KXuWF;HJ=zZA-vwwV&%moeK^k)K8tboO6RgsuOWl2at`Ur`teqWF2tytH z!RHg8qiSMh=`n<&Xp^LS{Y#2wHA`iDy|v)^dZ9p$r?-*}Lda?;nb=62Kj@6fa+0lE zld~76SUsMkSa8vAy;5ct(_n=0;~KuIt<&1`Xv+-}q^3*46H+n|MC6`=I=re?T%i=! zp}_m|XP8uS>QaT)@>>kC#?8lASt-55N-$TC6kpwD0U!;ZF*^|Hv!#jU;K4i4YtRV1 z)_rMioGj~Tbtd?r(=~>m_4W@WH*{HGus?Y0nA>E3b!6S(7W0p})`L4a^gjI()&tKV z*d4&${ahnt<)Ez4(s_X8;XCl0VG}!q4Olo`7TBN?tb^gB3o!3NcJ{{r`)*8hi;4vC zhHgu~Bnjm>5IT+#uuXTjWP7j93v+`896EOK*1s8W#xy1{}9U#ja0PA2q*bBibe~rSRf`x`S^nOYToSf`|h$dtOeidX5^ChO-)VX zR+7`$VW~!eoVW_W?53+}-pZFQ-2k$VaI7r7xj1Wy-W;&g*~A9Ic?Gt!!U&2Gl~YjA zKFw0I2wMM}pAh^%*4CE0GXHbWZ!(30}uux&_fk! z`!bf7E9-(Jn?6`SP2C2`9fEetWF6V#37HC3)a7kU(O%-iWP-&Ym}XSlt2H_sFNftg zoEa9^Qhm@TP6`K)2HHMi2O-@wRtANKKTdU7aV9!25KD%b1(8Ymz{8V8lCC;hKr|qG z{}-Y`lxn}h4A}Kjv9B>$ClF^1a#DQ#N7(XQv^;N2K|l%INwOXlxGKm*+S|C#7M)Hr z6R+1$wmQ@Z6W3u+R4AakBP*$HJAXwvI=;2?+ybBO5~ z+uPZ1Rfe`eQ~i0+8Zev%OvWWNYySpFh}H_%ftMzdxy{4F2T8u7;0a6th%w8D1N{foR2MV+Xj53}5w;dFb zteUAkxAzKfXM?7rCSg%Cyq+Z6qr@T=$GNZZPK@pC#AS%^(G zbb#?7h}myY6rfnP9$hH$a4@YDr^w^KRz5H zD=g{heL(&}px28}zTImnXw{S9S87gUl$00$%61g#=rF!F8vAkJ4n4iSy6gh`t9MtG zwhQzmrWAF9Y`>1d3M_1SOomCIVk#&7L2B<$fgc4f{fg2JH94xoZpGMaP83n9h0j8ie`C7X>pNw-$l^C(Yfz!KU8dQaNpEI=HKI@!a~s&6{iZKZ?M( z?6w(8^U&E^57ypC{o2QLgoXhLou&gbFuv{lq>}UX7eOC*w{KL)7DP<$Bwrx6*BhgWqpdT z?=J|AI}^z98#F?{_eMd8_H6Pcn(xNu#fc7dTWkCiI{Y2`0`(EGwbmYcsae&C+2}!xG|4rbn=p;RsQ9oL8w#*CfmAu5;$`SNQ7__a z{3GUney>TJP=wMlyx=zSt?{y&xjx>!&^4DXffslW@g|WK4f?y&Cp1(Ny{miX0|Q_9P z)6}E`Yy#|Njk2yaOmDWM?c#uahyn4`shydZO)b@J{9a~9&&$(fHHQ6N3j_Zf8>2DB z`r;m#>AHcR6ZIbkIt+5FUPGp3X=kU857z4xK0>R5#+9thg(MEn`U)T+jK`1n0Jg*y z%|LKHeiodi|VWgC>}m ze4EypD;7X60O)| zY_iIEWua41UjB_FYvDW6a#=ZeiKFi9J9FbO00`n{?E-HLKKvQH^4L~+b)q?MbeH5m zBZsFQRFXW^T6`^pz)ifqapR^`4FHIaY? zD0!g)A{!=g>M4N$(B1sH3JcN2F;UnLgu89=l37{?B+(Zi`#b_a1!M)RAPq%+kNKB8 zE>fc*HY0AHqcf>~KjB+KbDMOnOAoI+gm#I`(Y>PmpB)w?&~OQahO;Ns7?Ag}LVGV3 zS@Q_`P~?ha6rkGaEF~eMlE!((;Q5odV?4WHi9oA}#E6-P0|-RqceGt%@>Eo!U;*xc zTLsubq2zR*^U$HY*Z@U(wFT(_wl_Os4<=y$cMr70=aMh&!bT6IXbFh$P@VVqq-OG$ zKqilEuMk$e2U^qr{MO4$6EOP+06^*i;{uIM%d2a58*YM?0ByXE!mh*q7}jZ=3U&rf zP64p%Xtv$IV|VE2CjFd<)jQy{>E+}qC|2&p$-=S7rElvbZ6WxhY~wN!&w9T+B~$sI z*{3!ah&g%t3UQX!VsIp)48rFF@S=b{lhA;o@{A`>X7Pm${T**x(D5RO)iRBC1Cu%@ zPgSz`;CIAX@c$YNeurCxWokVocVwKCpvJfr*;rjY;~vd=;(T4qK!QufD_1PYUlX9Ep%kXX?0aozv>+}2 z%|{@kCy-+-kh0*#7-C_S=L2*JBhLGQ$!8AJS7b+`88;CK9+a&2wrH^sE;Dr|4`CKF zce1G#6RZ_nJdkM)v7KU|_CWDXn3oP5@V-LIC78-8 zlzohg84iNXzMJsf0HVgn#r=t%2)p)?73*FI-immN0U`@eFMl@?Y*jKP#e<8FvWZ~GUODsW~^cMwG6l=$}3#-~CqV31m+gv4E%j4XN-3bfOz-2Dpay zm>3$ym;B(v;$4y=cDE<4mqgf(sWb1W!VAO?|41OHg5$^ zU9>+?dUpJn#EUkNm%lqJEP(t=s^h`+aVELW4kh^b$QBmuxDI_2CJZ;g%w&}>df6e) zrDxBmzPgyxru94GNR~WgExHKKO6tW12I_j$q60z2@E;j03vcK@Y^pRI<;8q&dv?O0 zYUba0Vfn+Xx6P@dLsEtVT#*raQ^x#YE=j(JT2=>o1n+GX%xb~RK$)+!QPa{oR)~zC zetoh6eB}qafQGskJ6w+N@azL>19`=@3@xy1n1)JW2*zcmgAge>EhJcHQp<(i246SrOdRe?=bk{Fb2q`^yQax3BErJ+pic$nweH0bq zmD+(;!4McwMt92-+h|HgiW(|9Yq?UY6a;o0{PRzWP#L?5Fjr@bgw%J)yEK@mgypFq z(Oyef1CI!V1x1eY67O&PsqFL10Dk+lu36Ti**hT9!!IQg6h~0-Ie2-@?OPXyS3WpJ zT!U}L4O1FK7PuIgr9-g;XH5VLy>ah;?3BIeg35>IM;#r$@-{wxYR#H8Em%g+VtG<6 z?H=s*7U;{zF{aQ+KM#1Qk9!K<1(@+)3EbU4#)_Snq%LI2RC$Li~jvAp{`cf=@Zai~7c4CA^|XL!;y&S9`Pf)Qv7-bEk*(rk_9 zs{R*YJ^-IjFP=a(#kh(4vL8bLZf2Sx+Pv}_goWA2CU-+CCBmV&N@;U-RE-NR!DV$kkvCfk7^NQo{e zpbeGLA4n2bzA%X&>jP{9t_x#_(K9V%r(-f9SAk$o+pC3i00)@`L9}IMv?kImyrd9o zK1-@ibNjC5J|{iabmjZ^*6BvzT?w6n(dKDb7$7noFg+9mo`qZqcMgXSXb8Cq94)Wf zW^4`v|A56$4Ks$Ek`UUso_Vs)#}fzS zxlPm-?17WyH(Cct&?Gc63|aG{fPln2p`Ml0C6n{GE@fg1wyL7p!X`!v#;uiC-ndu$C4XWWi?Jf{W7X{Ac`u>?vIwMG7+ftNL18Efzt)D_G>0?q z?&%o?6n|>;OEB5$0E^MSpZ2}JDiHMI2D?JK3zGi3_xr|0$Yn>WBH2F|GlkC|OPF?P z;?`p^&3<1QR*cE=!8Q3>+QAa{vXQT1T5!vZTr5=V;W2;g(9wDjCusv(7XS;{GuK)i$k6}OdeLv>* zzH#Ns9ul#{nkHICJxQwK5W!T?a`^ByB?V0 zeE3uEPBvot22Xb0^?_v7-@wlRGa7#$jWZ@hCP0aqjjIG)EXqZgkJwz^KRD|IZaQ7M zc5O9qX+H)UZ4C|YKVRPWxR7^)O0SIjb!RKN9&$h+1v_$J83hl{t6A5^)RwE{e1Cc2sf2qjg}68FA^Zsv1@HIhvyV5!mWNEIPo&q- zUm8|M=`L@Bw~TUCCfTH!n8d^@Pt(ixhsUOQ9kz9li_fpnXu5Av&~-IHIV9Zv*Y{~+ z8Ql1nWCej6T)*!>azI%#ug{<~rwQwOd$i^S@>cu&Y$#F-xYrl6v$M;5okdq+mVdLg z^T7;$Xi2A^UoVAd_EpSTtSK%xi|L~AOsbb$bTGmH-uC4^7c<^BJNFEzODTK6uYi(U zx15$7_WMyVF1l~;ZwbTCK;oni_oyjb+Ae}9;>C9NRfzTdr#p~%fqckXKyF7w$LK|w zQ2t}n z!xfRv$sOfuE@SF(@$%(0TtDIrtXadtcQii`IWP0BP&>4T35YVgrchd*Vu1o8@#3ac z$PM~-fj?yh8R~LK4Avlfu^R`j8yT;^WW#|MKb{0AVu zq1=JBflX@yTGv#!8rq^oizCb*W53KPnpC*mic1kPLR0#%>U(iCZ1M1R&jw`4tuU)d z+wGH)a5OHiHok96;UJu!hOz3SI%mrk%yInuj2Olb&MUn6eFh*DrR zqt9@9BlJ|=wcTbd4qUqy+Pa2#bKncf_{8PFQ!UONFrs+eu#8Hceac7SAix(^ygU7Q zH1(Ie;U9`y1@OD&ocH$+XwrT+YuPe3X9jxC((b>Y1lH_H1M3u(q7w1|0g0B@s(EUh z<+zxb%;$zl=2pAozqYdtari-JlHom_Rs!^*13{Kp0ZSsAc4EWbQ;Yp)x*1NmYi6M5 zX-D>L6Q|EIYV)6H z2Qtg#XIG+}H)@+Op=4%35}m$i`-s?_L#SPOoFQhh?Yeed&u@2nY#$Zx6Z6k8u(r8> zR;6(4)IW(Tx2$p={5&wOX?3Uk-l9P}OPgkEfVz9K>&1NSINe(seQga!-cY*r&`Wv; zE2W*0Z@QL+LPueOp#Wd$?Ru%cFu>K%+$_@mS0nBH&?m|7kpjTwB~{bW&SOR$&PzD! zjMd6DR_x5K%w6nOLbqJHmq8#^Wnkl0Lt$o!2Y0}WD9lQLKHi`G)dTRp#W#6xP(Ebw z3a=}#({9Q~4jxCdUGLsU4ciUa&>LRFIGYp2=s|ZA37$S3_jzHogb=|HD^$Y0e|_Fv zb%UWG%1SZy0ZZxyNDN&uj<9Oh^g#}KfUc2W{^lEcDymxWEotcYLdT%c85!tX+angXBdnY*A=r8ZF z`p}x*MT1^c>QPUOKfS0I>^;H-X1ZoWhpsAF=`-BAY=7#L4dv#Vkza0`HVxMs(~5{O zLrwZOZ2G7YjlbL(hSg^;))twsbq^ni{n+Pw=luQ8{w(WC9Z4U(fwma_wV`7rR2fm(YiL{Y&g_M3qcfU$Jt!geY~ik$(zq%<@;grXYL%Ni zH@vS7c?EO-k8A{}g3F>_b}TnvGb8$WJB(E>P5}O9y~&5G9e2aNyX1NN@ig_RxII11 zcp3c+0d!lm3ou!FvWaX@S@2i5=^rswitW^plxj%3te8x-E%G%OmO>Od<)&^pTb)r( z;PF4rnm1R4a|08S^#;-iQq8w~;b5cs4Q6K+y zZ{{=A1f~wQPl=C-Sp|OXoj+k=F9_I(_$=vCDQ;z-IeiNMp8(v#0D}Xo;jK4P785IQ4&BiO+l_nzxT0X--F9w;tfT0W5ecHwT```$OeVyZvoLHw_Oy zm$OTEko_?n8$bQUlo1tud)$UyMipF%`7k8NOt^$UXkhv=e+24&!BfY#vEF%ClOquu z4`h!tVH}``O?Rd*@PRH&{hTTF>-EaB|HGwo+2?tSvNL_o<7>!QEEweQ6FRhOcVTLg zjDjB|)b(g~9p2kwIwV0*D7bV9m&JeHEo^ckGp*wv<=6f}T_UcXZdW;|es@xmZ9^CZ^ynegVEfxKlHHNm_p~K=eDULc4!~vPb%9~GE0~N%Z!47 z%|5tgfx9RD-7pGX4XfvtNKLetyy><4=j)ENRZ8ap34bvBEWk;I$KHn{C%2fTtWHiE zz_M^FGJLZo-Et#gy(HS&9k=|m;-2(F2-h6bXSd?%oOCY+6W?xWlS_Q}Q?DF2?S}d= zH@lQPeeO0osait$L7}#z=aiasC25yE^TwyFFD+Xgo#f}y<(=}t@8?U6E8bLFt*!2j z&6)k`XMmA!-?z$$w)XraA_A3VG8STH6ZLdv`NaAoSGbGIl9 zzq5USd?*uX39Z#}_Ni;Hs3S-88AD~!*NnzIfJ)K#lKXrovh`B@2NXWzwEO1Qo3+45 z>#x%vYEq=TGCh%9UNEJ05HvOmsKKd!xV4x%&fUTr&^7SfLycMU!Gd)uhB8Osj1)>i zXJpj4q;22hc2-HAJ3kX+xTvCVrp}wwYp%CvS>&Y{2ZF)p+k5`J;X%AV6Z z{Mp#$Zq^6+%g*zF5}e!1aF%JX)(&F$Z!Su^pzjQwaoeozVMsYryD zNgXLn>3?^UeG04i_<^_Ao@Ka7K_oPL2m~))yuf}Uhfqz;xH9?pp)&}geJ@YGwyI7e z{JzXm^1RUE5EWd?`wV~kdQjJtJP&#GY+`HMt{s&{lRJ1gTpTumXuuZV<~*PAj}q)t zlx8}otiNBjI>|42cb}6#yiafs(mRhaSyFaNI++a&l-8TYmK;3wPCsKA+&l{C%tNX6 zDK8)z8y3FgfQca^Nm^9K!u)-`fKl${+0mtU#i%y@;T)JqwO0@?7)Uriyf-_dEXJ6% zL}O!(-=BIvkP}XEDjIXP4Ar=U@s!BA3U?I?j`67o6;Ft($-;NL{+E}{)&%pl?KBP- zruU~_8$-@$0)}c17P! zCr_Lh3!t#C7j0TYXu`k#J>GweQ8RWxt$8+T^x@LO>@8CO3e}riJGH zbij|Hd`Hi-qeqYAljYuM|kM+d>~<+VzobHc`|kJtm&E_;){vkU!70WXxgOOqG+A!Tnb`-b zzTOv;mdpo_d>?v>e-8Kolg}wW#lV_p|9o{8VMcoRwr%9dp&Z1{UAjEQNNHP+FI`ZN z2`8r-MD5wrR&1+f^3%1eF4bz2TSsOJ7YS0J9=b@LEiT8!MqM}l9J}-PY!mx3S*lR7 zi=s*aq8b~Zs~T#1=77gvv)*PAWQ7MN%8QYsM*ZM+&bj)~Y3Ip=fmOe~q0q=zr~VuL z>rl*>L34CHbMjB_4G0GNvph5RFVGFAK73gXHJv_#Y;ac zp*R!WfMsN#t7Yq#m-}fY?QhdOW@bvowi5sum?a)q@5l+vWq`YbA!9X8{m%fm=42{# zNVFf=-61=jK~ZD^xILN%1h+hzDn8+MlO?_fTrF613Z?{UV4S0)#Xo#(c#tgn+d#jd zc{l0od(1O#F(a6vZ)N=rEoY6vknHe%V(`wzdHQQnRFm+NUS zI`0~FXj{B-%D=dsi4^>zY?bmh>PFVQZ-K%(CwBI`N%MnsBZ&RF|i6bv1 z)WFFR;1F4~N{bfL99K8GLu9s3ooEG*nSZA5&QSD$;`<{^9x6BDd*A0T<{8aBQ_%t_ zfLp)r;ha+;khx%=7)2c%eJysNW3suA&;Gw7UH=!jQ`$Y#kNI&jg!U7~B!jdG8rrhP_x zwb}Y>k0dot?c&_LpTnHqf zz!gL0l`*epjsA9#R$8$sBBJz(1s~va>4HgfEctQ6(ASNk@`Df++QfW&yFXhZcVc!3 zTX6mY^`Yv0))sqL&mMK&w#wdA-edKN@!m7G-QRl5ZH-F$JlNZhidWBY`FpOo%}FaN z0x8v&L7&Vk3qEG=&`99h?%EyGDa7bZR^s7Zu>^GrD!t=LwrQ1z&@9Na2$@KFg}E&0 z;TH}hu->{Sd`7v}6pJOqF(LQ%Gl->Uz}Rnmh(o`9+;5K4^BQ6;%ug^486WvgHFI;8 z%fhJxZR%TVdie{f#?;#;Wi58SnL8L?#&){a-M@MW8p*#>uhU17A8-_`4&iLVyk#`0gIVsKpb$D6l-3JU)Q^BFTbiFGbi2%aO<)vHeX-U7mSxtKz& zUi#T;ZB|{orbT_TD+uoSBMJ?Co}8~CGqq)#Nk!Z06qrB@InXbU}t=FD8bSeO|@bydzr%5D5yOUB2Ix5T3Yvw6Z{B)`840 zglcNJym|Nj{U7A)#RhTS3hBLW`&VuOCfI}l5%cc{3`xNQDmE9x;A?zt3CLOJ?%gkA zQ9bdKTJ=k~H4}4Fc$ur2+6!BTOI$$o{r1RPnX)?7u8=s)1cg?p!C*v~jo;pb<#227 zVY`Jwpml-MQ8!udfsc_ErhaZwFWdm>8jkh3^|ds0kp1q68BWM^QS5tjqW(@o?KC?7 zIsHT`bKwv=IYq_}I}&_jdc$`6!+!0oeyeY*>1n*;w5|V56t5wppikL@Dpgl-D-lvw zQL`w%Uu;uZO#@vQ^MR`3>((2<)+zoH-?~?yqgvvQKipdE$rPZdU})7O_Q{~=27bMt zvvoizTev~8vzB!L{L7##MB&Z5cl%#=*zi2Q!!6kXD~o8P;FGGC)Af;S(i_|Ah7B7o zqMo;k@MfKckn!T&4u(2HiO~k{gJCZ;N;KwhqaSs0y|+(`iKBxPeyx(GnY()j!ViN? z##A8(=eohAR{$0V?%uJ}IX(RVvZT;~(UCVkUR#pcVULQRSL(@U`t?6Y2OzrDpykHs0G~q^6Zb6ydkQLeaRN$iQKJZPpIH^gu7+{m z6jst9va(3}+;HrPefM@o| z+pSt-)CiS?+uH4&!@XGv)017uol_j2Al#RUW9RA1qWSvB0;Op|@9nU2l$c-Eujf6# zmr1wdcomE92?qslPjj3g>2CLw4f*o_K7b4|`hwM*LR2IdpLSdUVMW6;U!W1DqQu6} zNQM;Y`C&Vw7`QT17e7HpM_Gsv%;rpnnC~Dltp;Kpf|3EZuEGO z-}R?yP&S^wd2o~gcI4yi`+aKFtSOtysq|WWZg%S>JYT$LZvi~~9kF+9Py5jhpANP= z>Tmd?5cHnCKdm8x?0mV)GHKv(_s&{NUZ!-5pE+l=7nKY3>9~m#Lr3ouRYqvnx9UF^ zUtqKkF;yMr@#MvG9^6eYdvjobUqtC5_klM3BD+^9Q=@&S$LAYNMgGM0lbi&jvK!@^ z8cj3KUHxMvpCqlLbc8(O+)VnjS7>2}1vZA@brkDtMs=pv)~XP?X}>o?wMQC6=h%U& zkn+OD-hSGmlmX|vrko6)} z9&CAqRC-9l7ggjr{NP5{%J3~m#s1HUo!4Jb2@-(m)Wv9|7iWVsHe1_%i4FZ9i}MI(=IFJZ>BLf1_8Ij6{4)xRMS;Sjq~zzWjMhK?P)M5jINE zw+H8L+gTo%ls=0n!k2pFlRp7WcRsp>^XW#eBZ2v8lZ?L>xJd|tK$wFtc_0D*V$7ky zb#Iv%1^Rkdq~5V}*z)-iLK`4E-s>)&rHZEJtdC9wnfLp6zFk=VrQ9{?)~b(#b=Ui) zYj4~f&ZWvZ;M`Z#w|mM)<$&}Y?rb$G`L^fyk=aE*x{R9Xv~$Y>GhRJ&8>!#FyRMJV za36&@_V~M3<0GGi|FA5Xk^Xy-C{X^B6~8mzzF+yzz%0{ubN{Ig{+(PVds-JUt-N~W zN?QO0ED|QCUx_J4L0`!AmLNGmLLw0a?q zl!y-nOyc1n7kb=tQ*(wN^e-ZsAalPA9q_KFU%=_4FS~}LF=6!o6qugY#N${xT`|?s1csH+Hz?&Ozm7*m`bx zt;(2>KR^JL&*PIc`OA}Bu+ZtpwDwR=2cE;^M!hVsyU!&xZ{?PTfd$DaBbIolij@?LQxnnC9zi2+B+i^kD1pdD;#KUG{P~LYq`Ofz#<+>UL9k{Lb}}#*6iV zWTpEcBY8oraVj>{(IjEjsYZSVQ^c3*pbqL{Da5_|sBqaz3Y z3P`egT;o^u)$Fr1@@%uDp9QgLe_ux3jC0jDuwabPmN^fJYrwK(A;y1jgYWjtQDrR@ zV8?6ZT?#NNHxMKazO&US$ksw<<&WQzXD%FZ_P>B{Df5$5% zsqzk?f%j1*1F2pW@D!q?IN;(C>yES!b4^F%`-KK6ot>{|U>(V#nBBlDs2ZN%94#Ml z3#6X^zjMyHi!a8_TP^~RFmvr8V1#pvj4>L{#PVJsEvmeipnXRp1JC$O{ePe>_wSQl zm|%c^Gw?7PzK)z&m|H2Ym4TqyWx){Fb73S*S*(z<6VC32DEhCZN2Z?RBikG?Y{@>m zf6=u!oOPYx;JrTL?%_U@81ykpr3K!?%pG=|hxcq!^Ftvf`@d^U@g~2`*tqo}62{~HRg(nIv#S1@u{YV?hS4HNiH_eoFnMJ)Ya zT;$tEAyq5uY472SOGUp{+b;;B)x~slPjqW^d*uC(6ejc&w)=hzxC;|p9VQZ{*_jU? z?nElun)en1M&;koPLDYHKW#CvY+>BBp+O082OnyM#B15?*0|n(q_dT*Z2JGi zCGKqSf9DdH4FP8c8tPzR&|Ey&7d`!#B_PtnV<^IX+W#Nq_eRFN|2%>ugVfGFdcb$? z>0N4>@;W;!OXJpqX3lplt*q)yhEHzL-=L(nj2U`O1ILKOBqhmXOziB9s`aJA@X^dc@lm*b0YoTY~YBlq|-R%3NDV1N3pOexF-YjK2 zdn!hGZ~kA0&j&wd^c(pfb56%Mb7#(ZPeRh^T2imQokoTX+TA@e z9hDa+?*;-lAo0K3=E;ty?Y8-3NfX!A)e4~=^i#SevNcKFmvWVJ#%TBrzBNRT8&ctIPt#F=-cs!i7tsg(FSnE5pVb#mXPd;lCzST7_Squ4071ZQ16T(@&r^R0UyvKLSvXp+` zGaTAf%^C5~V2#&IpJ2Ptyue@6Osz(^8T%W)V1-K$_|kRj)@^=ElS+#L?xeS#mB-%& zD0xLMJWuQhi9kJ@`(lNFg=7k>Wi~bCWTh_?z>$1* zC6qi{yon#cpTwVEDk|)h)*PI4E|gTG)4zYl{cjh`XXw;aBo)*M3PSX6ZEIUQ!=*{E z=_<+vq`P&o81HFl7`T3YqjyDToadOtj|&FHoI*{_ZVudi&6xY;>>uA-R<#jH?7DXi zvyhdUvet+AoK5pr{<-@&Eim?Bymp9&2|p{}zI}_YwQcD%TleB~85#ZCt2NjV67rEz znNEie!wtshgq<%gZW9p^!BQ>v_*Ns8?NZ+L>-H8_9lM5Cu31wDA=B<}Zf%FUy6Uu7 z8<9b2KjzI0Q`5ABBl@mXW{~$43QW0In(AC&98*&v9?r1QYYkqu<52REI6e1OYI)rk zl9dT%nmnpa7VY`TIWv}o)R4z)8s|yyR_wW}-9Kq&V&dpiagWC5oW9$q`<8JPAAbfU zo?aI^!6&=ae`EIM=$PL6`eNIa=QT3^h~@3u;hsm27;Q|x6&@d6c`AAToR{Ga6LAG> z%;xwzD2Wx}`MNWEN7oBZ)c0A|#@czwpN=tsYh&lBSi9Ljp7kZtc+#GCPTOzax$oj$ ze2D*q54H`=X;vG}eID-Le{0y|j3Zr| z|54h_-Zfh8%fh07f|*ufQMlpd5S`@4?{(RJr)tnRa$;<)JpZ$Ux6CTZ`u%35WA3ss z=rk@u(WyzbB%g%=MNsmVl51rH7in3UDikY&EQTpoi{$JL?<8SBKJ+l}8^;Jxu6!p>nbvruo>U@Vx1RFjs;dfY1!of{8#-AEh@AjTR zpLstK%B-@~OL