From 44f3080226888eb709cc6e027321647964ebe64e Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Mon, 23 Mar 2009 20:22:56 +0000 Subject: [PATCH] Fixed #10505: added support for bulk admin actions, including a globally-available "delete selected" action. See the documentation for details. This work started life as Brian Beck's "django-batchadmin." It was rewritten for inclusion in Django by Alex Gaynor, Jannis Leidel (jezdez), and Martin Mahner (bartTC). Thanks, guys! git-svn-id: http://code.djangoproject.com/svn/django/trunk@10121 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 2 + django/contrib/admin/__init__.py | 1 + django/contrib/admin/helpers.py | 18 +- .../contrib/admin/media/css/changelists.css | 44 ++++ django/contrib/admin/media/js/actions.js | 19 ++ django/contrib/admin/options.py | 194 +++++++++++++- django/contrib/admin/sites.py | 125 ++++----- .../admin/templates/admin/actions.html | 5 + .../admin/templates/admin/change_list.html | 12 +- .../admin/delete_selected_confirmation.html | 37 +++ .../contrib/admin/templatetags/admin_list.py | 9 + django/contrib/admin/util.py | 91 ++++++- django/contrib/admin/validation.py | 15 +- docs/index.txt | 2 +- .../contrib/admin/_images/article_actions.png | Bin 0 -> 35643 bytes .../admin/_images/article_actions_message.png | Bin 0 -> 21812 bytes .../{ => admin}/_images/flatfiles_admin.png | Bin .../contrib/admin/_images/user_actions.png | Bin 0 -> 25950 bytes .../{ => admin}/_images/users_changelist.png | Bin docs/ref/contrib/admin/actions.txt | 239 ++++++++++++++++++ .../contrib/{admin.txt => admin/index.txt} | 23 +- docs/ref/contrib/index.txt | 2 +- .../admin_registration/models.py | 2 +- .../fixtures/admin-views-actions.xml | 15 ++ tests/regressiontests/admin_views/models.py | 38 +++ tests/regressiontests/admin_views/tests.py | 96 +++++-- 26 files changed, 881 insertions(+), 108 deletions(-) create mode 100644 django/contrib/admin/media/js/actions.js create mode 100644 django/contrib/admin/templates/admin/actions.html create mode 100644 django/contrib/admin/templates/admin/delete_selected_confirmation.html create mode 100644 docs/ref/contrib/admin/_images/article_actions.png create mode 100644 docs/ref/contrib/admin/_images/article_actions_message.png rename docs/ref/contrib/{ => admin}/_images/flatfiles_admin.png (100%) create mode 100644 docs/ref/contrib/admin/_images/user_actions.png rename docs/ref/contrib/{ => admin}/_images/users_changelist.png (100%) create mode 100644 docs/ref/contrib/admin/actions.txt rename docs/ref/contrib/{admin.txt => admin/index.txt} (98%) create mode 100644 tests/regressiontests/admin_views/fixtures/admin-views-actions.xml diff --git a/AUTHORS b/AUTHORS index 88ad4d3e0f..f47bdb4175 100644 --- a/AUTHORS +++ b/AUTHORS @@ -56,6 +56,7 @@ answer newbie questions, and generally made Django that much better: Ned Batchelder batiste@dosimple.ch Batman + Brian Beck Shannon -jj Behrens Esdras Beleza Chris Bennett @@ -268,6 +269,7 @@ answer newbie questions, and generally made Django that much better: Daniel Lindsley Trey Long msaelices + Martin Mahner Matt McClanahan Frantisek Malina Martin Maney diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index b2eeeebb1e..f8bfde1f5b 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -1,3 +1,4 @@ +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL from django.contrib.admin.options import StackedInline, TabularInline from django.contrib.admin.sites import AdminSite, site diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index aaa2e304ce..5cb8f1f07b 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -6,6 +6,14 @@ from django.utils.safestring import mark_safe from django.utils.encoding import force_unicode from django.contrib.admin.util import flatten_fieldsets from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ + +ACTION_CHECKBOX_NAME = '_selected_action' + +class ActionForm(forms.Form): + action = forms.ChoiceField(label=_('Action:')) + +checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False) class AdminForm(object): def __init__(self, form, fieldsets, prepopulated_fields): @@ -132,11 +140,11 @@ class InlineAdminForm(AdminForm): self.original.content_type_id = ContentType.objects.get_for_model(original).pk self.show_url = original and hasattr(original, 'get_absolute_url') super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields) - + def __iter__(self): for name, options in self.fieldsets: yield InlineFieldset(self.formset, self.form, name, **options) - + def field_count(self): # tabular.html uses this function for colspan value. num_of_fields = 1 # always has at least one field @@ -149,7 +157,7 @@ class InlineAdminForm(AdminForm): def pk_field(self): return AdminField(self.form, self.formset._pk_field.name, False) - + def fk_field(self): fk = getattr(self.formset, "fk", None) if fk: @@ -169,14 +177,14 @@ class InlineFieldset(Fieldset): def __init__(self, formset, *args, **kwargs): self.formset = formset super(InlineFieldset, self).__init__(*args, **kwargs) - + def __iter__(self): fk = getattr(self.formset, "fk", None) for field in self.fields: if fk and fk.name == field: continue yield Fieldline(self.form, field) - + class AdminErrorList(forms.util.ErrorList): """ Stores all errors for the form/formsets in an add/change stage view. diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 40142f50ca..649cff7ae9 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -50,12 +50,24 @@ #changelist table thead th { white-space: nowrap; + vertical-align: middle; +} + +#changelist table thead th:first-child { + width: 1.5em; + text-align: center; } #changelist table tbody td { border-left: 1px solid #ddd; } +#changelist table tbody td:first-child { + border-left: 0; + border-right: 1px solid #ddd; + text-align: center; +} + #changelist table tfoot { color: #666; } @@ -209,3 +221,35 @@ border-color: #036; } +/* ACTIONS */ + +.filtered .actions { + margin-right: 160px !important; + border-right: 1px solid #ddd; +} + +#changelist .actions { + color: #666; + padding: 3px; + border-bottom: 1px solid #ddd; + background: #e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; +} + +#changelist .actions:last-child { + border-bottom: none; +} + +#changelist .actions select { + border: 1px solid #aaa; + margin: 0 0.5em; + padding: 1px 2px; +} + +#changelist .actions label { + font-size: 11px; + margin: 0 0.5em; +} + +#changelist #action-toggle { + display: none; +} diff --git a/django/contrib/admin/media/js/actions.js b/django/contrib/admin/media/js/actions.js new file mode 100644 index 0000000000..febb0c18c2 --- /dev/null +++ b/django/contrib/admin/media/js/actions.js @@ -0,0 +1,19 @@ +var Actions = { + init: function() { + selectAll = document.getElementById('action-toggle'); + if (selectAll) { + selectAll.style.display = 'inline'; + addEvent(selectAll, 'change', function() { + Actions.checker(this.checked); + }); + } + }, + checker: function(checked) { + actionCheckboxes = document.getElementsBySelector('tr input.action-select'); + for(var i = 0; i < actionCheckboxes.length; i++) { + actionCheckboxes[i].checked = checked; + } + } +} + +addEvent(window, 'load', Actions.init); diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 69f52aadad..3c712a55c8 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -5,9 +5,10 @@ from django.forms.models import BaseInlineFormSet from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets from django.contrib.admin import helpers -from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects +from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict from django.core.exceptions import PermissionDenied from django.db import models, transaction +from django.db.models.fields import BLANK_CHOICE_DASH from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response from django.utils.functional import update_wrapper @@ -16,7 +17,7 @@ from django.utils.safestring import mark_safe from django.utils.functional import curry from django.utils.text import capfirst, get_text_list from django.utils.translation import ugettext as _ -from django.utils.translation import ngettext +from django.utils.translation import ngettext, ugettext_lazy from django.utils.encoding import force_unicode try: set @@ -192,6 +193,12 @@ class ModelAdmin(BaseModelAdmin): delete_confirmation_template = None object_history_template = None + # Actions + actions = ['delete_selected'] + action_form = helpers.ActionForm + actions_on_top = True + actions_on_bottom = False + def __init__(self, model, admin_site): self.model = model self.opts = model._meta @@ -200,6 +207,13 @@ class ModelAdmin(BaseModelAdmin): for inline_class in self.inlines: inline_instance = inline_class(self.model, self.admin_site) self.inline_instances.append(inline_instance) + if 'action_checkbox' not in self.list_display: + self.list_display = ['action_checkbox'] + list(self.list_display) + if not self.list_display_links: + for name in self.list_display: + if name != 'action_checkbox': + self.list_display_links = [name] + break super(ModelAdmin, self).__init__() def get_urls(self): @@ -239,6 +253,8 @@ class ModelAdmin(BaseModelAdmin): from django.conf import settings js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] + if self.actions: + js.extend(['js/getElementsBySelector.js', 'js/actions.js']) if self.prepopulated_fields: js.append('js/urlify.js') if self.opts.get_ordered_objects(): @@ -390,6 +406,121 @@ class ModelAdmin(BaseModelAdmin): action_flag = DELETION ) + def action_checkbox(self, obj): + """ + A list_display column containing a checkbox widget. + """ + return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk)) + action_checkbox.short_description = mark_safe('') + action_checkbox.allow_tags = True + + def get_actions(self, request=None): + """ + Return a dictionary mapping the names of all actions for this + ModelAdmin to a tuple of (callable, name, description) for each action. + """ + actions = {} + for klass in [self.admin_site] + self.__class__.mro()[::-1]: + for action in getattr(klass, 'actions', []): + func, name, description = self.get_action(action) + actions[name] = (func, name, description) + return actions + + def get_action_choices(self, request=None, default_choices=BLANK_CHOICE_DASH): + """ + Return a list of choices for use in a form object. Each choice is a + tuple (name, description). + """ + choices = [] + default_choices + for func, name, description in self.get_actions(request).itervalues(): + choice = (name, description % model_format_dict(self.opts)) + choices.append(choice) + return choices + + def get_action(self, action): + """ + Return a given action from a parameter, which can either be a calable, + or the name of a method on the ModelAdmin. Return is a tuple of + (callable, name, description). + """ + if callable(action): + func = action + action = action.__name__ + elif hasattr(self, action): + func = getattr(self, action) + if hasattr(func, 'short_description'): + description = func.short_description + else: + description = capfirst(action.replace('_', ' ')) + return func, action, description + + def delete_selected(self, request, queryset): + """ + Default action which deletes the selected objects. + + In the first step, it displays a confirmation page whichs shows all + the deleteable objects or, if the user has no permission one of the + related childs (foreignkeys) it displays a "permission denied" message. + + In the second step delete all selected objects and display the change + list again. + """ + opts = self.model._meta + app_label = opts.app_label + + # Check that the user has delete permission for the actual model + if not self.has_delete_permission(request): + raise PermissionDenied + + # Populate deletable_objects, a data structure of all related objects that + # will also be deleted. + + # deletable_objects must be a list if we want to use '|unordered_list' in the template + deletable_objects = [] + perms_needed = set() + i = 0 + for obj in queryset: + deletable_objects.append([mark_safe(u'%s: %s' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []]) + get_deleted_objects(deletable_objects[i], perms_needed, request.user, obj, opts, 1, self.admin_site, levels_to_root=2) + i=i+1 + + # The user has already confirmed the deletion. + # Do the deletion and return a None to display the change list view again. + if request.POST.get('post'): + if perms_needed: + raise PermissionDenied + n = queryset.count() + if n: + for obj in queryset: + obj_display = force_unicode(obj) + self.log_deletion(request, obj, obj_display) + queryset.delete() + self.message_user(request, _("Successfully deleted %d %s.") % ( + n, model_ngettext(self.opts, n) + )) + # Return None to display the change list page again. + return None + + context = { + "title": _("Are you sure?"), + "object_name": force_unicode(opts.verbose_name), + "deletable_objects": deletable_objects, + 'queryset': queryset, + "perms_lacking": perms_needed, + "opts": opts, + "root_path": self.admin_site.root_path, + "app_label": app_label, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + } + + # Display the confirmation page + return render_to_response(self.delete_confirmation_template or [ + "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/delete_selected_confirmation.html" % app_label, + "admin/delete_selected_confirmation.html" + ], context, context_instance=template.RequestContext(request)) + + delete_selected.short_description = ugettext_lazy("Delete selected %(verbose_name_plural)s") def construct_change_message(self, request, form, formsets): """ @@ -529,6 +660,48 @@ class ModelAdmin(BaseModelAdmin): self.message_user(request, msg) return HttpResponseRedirect("../") + def response_action(self, request, queryset): + """ + Handle an admin action. This is called if a request is POSTed to the + changelist; it returns an HttpResponse if the action was handled, and + None otherwise. + """ + # There can be multiple action forms on the page (at the top + # and bottom of the change list, for example). Get the action + # whose button was pushed. + try: + action_index = int(request.POST.get('index', 0)) + except ValueError: + action_index = 0 + + # Construct the action form. + data = request.POST.copy() + data.pop(helpers.ACTION_CHECKBOX_NAME, None) + data.pop("index", None) + action_form = self.action_form(data, auto_id=None) + action_form.fields['action'].choices = self.get_action_choices(request) + + # If the form's valid we can handle the action. + if action_form.is_valid(): + action = action_form.cleaned_data['action'] + func, name, description = self.get_actions(request)[action] + + # Get the list of selected PKs. If nothing's selected, we can't + # perform an action on it, so bail. + selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) + if not selected: + return None + + response = func(request, queryset.filter(pk__in=selected)) + + # Actions may return an HttpResponse, which will be used as the + # response from the POST. If not, we'll be a good little HTTP + # citizen and redirect back to the changelist page. + if isinstance(response, HttpResponse): + return response + else: + return HttpResponseRedirect(".") + def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model @@ -721,6 +894,14 @@ class ModelAdmin(BaseModelAdmin): return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') + # If the request was POSTed, this might be a bulk action or a bulk edit. + # Try to look up an action first, but if this isn't an action the POST + # will fall through to the bulk edit check, below. + if request.method == 'POST': + response = self.response_action(request, queryset=cl.get_query_set()) + if response: + return response + # If we're allowing changelist editing, we need to construct a formset # for the changelist given all the fields to be edited. Then we'll # use the formset to validate/process POSTed data. @@ -764,7 +945,11 @@ class ModelAdmin(BaseModelAdmin): if formset: media = self.media + formset.media else: - media = None + media = self.media + + # Build the action form and populate it with available actions. + action_form = self.action_form(auto_id=None) + action_form.fields['action'].choices = self.get_action_choices(request) context = { 'title': cl.title, @@ -774,6 +959,9 @@ class ModelAdmin(BaseModelAdmin): 'has_add_permission': self.has_add_permission(request), 'root_path': self.admin_site.root_path, 'app_label': app_label, + 'action_form': action_form, + 'actions_on_top': self.actions_on_top, + 'actions_on_bottom': self.actions_on_bottom, } context.update(extra_context or {}) return render_to_response(self.change_list_template or [ diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 5171e71583..872e4a02c0 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -28,11 +28,11 @@ class AdminSite(object): register() method, and the root() method can then be used as a Django view function that presents a full admin interface for the collection of registered models. """ - + index_template = None login_template = None app_index_template = None - + def __init__(self, name=None): self._registry = {} # model_class class -> admin_class instance # TODO Root path is used to calculate urls under the old root() method @@ -44,17 +44,19 @@ class AdminSite(object): else: name += '_' self.name = name - + + self.actions = [] + def register(self, model_or_iterable, admin_class=None, **options): """ Registers the given model(s) with the given admin class. - + The model(s) should be Model classes, not instances. - + If an admin class isn't given, it will use ModelAdmin (the default admin options). If keyword arguments are given -- e.g., list_display -- they'll be applied as options to the admin class. - + If a model is already registered, this will raise AlreadyRegistered. """ if not admin_class: @@ -65,13 +67,13 @@ class AdminSite(object): from django.contrib.admin.validation import validate else: validate = lambda model, adminclass: None - + if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) - + # If we got **options then dynamically construct a subclass of # admin_class with those **options. if options: @@ -80,17 +82,17 @@ class AdminSite(object): # which causes issues later on. options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - + # Validate (which might be a no-op) validate(admin_class, model) - + # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self) - + def unregister(self, model_or_iterable): """ Unregisters the given model(s). - + If a model isn't already registered, this will raise NotRegistered. """ if isinstance(model_or_iterable, ModelBase): @@ -99,44 +101,49 @@ class AdminSite(object): if model not in self._registry: raise NotRegistered('The model %s is not registered' % model.__name__) del self._registry[model] - + + def add_action(self, action): + if not callable(action): + raise TypeError("You can only register callable actions through an admin site") + self.actions.append(action) + def has_permission(self, request): """ Returns True if the given HttpRequest has permission to view *at least one* page in the admin site. """ return request.user.is_authenticated() and request.user.is_staff - + def check_dependencies(self): """ Check that all things needed to run the admin have been correctly installed. - + The default implementation checks that LogEntry, ContentType and the auth context processor are installed. """ from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType - + if not LogEntry._meta.installed: raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.") if not ContentType._meta.installed: raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.") if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") - + def admin_view(self, view): """ Decorator to create an "admin view attached to this ``AdminSite``. This wraps the view and provides permission checking by calling ``self.has_permission``. - + You'll want to use this from within ``AdminSite.get_urls()``: - + class MyAdminSite(AdminSite): - + def get_urls(self): from django.conf.urls.defaults import patterns, url - + urls = super(MyAdminSite, self).get_urls() urls += patterns('', url(r'^my_view/$', self.protected_view(some_view)) @@ -148,15 +155,15 @@ class AdminSite(object): return self.login(request) return view(request, *args, **kwargs) return update_wrapper(inner, view) - + def get_urls(self): from django.conf.urls.defaults import patterns, url, include - + def wrap(view): def wrapper(*args, **kwargs): return self.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - + # Admin-site-wide views. urlpatterns = patterns('', url(r'^$', @@ -180,7 +187,7 @@ class AdminSite(object): wrap(self.app_index), name='%sadmin_app_list' % self.name), ) - + # Add in each model's views. for model, model_admin in self._registry.iteritems(): urlpatterns += patterns('', @@ -188,11 +195,11 @@ class AdminSite(object): include(model_admin.urls)) ) return urlpatterns - + def urls(self): return self.get_urls() urls = property(urls) - + def password_change(self, request): """ Handles the "change password" task -- both form display and validation. @@ -200,18 +207,18 @@ class AdminSite(object): from django.contrib.auth.views import password_change return password_change(request, post_change_redirect='%spassword_change/done/' % self.root_path) - + def password_change_done(self, request): """ Displays the "success" page after a password change. """ from django.contrib.auth.views import password_change_done return password_change_done(request) - + def i18n_javascript(self, request): """ Displays the i18n JavaScript that the Django admin requires. - + This takes into account the USE_I18N setting. If it's set to False, the generated JavaScript will be leaner and faster. """ @@ -220,23 +227,23 @@ class AdminSite(object): else: from django.views.i18n import null_javascript_catalog as javascript_catalog return javascript_catalog(request, packages='django.conf') - + def logout(self, request): """ Logs out the user for the given HttpRequest. - + This should *not* assume the user is already logged in. """ from django.contrib.auth.views import logout return logout(request) logout = never_cache(logout) - + def login(self, request): """ Displays the login form for the given HttpRequest. """ from django.contrib.auth.models import User - + # If this isn't already the login page, display it. if not request.POST.has_key(LOGIN_FORM_KEY): if request.POST: @@ -244,14 +251,14 @@ class AdminSite(object): else: message = "" return self.display_login_form(request, message) - + # Check that the user accepts cookies. if not request.session.test_cookie_worked(): message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") return self.display_login_form(request, message) else: request.session.delete_test_cookie() - + # Check the password. username = request.POST.get('username', None) password = request.POST.get('password', None) @@ -271,7 +278,7 @@ class AdminSite(object): else: message = _("Usernames cannot contain the '@' character.") return self.display_login_form(request, message) - + # The user data is correct; log in the user in and continue. else: if user.is_active and user.is_staff: @@ -280,7 +287,7 @@ class AdminSite(object): else: return self.display_login_form(request, ERROR_MESSAGE) login = never_cache(login) - + def index(self, request, extra_context=None): """ Displays the main admin index page, which lists all of the installed @@ -291,14 +298,14 @@ class AdminSite(object): for model, model_admin in self._registry.items(): app_label = model._meta.app_label has_module_perms = user.has_module_perms(app_label) - + if has_module_perms: perms = { 'add': model_admin.has_add_permission(request), 'change': model_admin.has_change_permission(request), 'delete': model_admin.has_delete_permission(request), } - + # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): @@ -316,15 +323,15 @@ class AdminSite(object): 'has_module_perms': has_module_perms, 'models': [model_dict], } - + # Sort the apps alphabetically. app_list = app_dict.values() app_list.sort(lambda x, y: cmp(x['name'], y['name'])) - + # Sort the models alphabetically within each app. for app in app_list: app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) - + context = { 'title': _('Site administration'), 'app_list': app_list, @@ -335,7 +342,7 @@ class AdminSite(object): context_instance=template.RequestContext(request) ) index = never_cache(index) - + def display_login_form(self, request, error_message='', extra_context=None): request.session.set_test_cookie() context = { @@ -348,7 +355,7 @@ class AdminSite(object): return render_to_response(self.login_template or 'admin/login.html', context, context_instance=template.RequestContext(request) ) - + def app_index(self, request, app_label, extra_context=None): user = request.user has_module_perms = user.has_module_perms(app_label) @@ -394,46 +401,46 @@ class AdminSite(object): return render_to_response(self.app_index_template or 'admin/app_index.html', context, context_instance=template.RequestContext(request) ) - + def root(self, request, url): """ DEPRECATED. This function is the old way of handling URL resolution, and is deprecated in favor of real URL resolution -- see ``get_urls()``. - + This function still exists for backwards-compatibility; it will be removed in Django 1.3. """ import warnings warnings.warn( - "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", + "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", PendingDeprecationWarning ) - + # # Again, remember that the following only exists for # backwards-compatibility. Any new URLs, changes to existing URLs, or # whatever need to be done up in get_urls(), above! # - + if request.method == 'GET' and not request.path.endswith('/'): return http.HttpResponseRedirect(request.path + '/') - + if settings.DEBUG: self.check_dependencies() - + # Figure out the admin base URL path and stash it for later use self.root_path = re.sub(re.escape(url) + '$', '', request.path) - + url = url.rstrip('/') # Trim trailing slash, if it exists. - + # The 'logout' view doesn't require that the person is logged in. if url == 'logout': return self.logout(request) - + # Check permission to continue or display login form. if not self.has_permission(request): return self.login(request) - + if url == '': return self.index(request) elif url == 'password_change': @@ -451,9 +458,9 @@ class AdminSite(object): return self.model_page(request, *url.split('/', 2)) else: return self.app_index(request, url) - + raise http.Http404('The requested admin page does not exist.') - + def model_page(self, request, app_label, model_name, rest_of_url=None): """ DEPRECATED. This is the old way of handling a model view on the admin @@ -468,7 +475,7 @@ class AdminSite(object): except KeyError: raise http.Http404("This model exists but has not been registered with the admin site.") return admin_obj(request, rest_of_url) - model_page = never_cache(model_page) + model_page = never_cache(model_page) # This global object represents the default admin site, for the common case. # You can instantiate AdminSite in your own code to create a custom admin site. diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html new file mode 100644 index 0000000000..bf4b975dfb --- /dev/null +++ b/django/contrib/admin/templates/admin/actions.html @@ -0,0 +1,5 @@ +{% load i18n %} +
+ {% for field in action_form %}{% endfor %} + +
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index dca5b80245..63254b868e 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -7,8 +7,8 @@ {% if cl.formset %} - {{ media }} {% endif %} + {{ media }} {% endblock %} {% block bodyclass %}change-list{% endblock %} @@ -63,14 +63,18 @@ {% endif %} {% endblock %} +
{% if cl.formset %} - {{ cl.formset.management_form }} {% endif %} - {% block result_list %}{% result_list cl %}{% endblock %} + {% block result_list %} + {% if actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} + {% result_list cl %} + {% if actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} + {% endblock %} {% block pagination %}{% pagination cl %}{% endblock %} - {% if cl.formset %}
{% endif %} + {% endblock %} diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html new file mode 100644 index 0000000000..a6258b94c0 --- /dev/null +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -0,0 +1,37 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +{% if perms_lacking %} +

{% blocktrans %}Deleting the {{ object_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

+
    + {% for obj in perms_lacking %} +
  • {{ obj }}
  • + {% endfor %} +
+{% else %} +

{% blocktrans %}Are you sure you want to delete the selected {{ object_name }} objects? All of the following objects and it's related items will be deleted:{% endblocktrans %}

+ {% for deleteable_object in deletable_objects %} +
    {{ deleteable_object|unordered_list }}
+ {% endfor %} +
+
+ {% for obj in queryset %} + + {% endfor %} + + + +
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 063ef0e4a1..a374bf58f7 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -325,3 +325,12 @@ search_form = register.inclusion_tag('admin/search_form.html')(search_form) def admin_list_filter(cl, spec): return {'title': spec.title(), 'choices' : list(spec.choices(cl))} admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) + +def admin_actions(context): + """ + Track the number of times the action field has been rendered on the page, + so we know which value to use. + """ + context['action_index'] = context.get('action_index', -1) + 1 + return context +admin_actions = register.inclusion_tag("admin/actions.html", takes_context=True)(admin_actions) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 4164c8ad9c..38f86e3d58 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -4,7 +4,8 @@ from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.encoding import force_unicode -from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext, ugettext as _ +from django.core.urlresolvers import reverse, NoReverseMatch def quote(s): """ @@ -60,8 +61,27 @@ def _nest_help(obj, depth, val): current = current[-1] current.append(val) -def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site): - "Helper function that recursively populates deleted_objects." +def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root): + """ + Returns the url to the admin change view for the given app_label, + module_name and primary key. + """ + try: + return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,)) + except NoReverseMatch: + return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk) + +def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4): + """ + Helper function that recursively populates deleted_objects. + + `levels_to_root` defines the number of directories (../) to reach the + admin root path. In a change_view this is 4, in a change_list view 2. + + This is for backwards compatibility since the options.delete_selected + method uses this function also from a change_list view. + This will not be used if we can reverse the URL. + """ nh = _nest_help # Bind to local variable for performance if current_depth > 16: return # Avoid recursing too deep. @@ -91,11 +111,13 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []]) else: # Display a link to the admin page. - nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % + nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (escape(capfirst(related.opts.verbose_name)), - related.opts.app_label, - related.opts.object_name.lower(), - sub_obj._get_pk_val(), + get_change_view_url(related.opts.app_label, + related.opts.object_name.lower(), + sub_obj._get_pk_val(), + admin_site, + levels_to_root), escape(sub_obj))), []]) get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) else: @@ -109,11 +131,13 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []]) else: # Display a link to the admin page. - nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % + nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (escape(capfirst(related.opts.verbose_name)), - related.opts.app_label, - related.opts.object_name.lower(), - sub_obj._get_pk_val(), + get_change_view_url(related.opts.app_label, + related.opts.object_name.lower(), + sub_obj._get_pk_val(), + admin_site, + levels_to_root), escape(sub_obj))), []]) get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) # If there were related objects, and the user doesn't have @@ -147,11 +171,52 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_ # Display a link to the admin page. nh(deleted_objects, current_depth, [ mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \ - (u' %s' % \ - (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []]) + (u' %s' % \ + (get_change_view_url(related.opts.app_label, + related.opts.object_name.lower(), + sub_obj._get_pk_val(), + admin_site, + levels_to_root), + escape(sub_obj)))), []]) # If there were related objects, and the user doesn't have # permission to change them, add the missing perm to perms_needed. if has_admin and has_related_objs: p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) if not user.has_perm(p): perms_needed.add(related.opts.verbose_name) + +def model_format_dict(obj): + """ + Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', + typically for use with string formatting. + + `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. + + """ + if isinstance(obj, (models.Model, models.base.ModelBase)): + opts = obj._meta + elif isinstance(obj, models.query.QuerySet): + opts = obj.model._meta + else: + opts = obj + return { + 'verbose_name': force_unicode(opts.verbose_name), + 'verbose_name_plural': force_unicode(opts.verbose_name_plural) + } + +def model_ngettext(obj, n=None): + """ + Return the appropriate `verbose_name` or `verbose_name_plural` for `obj` + depending on the count `n`. + + `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. + If `obj` is a `QuerySet` instance, `n` is optional and the length of the + `QuerySet` is used. + + """ + if isinstance(obj, models.query.QuerySet): + if n is None: + n = obj.count() + obj = obj.model + d = model_format_dict(obj) + return ungettext(d['verbose_name'], d['verbose_name_plural'], n or 0) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 92485cac7b..24fa9aa978 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -63,7 +63,7 @@ def validate(cls, model): if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): raise ImproperlyConfigured("'%s.list_per_page' should be a integer." % cls.__name__) - + # list_editable if hasattr(cls, 'list_editable') and cls.list_editable: check_isseq(cls, 'list_editable', cls.list_editable) @@ -76,7 +76,7 @@ def validate(cls, model): field = opts.get_field_by_name(field_name)[0] except models.FieldDoesNotExist: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', not defiend on %s." + "field, '%s', not defiend on %s." % (cls.__name__, idx, field_name, model.__name__)) if field_name not in cls.list_display: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " @@ -89,7 +89,7 @@ def validate(cls, model): if not cls.list_display_links and cls.list_display[0] in cls.list_editable: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" " the first field in list_display, '%s', which can't be" - " used unless list_display_links is set." + " used unless list_display_links is set." % (cls.__name__, idx, cls.list_display[0])) if not field.editable: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " @@ -127,6 +127,14 @@ def validate(cls, model): continue get_field(cls, model, opts, 'ordering[%d]' % idx, field) + if cls.actions: + check_isseq(cls, 'actions', cls.actions) + for idx, item in enumerate(cls.actions): + if (not callable(item)) and (not hasattr(cls, item)): + raise ImproperlyConfigured("'%s.actions[%d]' is neither a " + "callable nor a method on %s" % (cls.__name__, idx, cls.__name__)) + + # list_select_related = False # save_as = False # save_on_top = False @@ -135,6 +143,7 @@ def validate(cls, model): raise ImproperlyConfigured("'%s.%s' should be a boolean." % (cls.__name__, attr)) + # inlines = [] if hasattr(cls, 'inlines'): check_isseq(cls, 'inlines', cls.inlines) diff --git a/docs/index.txt b/docs/index.txt index 9e96422ccb..4b295558a8 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -78,7 +78,7 @@ The development process Other batteries included ======================== - * :ref:`Admin site ` + * :ref:`Admin site ` | :ref:`Admin actions ` * :ref:`Authentication ` * :ref:`Cache system ` * :ref:`Conditional content processing ` diff --git a/docs/ref/contrib/admin/_images/article_actions.png b/docs/ref/contrib/admin/_images/article_actions.png new file mode 100644 index 0000000000000000000000000000000000000000..254a8ad5576f9f5d1a4d5a00375a06b4b9a5c1ff GIT binary patch literal 35643 zcmYg%WmFu&(l*ZG?(XjHE(-*AcXxNo;v~2`ArOK?(BSR_2<{HSCAfT>LO?*E0Oh6CAt0dEARr(y08rp7^cnGY5D?@LKxqk0U&xa@ zcvGB#`5`_L(2s!g4jp>SbfuqFJ@J0fw( zVUo}J0-R~b){}G4slyt!t-bfhv$EbM*RD6XYDx@ShkMZ6u$H{_lUI zik_(d8zfd-O+wrcs3w55jP`#MP&F?0_}^?)VPX-XE@eJb{ofs%0GUvoOy{L~tMzuj z3Z0tuHm_q0K?SbLiki;Hq9{^CzHSf*#A!A9v&N(==;=0%N#|os3_`l5F=l+s{{J2x z5!u}t^hB$i%kOixDmkm`h+}1K?d;yX5%ShyG4zox-`6~_y&!6r^nA6|<8--^fPesJ zm4$C?c_fHKwJG&2rJcPo&uBlqF*^Ajc4%Soo?`j0SerYy2E(w3vF^qgZCNZjhY*4C>P0Arg2p6}oXBDS_eN-`dfcFXhi zj)LIFBM<^seSQ6j>UYm0Oq!`~nuq}Vr`yve=Q>4ZlTO1EzRAzFzCbquPMdU%zN5`3 zwG-u7Z{0sT8MgfqVvMir<<_869$2pac~a~xFc5|;@FHx19tj)#IyzK`?SHD|i%dKlO>%Z&zb%!&A8eSjt#d15y;c;8oAmPGi(!=83VXi?Sjm zbeOMPLg8CAHDJLGwrrFs=-y~_-IDAzL&ac2C@=8q#touFGv>79Lp83l|8L+60$h=@ zHzv8ASMdea!E09fE=S*3vP3$E*Qs`OrO62^A`!too6iOHH&8+0D(fT`hiUtlX+eJe z>kwH8vq2LwWiN`D{pB52U@WG#%y7BdCpco;fmj^hci>fP`88KTb+KR)qBd}w>y>`C z_PsB~`*fMdZiX7S#lz`Omlzzua2{u^2T+M=FgV@N@h&=43 zbbDS;@U7^t!{GD~dM+N4j!IPdoUf<5c0B&7$Q8>T1V%)~UL=jNbrCnK%D>#e6o&@m zw($dJ(6dLVy$LNx)0pJw*gEgGT`Pni!s&b21z@>HHLXL-%ga6Cg0*XmMa~!B&rW;d z`nBW_zd|Omd0MYLJWvG=e-uGG2nuu1S6>Ll<4v-cYsC;c@_a z;q&Huv~3|utS6WQ^1*cODI~}rGoKapfLrK|l8=X!mA!Hh7_~&g7X8r}{q9h>(H>_} zs3ZyuxN4S%)Xd3llByv0Vy@iaE15y_2?Pj#3a4B~O{c|L$glXx8M}DJu4|!-qL=+x z+%OE5)$A75VfeTZSRP(yidaxanwi}$e;&=$_ zvxyEnQ|V|b1YWraDcHPBvFZv#7oZeFV3jMz;H&wBhPwI*3bmk>pR^-~?1sO|mq`?M zZ!8Ozd32YEIUojfkFEzW3c(bsV;!3bH#ad8)1{MdXfWGUw0kr*>mE}WaaC!Sv7?`L z+jmo27l8e_Ammjb68`aOm?}Y>$`l%Ap_bKa21qxpSTRhidIio}u8ZKz?lCC<-*~qk z!1kVi&pDUhUX{LN_uH&;3h^Z5z4~6dfipP#Y`S6JhPvKfTq?NudzN}}5p&FheKvyl z@+`APuQ1EQ8CSL6!=Z%JL0P3f#-P8RG)$=~@;QG$<;_>2>Q}Mp`hzRk2oI0 zGYN!bkl6abw?14?IJ^CDP+$Y--{m}QN2Oc&K>vM|Kwt}mCFpa8Y?904fIF+4D}ES5 z3S_tZoKP^7NFnuQxKFfKu@jt+t#Jf&jJ$Bn1U;W#?lv-_X;!A5B`ulkCI4>0Cm2X2 zMx}5fVc`i*OIt)^XuVGKVpEp|j2&x(e#cT+L^+{O+%xJrjQe34G7SgVa4hdeP+Yqz zIukS*IhGu%06*agN*NCQD(JGWuj}=9O^92WA|W(#Sa&nUF$o044dQnIgM_#eMqghU zc0E&~P}q5<#YYK^&DR{mxjMpt=EVQxv%pwOer3=6DyZMR8CZ(|Fgt! z)q+WQP{w5Rhe1(&c$kOPntCOTDt(?a(?#j~qPT-5s}6W=XZZ3A5eblE1;p75vhE;69vk8ogU#Ym3{ zB?7K0HAN?SQq*^ZhQ1qx!wIjTfczi47JbV&>2yff!WZ;iifUrO zi&3VHhVutg(QgA>@$fo)AkP6hntHNkhWACSLw(x8Z!dSQ*v?3L8wcs?Cs6G$-<8M# zO=-I5=X-b>y;>wLP>6bYg=mrq%xoA;3GV0bo9|K4`=-M;An(GOSMH5AnfhZWr$*H* zyA&ZO0fEB?Ox=pZ=9-n8o2WgCXaeEr7^&pzPzrT6>7_lB!x$MKbuvm=+`_PY-`hk) zM3X^{Bz8ubI&HZFtoAgw7$|mOUES7okw~f)ZXRwm^}4h{ZQkwEQrPM_GJ|6^19B08 zwK>YEyA>4_D=`+2wyQqWHA5c;JItq&YiG`c(&X5B`lpDO)34TkrcXypQsXbhL-miTiOCy{fwrYEuo4aL>W~|Zi~h$ z$=H%q;%&afGDYp8IqwUEvx=V4Un6@o$phN}(YLpk^AKWI{cnI$I1`NTy$CY%uI7_|mzM!rA^8B6v}4DhYe&oq?!S*lMvjO+IPsZ5Z}TE2cHiJ!Njc zHWT2PDj3t#sS)R_CFH143RUR>C5SN-5Z>yg5#AF{ro68K9*!l5y&ihO0JFc_3x-L4 zRWuBD(!t!0G<%`%MZ~43rX24jLcq`6m42_!{dk%UkV0*iwES)a5&eZ|6(x}ZCXEux zk)ZQ%$cGL=04e$Q?0QbiMFvQQoLn5XwX#W?Z`#`ks7mj)fw82;MlRl5xBmy+29O}N z;Vus4$kue*Wix8l6)a0^1be^A8K_&Ta(LN zbm5YH)@RY{Qd;1BL20Py4pQHJ0lxgvFcW4)b$&faFqO`xwG@Jt)pgZ_T(rHiTj)0$ z;j%#;^}3*+Dqh)Ak>WbSvX1zU&X1Q47)bB?eMRR zna4uxryDr1#t0*gR0%l13E>tZUtU8%}HSoVeH7 zGL2wbBVR6LVo{>g5@T|-x>zqi90WY6^}9-yv^(}cl^bqYe2gFO>Xyg_3uETt*H;yl zu%w%#q`nN3U_DZ#gTAp`@*1UJimUm#Z~r(*H%5YZXa@1+&QC~EK^RvC4Ehf_QC=d% z7R)PKn(t}1C%%J`Gn&!&uX_I|^X9Nr_nYu!5}zc=bdfAX2LAbq*)?xJBN3D8I`KQE z|1xGj%EH%VVG8vyVRTc5L@l~r(9$M7DEZ=RPLtD$Z0ll1lsAj^KMKyDjjn3!&YkB( zQt@v}p_HoMFouItfYWDXx61z>`-dWb41`I|AbvQ|mHYBvij9p2zJ#5@_4!5f-(Y|5 z+J5*pl_aJ>QuN>P3IW)DFupw?Kjlm@na+x3iT~r@{K**Taxv9A#3Ki~L#6*4m+jBE zHP@dzwEj1)CCp7wjoj7JTpbB6%zd$YQFB~C3K|f%O<(Kpe8knH{?L0Ns^tPI$S^QX z$30``7VU7o)N}%8KUGT%RbzEal@H#P_*CQtN34U;KdS#Omun_EJ#YK&AJ(9RZr2bS znzF%zy++T8YI74a0t~y+Fc|5)rIOI@=~HTJ{L)*_uxByaf7hwdmqIo7nI@b$BLK5a z2L>?`;v02#!C`+4*3bu{g->#5=aq4#<~_TgrkQs>$f{fYn@J}CTgSGO zFmrl%Jgxzw^SYluf4V}CjgH2C;Hi)_^w{v-`4B}G%%8g}v41@GKn|`G60XyZKM$3He-X_N2N~`7RjwkhD2`c~q3lhnk3ogFI%7kzHg;*?1sUf8CB_ zb6#z^1haldWYTO*_3@QzzC1QF7ny&-ts0L$oRi@ikdc|W_}%Bc&SI!Oqi&)=>O)H#cF?Uosgq zN}q36TvWtfxF6K@KP%t>pKoSW-X0gaywBH-zI&fiNJajv&`E$`{OZe{Suz<&M(+P7 zLglET{H`%sONWR(l)$J0%E z6;mAO%`*($fiI80^$-X-%wg;*p%i?HXhqY&rT6|%`CX_n0c%5W$oS}cRyr>wfFRKsf;crvYd`gry}_zi9b5T$b=yVq72HM{o=s zgVXK>m;@zy|IWO3K2P5e^*y%N|F@sv;-u{$d=cDU@RC{%&emHg&N~!hkEOFvk=6HV zE+ZE)@r}hs-e1D{X{$n>9njvHQjs?S< z$>CvndHF=C-Ew6aRG?z1ZS4^~$L~kQLQ(0RY0VDPiA$pAQq!X2Ykx0v-sn!ac^(4yi&iH0Do)&8W0 zVvu0&YT#aEfRZrj3Kl`_{YWBIj9#z}E|)%iD10ZYtyuc3SC^NU&$7EwSjcL@Pw15r z9{r4i2FcH8Xk45M53o!8r2 z!P-`X&6M-XeCXBcv^1dC0sw&B&fq%3;%mabmQh*<p$T!B?3<;uc8vQkvL+l2&|P*03^^rL}+>I;U_y6Qqj_;b91f-)^x!v_w21NI4vf# zMR^7BnpnHaTsnTwgEcHfAv2)oTALSGJC%rUoP;L}L>KI(fPu|Q^qLkY3-|JOmjsRa zD@f5c#)`G@Q!C{w$tYo5D@C=Z2%239nIV_%bA-u$AlyT7Nrce>7Av*8aEArDRvcg< z<6a}ckhvS-j%~4o-yPn$wTIfanF6A_K;;e9DXfMhWMdTtKkvxY*$-ZOKNR}rmZ2e>U*(Q8KHblr z1J`{)qQD9~=KVRlntfi1&$R?aw8e=Xj;Zb;416DVeYNcLF@~`q&2yp|Wt2)2)6nu2 zciEw73Pjo^XiH8`pRrN2buoJ$ousIBZybLVhN-|cw%`PHT+LImKLtNAMYs-w59uS zB$5$kGMqG~FmoR%*UlOhgMhFvgIz8Yualw^OOAz==Iiia$|5Q$$IhioBm>dxh|tUM zHv#HQ3UvihyK+h{jsD@SAKGEA(k zBfZ&#upa(Llw4A(zVErZ{itm(!#}Pe{`fv~El?l{tsFc8<-K9*C z#|%pKxo2btHFb*1%>|%Ok*#Bw1rzcX9}cKkPP`n{qIA`VqnBq)!^l<(g05=Wv6yP) zlgR#1beRPkuEpY3_n^)gur-Pc55(T?jDA)|a+ChgQ%qrEr2}I;u(Q|kZU z4JQsZrfhMYXe1ISF{7@h>!CHjGRrGQ_4*F51XbW^x(&agsu zgshaSAbQMsH%gh4g!a1Q=)W_#7sB9wZtt&0*`kqDSd4>Aj3Q@8(1q$ae9PmKTDhV$ zH4aG5V#vpQxS(!GlDe&@Q0i5-b}{-d?v_pn-J&sc{Z!*hPu-gB@h&P3K!PDl8Fhil z+iBNpz?md~T~(nhL#_gI|A8HWf-(`)j4nM@9tuTJ>>8v7`*#Z9quHl;f8mnv{(z}l zZ;3!g5!Z<*EzP?{(<{GdfegxUTxptkl1Mq^sVJdTr3YLj-+$Gr7#tevQ?z756>f>9 zp&wO5gtgZy24EvXjb*U0dCqFnlW5RjpeIJbA+rf!6A4rKH>#q8Sa`Nn4*>z%u8ZXK zslU9${N=UkT`>~<=mk%Ov4>)myl#ncoj-kj_cxY4p@;R2D>AQ;Ly{zQ(9jqgn}6t0 z^G)^~jv|4i7~({DUsl|rl-c3f^Es;HdikF*7mEQEm6Z{i%C87Y6_=JDrab|1PhwU= zO?t3Z-XH$K2oBJXx2Hm`o837somu1%btsMYb8TRm6|9#}$o-Oue?G41>NIS1PY}7q z|0=D-c%}A~#bpy4d}R`NDPyeV%#<9m=C#o1w8E<^hGR3HY4%&g4P3Z;9y>%9Jl&*2 zRX8~i2B@};ddz~|Zg5U$JkQB|KR|JEoBiA@L#ljxw)g*gJV?+6TAs=hIxOjFE$ z{6$jjubM|-@RTG4s?aQ#d;}XnWo?$FB_)ZrYi&G9kjZ_W;KJEX{`|F~x*k*tM%=r1 z=NnxH&Cc5K3Fp2{#Rajce-MIho%z6Dgb>RB#Ywcp1D?PzWEBjrhCgb5QQeAVsjGN}iO{AU?z=Y{G#Huz4;)PbChIt#gTilayrqDOa0ZRB|} z!#c~6?Kcp0ic^MTl#!0n>N zl<1S=uGDIXT;PWAA3>0vxEbpW5 z6DiC|O$CV$w~6LCEq}x4>IFmMVV!-KwjQ!6H+lxLlkBu12tC)ipFhtbz-b#$? zJQ*Mbo5dFh^RW@(5L}4@BMISgcp2=dKz5l^YdeJ^?y^4>Xmb-|^WaD;yoXRUkl*13 ztjW9S>o4bv1fbvlUN8}RJp2U42#I)*z7bA>Dv(U&F};%Hm0gx#Hc^@8N@0z(gx^mY zP8L7*tx8579=y(9kOPQq-G>KW{>b@r&>0BC>BS4i0jD4=W!t&0IHOq{fFB={sN`NR zhsgL=UEgbcL6F&WkrfXHxMI-q5~ZYwT&Fn8OE+COUKw@fZQwz;|NhDNAow|UQBBZn z6PIzFQG^ll(i#@ff_|r)fPW8Zy?m~@9V(Ee|rHO@uxxUl@8#Lh^`y51y8u~!9OVD zG@q6k%It;4(7TAlFxtv*K)V(pmLEP>ENADP>RJH39fmdAXKM1Qape0^jv-<$>v8=2 za*Epb>PZxkR9lu(+QUhTA+`DwYA~ogP^#$Pd(<+Y9l1x1ODl+zcV5XQ-{>j5)hnby zbkaqXxKOFldU61Be%{4O8FG%GM^89b>w!23nRTgXBnQ3?FuI0D6Lx)lIOvnCbpzMH z?dtm(MYgmkBCtmiI6{+wZtyTjz$!o#D&&5?;qQ}t5ID6{Qx^Vw_>l)SR|yF1_B+Vq^E_0=e4f9b zt~2@W%`%BOE?pZ2nB0oH;r|PWo7er3x&SzJ=C({4rSUD-$8!}?(_tBjt-&vkU~k$N z)|7g;p-3-P5h|bCWnDOsr7X`o+m#k$HmCjQMkvL1h@7I`si@WC)V!&G`)%mPonca2%B&y-&Bl{5xx6F3EC=a1li1JR?B6zNEjmH8?I zGXKM(+tZcnN2Y1ohZ35e_0m>C-7K(eRt6Rgz+Db68Y&_spy+5>WkLRnn@SbuB24%=L2t8yn9rNEK&|SPfovY;V zvKZP?>|+fox*(|)ec3MS#S2F?VnKZ4wM3OUcvH?(Qf8HMq_)lIP|^g}F8rxoue>mu zkoZWv#H9Ufk&ZgSW$VR%0h8%V1Z6xD5u9>3iB662huu^Ljg}2kcGFPM*IIh1QXL4W z{yz>bSPj6ux6L7sYL;Jh8zMObSEJ9(YVQt^Ajz$mTDtnLFx7TfW^qhVqmmgqZb<5t zpf^CUV9z^`U6)}1`Q~gb2R_`FwiVn;Aextmv+mYA!4wwNPmdvqgoNsxN?e?>2;UVX z4dP%QpG?HL&Upaaf>&oeSwqu5t)$0yglW`-d@oGD8004#Lkh7bW`XU#U`3bm5-KVz z=8&KJ!zF1V-Tr7!+0V9<`)YATsD>Ey(lEZ?L59x{kdYe4OyM>c&Pdg_&&qWq{Yi+J##eCO@gli(H-0P7YG4k+frJ09%VUbML)C+ckOrl_1ve;NX78`9iBFp+mn9G)tO zpui0>mH+ZCJF;RRNKwO z)?S0<=&*>h#Os=nm$*ecUPGR;T=!TIAH2y;jH&qxT4Vs&PvTP4#ZIL^-Yo6gV>WKj z^n=qwrE@*JBKV0W_Wa|U-K^AfKUl+=Z*{FXuxntSvaf%Rn-;*rofeKMSVwm=g zSTZlG%^Q^|Q|_92y6$ZPa51}MgI}b!agM>-D?-ZZ#GK(PjlS7*J8$5!cZO&1UQ*JW zgMLx;XEH+%jW&cIrF^Y9fGX4`LL}?$F62#)#Wxv5e82qRhZGB9jQFBg?$4i^_f$y| zt`UhjsqRqWeERhZXTn&T8H&OIhUAH8JFHU=svlEFKa1@`41+$`hlW#Q{3PBs>)Yvt zHnQYYYj50C$1`+=e8xisp(Ro>^PRs^u^X&E!>QQMR}z%qN98wD9)a2Lr7yli%9k|C zXWx4}Tcr9EtaQ-}APFd|CUu6JZL&tMftEY>^E=WE8!K#=U9q|5}4H#DskO**k8aFdeG$n-bUc{1Xxc%T`D{5LlfH_k_|5)2-+NC z2tv>6y=CUqG4}gR-e5j}nB6@BLrCezeQuTPL1`&D`kX3t>*zP>f%B)srwa?RTIZ_a z_gU-t-n+e86c<{%dl(%_%%Am0Qtmq}$=ab(C#jbHcNav%v3zo~u;SvCO1dEkbO_(? z!Q~FV>#LT1Fa4$Ek6Pl#+pVkj1D3q(CJe>t)Y3|_G!N#YL35`RTnJR)bZzKf+ta+} zy^>{9`` zwip6ElD*&Hn)h#TwkPIuCLUu#C&^AB$0ulMM(cWe6$&yP08L&z@~2nyzubdG&=iK( zWK+Lrq{;}qiOc&Nu=I1#hebCZ^7dpR%&JPfh~G(x`%X=nuaNm!QWxXBOiUMOF?Aw` zj|&7oA*rHD+1xQ-SjjA&z-<_=Jbp=ASTMp=@OM}F7;NNnT7>Ak*uK-X*I4w_6QWnr zB45R4d%z_E5AtDxbY0Z;7*?DdlWohGV60zK|6*xxJC2|a2iK4~50F8&Dno6Rq9|qu zjD}34v`>l-92g=9rGvPBK5gE>-0I<^Fu4sg0I(&sT|n5Lvox=UlL0hP*e-A7!dWhc zr{}?qT)uaxz1pEFH)i{8BpA-`_QBfnB?o%YyX722;}M;v}1C0#v-W5qF~#Y5s{E=r_lX)MV1 zdI{AD_j__tc1o(qSy9!sowk z2i*!Eu@)dTnOBNj3X^ZfS(65-3#`TCDC#*OW zLTn_^n`+^kjFDQ!?#tm|nCbopUDEblaj*4Hp(5ATezVgmfYuhO^R<(3k97Qfn0=zs$JsC!}EftKq@A-%gK^=HU-N*P0{R7|6uM`dt7TljH3 zVfZnWO&=H;sxUa?uk*wE^fYf_4W{&CU37|~FZ6~ZiS*A=9P@vZq=rA{r`Veg-GRS@ zbHOYL=%m$k>2-oWG9!WaiK|JKvdsd;_{No>1rmj;DUwxUpvXpEVrq=fpg}T$$s*m% zK*cXx#Q>Y~abV&5q*|L`M5@%F-B$l^TctZ%FsM{ei>c4eq$Bg7=~As;2rOP9wEo_~ z4tGD63t5{hEuCE}fM)85CC_3Y(D&|MrZx|zV7r67P4esrfzqre{6gys^akg}DAr(4 zHlU}B9~v-F2#7+Q%IRSNl#mXwgl;r8b78sS&vpGK$PDI_a1_s)g_o-l%2n%Zq;qi74yxH;=2c%VsTfC;a(J$ZN}~>3`4bf?e=4# z8-g0(XN9!NogM?G7PKD#`ADN5MRfgsi&6!`qX=eOJ@hroIo3z9YN~iNi7dyE^-!kr z7}Xd#R0y(Rf$R%K*nWhK$8v!?v9#PRpp>!q%IrqbY(MJYos@~W!bFIm_!Sq$*>;sK z@}NZiR0=_yCX=wO9m%%%pEMuO&SD*m=iPV6LLB^ zI(|WWnL+?%q6wEKOq46OH!z(6^(~FtCMisQ8Q*85$I|k;;K?x#n4K2==>p~`F%BDQ z$=OxM)H(Yhi-pY5-NSuhG*+{qk?WtXCk7MWwae~|j(R6@vY*X{=bHI?AqDI{5W7 z?>|na%qIJj;x6UnS7Dvt>Vy3s9XLEX0Sl;!JFh666lsn^=sC64J#?Ewnto*Doqd5j zeLzn>gXyv#KH$#Gu?NIN3!ksq0wbmmI~Ek3QbO|oJo`d`bWFF|K25&yVDwherrNrFnzT7h+5TM8&C~9L8TO;1k0;~J zR5@Wp;Kwgdcdnxz_P&%Z8^KY0`(sG9k_pY8@MX_2@Hg4eoD*NDZpFxVstDCcdzPDt zePgea?|$(fLGOB$nvkc6m9Ban>9M+Z-`qINTKw~9!@hv^wDx}EiHYB9tD%HwF{=s7 zlmxL1h>omVhn?MaeNo{aoGA|cLo8nQ#%WZ28@a`%;o-W07f;*0V?>`x(^FKJ%KhD_ zUqm0oRjy`CzE)`zHJ1}E_KqlamWg5P8nhp2@~TLa{dfah6-lHZ7CljwSWc~1f<(ve zQjZFf?D%69Dc|oBm=1`B4YK5W|KzFmBosSJ_mA3Vlqe0X$&I~rNetFVPKjyq-T0&= zAk$=5^9-3a14`rTb1O61bR(J-TN%ogKD$X>23xKFBsMXqsDg~e3IXQ+j@6uKOyuzI zAQ!k@eg2K%ep5@K@^^V9gkklgHUfIzS3N%#4OdyB#5dDhmi|Q0^E53L;p91ypnpl5 za?9>aUC9vM^I*xZ+MO;iTsh(#VY#r>?-B3S;J%}^+2MOgoYLA;OU_|n2*JotKK(wv zs3Fwe#8uZ2l}${aAkVrqu4c|*aNXH3+`dG^o$p;NJt#-WE$DtD$3XT-4fhlFnMO&i zxmmWcB0o>bV5RbJGHRh>-;ctG!=1eTKxtX3vj5&^Lw=)mHiKZx^}6&!LC{q&OxUM} zLzg|8kV%7}>xK={+ex;fTHb{K3x#_z*UwWl)P2F(GInQfg~3e?cJUfZ?J(nxI4a&v z&%34Ox%F##^9B5;T^{P}NsI@Azt6q0QQh>HfZ}m($_;P)drz&6!n7#{r@SVD zBt!gr{+Y##n98$wd8nUqWmNYi+$wakDrvFKoQ;Q6u^19T@8-b_7rxUxu@pcnk;O(?QQ-Qr>c zthBzc6YI6RY{FpAad3QyTh@lS??5W_B{9z(oZ>(2^cH+M(Ia!)(;&2N^!3rPyM7He z9?&iyvN;0gaV0NX@&vKCj@lcYLzI6>c;}fdeC+wKRvuD-Wg?i!Kvm`0rg5O1BE^@; z;q;QcqU~KdyD6cOq&A$cIb?U;8n#T1WpkB$Ea23|-AE|1m09K;akY!AppT%LE+!ze zFAoClTKex2M2}498E_XrPl=6q9sBn)%>SXRvBXe5FV}-%Hu=71M=z{`=Rc>#;sFa? zCPS~^97oJe$|u_2&Q`YQJJkh=z;e#3_5A^b7Rf|bQvQ%$yKRr$c~`5& z3S^B)qiGteLGTY59d6sj#r@r^OyK}6$-t8~7NLxq75y^%%^(v29S_zE-#6$5{{>E% zc_(>AM%P>ml=#EQn%u`bekKz)g+?E5Ht*Y0z>V1grOoZ!! zHDml~O}D2YmF*SF&hjRiuYqC>_;tU$Y$o8V=SJPNk=;xgubnw%dag#i@)!^JXD40j z?*u-e`U*J}iZwdBYjiv56S7Kj z50;YU0*z2gVsP?WuxGg|1mevV#C)(1a?~`5c2D%T$J?0M#|=$M;2{onA#`7TZFXGC z9Chd#v$%d+I(+cJ;s7p{SKXfSfD+g+n{AieZ!7hP%2vma43q1LJ4vyyOy)xX>S z?&L25m_Jokm`{_Vr)Rjnzu&K5Gxk0j=-CLt=-3%b2s!vbksy3zPy|>xoG1Byz_#LR z+%;bipU>epuP~+JUY$f7V7gIacPba3!aE71 zKTmaZ{?hk2m4`U=eZod4V1Xb*eCZVxm!s9cP{ly@AQ^KJofNiL$L1Qi1%0Y9Z*23V zSvD6ixec!rUNr+@^X9zcEC2OZe};#4rz7WQ0~~mK56w4Li>y&k?p*T5RxFc2z3U{V zEyO)`HVr1-&OjHNV`;h+7pS4keFfgDJ36Pz%cRZUVW2%{=K{q>w|YH^hrXX{?BhrG z4Ol6*()h30KJHfU7+2yc9TEs+%yFOz+bvh5qYhJEgmTkrvMCo@;OtM0j?ty4AZWFD zvW~8iOWv7yr;F}eR(cJb#JJ|he^|PJ_8I$3%Ty+qwFE@qXos7_@$xYU@lYFB_?;(U za)BwK&X^MvpCY>9ttMm+n{ zvQ9K#6L7gS)aI+=%gKi^*Sv zX^LdCZMO*;xNib36ImVEbp1naejEAub!KDLXq`FpmCgfRIZYpO*}J4tz78(tk1kIW}PRXj`cfBo!OJG$uL+VkD1<;XFpTQ_12brB5E&hqH77M6X< zC0xk%N+Eckd(#>w3U3?HZ3eUCUOqB2sXTZN)LW`s(t|`;A!|!*osbWxP++jxo!ZIE zUfBD_Gj&5jq`B5Pl48IgwrOt=?0xIRb#LRy$yx{EoD6m=j#b|cLTMqe&UmjvZng*X z6|&qL;eCJ{bS(YkCq6EZBw9_rc1P^@%U=CYDS?9u;JTa47I^9N*$6dAReHDQaTeeW z@3^`@LF6~q;_2}#b>v>Akom2BFZ-56aKWXU#KKf@rQRyR0k@0jQfp$_i!|t$jImz6 z#$lk?)$ve%uwQRWff&&vWIL#$KM^nAKX@37aA<1UJx%XOgDx7|?^k!wM6Wzw;66m5 zylk@B#8y=xNRT@QBJ>Ca;t#y2zdIz!*h$>bMTk1t6<`wLs{t}Rtr283;MUNQv_#w3 zpz$|dE(Pg-v)5(u2$_FmIVz2YfV>N+DrPrD+wDH7160qM#S}dbOwOB-*o1r(|y`lLhqzt>n0$5FHAGTL4oZ zVzgfxF#QbdK^D?eu-|<0kqdlz0G8#tZAhue9xfofsFZ8ADop5m={MTF4V|Bz8YX%k zH<1+5o_Aaen!Kxv;9GZ>TdLpt_0uU-EOK0+!RBPCAxCz@?^;Z6NzcQ2f?OO*1(;IG zLQYR2jaX64M|? zy;ZKp??9VR$8Tcdy&}4=?Rspsui0G3?swj~4#8dhCl!vz-8yEY^3l|8*# zJUN?l$Z>d&w$jPo5d$+W2Py0Oxpsst+VR8=x&4;QgHjN9UXGb<#&pXj#gtAMS1EP# zy*`jM!qZr5HKb|2)X-bu62MufC(1*L%$!nOt>@l=3`KAHQXrOdiU;-4Oh) z!;gTI+qwW>SuQiIY3lLroI^XSslXsQpEYG`l5 zN1fpmQE|~o0y|#WQ28bScaM|y)FW}bv+>`DGbLc|i;V%p+Ihn$54_3i!G07awmxoh z*f>gCSP!^6)v4()|3UQjq#3S!pGgOGD_CoFP1{*9_o>;hEj(yKztLtYH$QnhAR^Wsq6gv#h zpqqb!`^E(pkT*FvMhp$MIT&|xU>wJP@vV_s3n87OQbz|GUKbq;5A*KFMkLS08D8gV zyfkZv^~u~3C#pzE_r3;KBIs>?G+eeCu$lCVEWL;x3Jw%lQhsziNFa4K;FyfXd$-9V zii|~f{40w3g?AZ9?=UYqo__l5S8T>o%&r@}l{cB3dP4RdiE%`RF6%z)ccH9ZT>cOusArnBcy!JUlU$ zAmRq61l`i08&qg5L9Y-#?R0m|QA#2ViTdKtO2Txo9Qdu>ue}BdC^f;ON zbZNQx3T|4+og?V96Z5$pcc^}gnz*B#nf00fYIL^_U%-D}`3eRQRcZ}Q&6+dP6^?7^ zkrWQ=#f`h)>{hnwLN*$L*Oo`MzRuXX;!MQ!E4oIZR}eeb!HFrfP?A+9D0bVWul z+`jv&sP;SdvnVN##pvo>kPuI|`f#Jzd!aa*!-sR2f*7xyfYgJ<_--Tn z$)ZD~_AwYDrJG1c0Vs++4-iy{USJ)s(ymoZKlZ_4omTXCEp?<4)j6Cc42+>4Rbm6| z7*U(W+_Ln?z)-~9-A`mSN@wXLl+uH#{<|M~JkeN~)kTD)PAURC3)@LtSVNac)!hq& zHN?u=D#p&3d6tBOO;7SSu=Hr8ejI+g;x%JRDy+)I!g~m;3&dJ5z8Xm4TF{bg^2M*X8$SQ)C&8W6_ikvRP^%%|CAYX0kgdja+*a-`%Iue)n^YioYI%zjI1+z}{p>rtX;A>yTb zdl7)S4m+~>662PhX*8M2#WJmyvf~UHX)#oZ@F&<50K%_IlLn;j{;&tWcc0k&*vq72 ziSG9Iu`U$=fG&SYsc0RXr6{t1`HYs=(v_H$iP6a>?W>BUu4rWE6x{Ur%S&x zNx@yL7(SGdj+=#wzPxmzAK$jw+HMpuW;3mV<^q$C5z7g)HV zl6-++Thh)4Ne?FkVM~}I?kX+IBf%+``s}oH)xT}wZLwu>@g*{=zdyZP)#*ZuuiiJc zjt_c%Ta>Cle<*9>ZJ&cB^TVP0C(ZaJ*Es?*WD>+@mr*Dh+F7TH;bAu2R=?kar$*Te z%t^Vb@sA09`=F9@?lIQHWW5IKj+-kcJY(N!k-JrqztzcQgBd&8JVjG zNPm?$Wm$C zVf`@LND)^&g`ExozjU4q&1n9WeZ=ss4p*<5COR(8Y4sbyrJQhFqmPU1{(1*GhB`ZD z6Ei%f<{LV%Hy zA{QigZQ>Z5$p^EL+DeD}`>?6O}v=5u-zPXqP~WE^DoH zGz^{Y59;Y#_$`)WZx3Dw@YGZvKO!)MrT@_KR`uZZpG4(o;_v%^#C=t49!t<=Vs;!e zGee9q#S}x#j4@Np%*@Qp%*@Qp3^6k^J7(TVZtm6U--ngs#B*+ z)a-ee_cjew?3|*O%f*{{q^=|W2_EyjB0`ObHQw7FExk-n1f0Lz*F+jOX=K>`I{Y(Y zVj{9WJ!1OS#E<&jejXiS(v=|ePdj92Is+~XNR{5g)d3Yh$bOJlMwe0tSJ%+Hu&S@z zS?nc#noi&$3m&WCUW-m8YT6$=yxWYfkd0C-LF;%O!9?6&q-j`Fu2X^icvCsif#up4 z_tyGS&$905c2Ll>N2kV%!Ms1hyv4?|EEj|SEEX$Vu5jpC;~2Rga@gCi%WxHhs_Ebp z-6=6jOFlqjFA~=%G1|)@#%`+rRh;T^lguWd;5RJ&ogsE5LI_HzHJi_p+0Jjpf#}(m z8>vfVIgh8tgo64Nhc}wlG-^{d(XWY=fSS^mj;$DPxX{BTE>&G~hna=ZN)~vsZh#SV zFLr@@tTA0Zv<> zhCxBf)K0mv)I&jD?g$WIXPO>QxFhU_7?-Psy}tasbMzNy51g4%UvKaUa0NVmQAs7} zGK%-;k(?Pzq_!mLG{%mIjcUrBRHN^a>iqWm&g0}BQIufU z_)@%KZ~ve@LqMEgjKa;pg;&-JaUdOaF;EAs<{npXUxX(|l+^~&kXR`BsgoFp5f}-b zl~d)2s9HtPWm+S46q$b-hKhD7@Y6`rmlht%``gR-{d(~7Zd_a; zo%3Xpsy#ZjLOeyH#`MYKGkT6OqPx%gHibt!-eIOX);Q{9Ye2ieP9pWthelcI2(NSq zHYLK!p|DzpsFUrCNk91X52Sv&g7|792!*z*+ycmA_;@fx^pYlQK%<{D!PJa+fRIp- zZ(nR$=zD=pCO?qTA|`kwrUe-j4fon5VI*rqLHq2e8=ti@B<`AeEhg9$diOu<*1Z1 zs2(*`XB+R^2wg?W6taXfg)9tanl}t14c-Td!NyWVD#xao3R~JjzpJk|?y8juwPit; zxXk%@XntlKb{W1wW_4E`ygh}MkR;tRLvJ&< z*1b53wd75iUd{5ieizBcA$R0=CM%?Sbbh>%KSaeb-6blRLe&hI-$*pbB}r1~{4D zKeO4fkq_>m1iRnaqK`l5rGshnuW;)}Lvzws6pZDO_bYEPpgAx=2nQ|0I#}8Zf>-}H zXSE};j<)ijAp3$k2xXq{`PA%jR`kC_rJ1ee58bR&A1308_bN3unkrb{A7mn+n1ZzS zLg`*2)q7eYjGcsmD1$OQvLZEzn&(G-&X=_oSJ}0h{H%n@A8zRSgCI14v&Yt_OnHXh z9eI1r>V;gNjXWSKa(fTbF|cLMCm^HcfRn{BsV1bWl8mSFG#n?JOv)J~FfIZ< zvF9)-ek1?aemo;N8NwfqB0xeuJ334yP|;&gkzC>1OiX1;Z$zY&w>VpE-kR?zR+7f? z21l)g5@v?yx%94^=RI5miR(%1=iJ_!CS8&ZwyH%12(aAj6L5r8pa8K`ODOZ5{z2uP zG6w+^&Ym5P3sz&P&ee`I?Iq6eEIzw8zB)A#8xB!FVG7AfR@nhFQlL>gPcAEerx(!$ zVju~j%P)zQ^5YLxw!LT~?I%$)fB4z}dzAo@g0j_D%hC!Rj?l%_^D{glX?$8_L?y~l zdiIYV0#V*<)(4qL=gKYM%Q~4WNIM^uJ~G$)^(@pHXkhS@!Hd39 zoj~r>v>V8Y)@py*M83dPp-;JhzK1JIl(+|T4Nw^UKQ5Do74>_S6%E&QMvkVlv#a>~ z_yeI(Xf(R>@MAhJdXa>^afWJLkXko~WszYZAL_HmNC+Rg^19e$#dh0R9iy)IAal@D z<-~VSsiS=zCIU^1=xBUq{}Fr%?gYkD>j+I86~cu!o?nJ}fhI%gP}?opiuO|#ogVJA ztpX=oXqToqIKqKTNhy)i6aZ06iaWzce`{eg5fCU?>6&ilRJorodVoqu99KHD@|EZ& z?h>cfV1>B4_gv9lo)v)tII1P@pz*F3+Z;5FgL*CP1TuB4d0D*%AF!vUJW{J$?RrFM zj^u->oi6gG(aSR0bgzQckL4-Uw>Ii<$FNRtU?2#2WhyIwt4MW5dGRLo#ullRNhWGofvq-mU*Ts@N} zS-5fNERS=(wdHtJ)h43zb>`vm_ukmR23h>G@iN#gz%`JDD>W(Pi|a%^cjr#DrLaDV%Hq zr_*?$AE17fu&-uft97>|+@MX%_ZPpXlKrXf6s;l|NO=S^*-{xcLecd$w)s zH9WZ~th?T>!LQSl$hN(X&fZ+j8v9j$sw;`z)7)0sEq8IA30z0Zur}?9-SKs3Ppi@a zFf)XM5Vk8get-O_OY(e~rF{iiT``9BmnnKrL_RYzfO*p>%$) zdV(#LA|;#d%_`>8+Vf)tW(dn%samo45Xmyje{=?(OpEpQ-?T#jHNn$ia=G4b^@Xqi zWxYVLB|tX>->U+MwW}=-Kov(HzzO}pFkK*}36x3zyfx>$vlpP~gI7*40`GU>s=#E0 z0JvahD-6j_f5H#+@{!c$itb0;r?$8F(nW@ypeJQMBd1HbkS{2cn9`J_(DvOSyYuF2 zBFEVEXDS_R>djrlaJD=BHcL{PwVTJ6AtW}d@z+ixUp!u~p7(AjX_aekT7tv|HJjX@ zgmU&{Y2sWrgT@NRH6w~o4_$^bYD}j`dS1mP16y88Z=o-(=-h4#M!5Oqw9XD^SM*W> z-kK}reMCv4ku2LxR{>R8ov!<~-Q!y(+5U+cRFSGDaZNCR_QYi}^fKTP)?V zKwNIm&Zke z8fmy!nc@5EN?Af(pDZH3emfjbi&f^F2@_Rqu%1c?J-dovFiNOf;((e zzE_gvEBq{F?znpWP<*%VS!?6*hMY(`; zqZ1zGJT`CTH>xuw(q-C5nlN)N*h>xEFczX|#(GO%ie1%Gl2L*>FVl{0kJTz90iad0W9`N0s$KG?Km_VDewdC=PU=kE z>s?Iz%FB8c=kk2}hVNdBy#a(GP#_u%Tb#h0ku?3+2O!{vs-)GyFR!vHHJLAO+X|+F zfX54sAmJYVUO$Pz+G+n-y9E~%5wbjgy>e96uEJ<@A_xC$v)>=5to*dBaWQz0>Uzfs z6A)Y>8XHh5xAC!aK)UMWy;QAaa^2%HbjSpE`=ZF|5%*(9&{tjTZ(3o&MT}@>N*$zW z1KKQ2sXV?T`B-%0&0~kKCS>NqjG?`<0Z>bD?NEEQx?wi!y92@}AyQgNGbD|m+itge z?e9Nmqgma;V%{3$sO)Msz4%&9E4IJAm=^ij9CRbAUK=Wg)TL1 z`c^Jf)sXC%{4Iu+2ID2-NgMPzz4jbB&F1z4s zg2+ivxol5t&9$=Cg0NRx-5N?aY|S;s=BlBY#6Azaz5uCY)8~A%7FrkG-tBVpd$oM3Q_Q6Dda2`5O7m28f55!UKYOX>y+4e2_OaisWREX>9_ z9V19C8?6Y7&6|?)=4mIP5NWaT6X@DaZs*adI_SGH=)*}dM?9J2nMwX51w;YT1%-gl zi!%h!tcP+sd?R?ixiSDVvcZGDkTS-=T8S#AFkvBu-zc;KHXSBSJbP+weS zKP`zjo5X4&0Z>(vII-}TmwIjP>q|23@nm`SF05c49Nso`MiNB*$uFcxe8817EMAUL}a!z9+`ub!7fJkUD>hJ0& zP`wwea)1b*cU*p6-p$ipDO?)XoGs{g#QXcMZ?GIV>xVN`{EmJ8xQzL<>CfVK8^6UU zivTSFOZUc9lZ?l9jk;>zy5M(S=t#=doSZ%d-q3we%7`G}B1QA6KQ-bgonzcQO<_+Q zPR`oJ6TpKm#HV*11x=75%gPh#Z$Wl@eS7P%!^R&PUrnO^vUt~x`5HA?(^KVWczens z#yFLM+rr>DI#58~$WAP=9&;Gh_o;WYkn+!PgNZ>NnECpwLWMLkl=*!k`F|xamDe_y z=T$QsgYoj1JKwfN=Z!3(yOXfeC&_<|qQ2-q8UnQlr%FlTqHvHxlAN;1`ObV!xmF`udrkxo} z{ptLV4I5B`1lKvVLKmKPhJPrn|A65CRBXimHI?<~w|vh`nl*0aKXC1T*Sh(?{Oj|? z_Qmi6@A-Pxr1C-9cfY?K2~mO=`v)H;F}IR%cr}cmpT*ll*`k@l%l{j2LJajx&f0Re zw!#Z#IUioIIxZX=l*H#;xxAU56Zw^a{&EIEG z-i23HE`iWjiY`~RRHE#OO^mC%47}8xyhKe4N@<{!Z*8V2ZaU{Z{EkUqK!rYN^=A8B zxjOUeKk9Nh-i2MKakwQR!m(H^-7k`#3n8$whY-0*H6kEB*p7}~PA91P$qyH-**MX; zI0_?Qyi4ZFmp_&LB;xeN_Rd4FZ=jFMb?lPpVXV<9$3xS0RXHtXwj|YHH4y&0%@_F%_a{_Pzwjt|d^(cb61QN<}I*NieGK zQ!iV}HuNI~>a_30&Hl;7u!cgd+)QAj(`h88#l^*@x*l$rJf%5W4~{>x-;)2r{$hMY zQXOmMzwTks`^_llv>sa*VgvN&7V z>)=M45lx`sKFFrRVlu_y3`|H{X&2m`8q|5psj%Z0O`mETx=h247a2>T+;zAIn5fJ~ zbHit5N7v_b7=qR2SL3tT^}C89tPgK3_l+2X%`E|&<|yKzkU(}-&ZBV#b2r$-~+=H znLpoOp10EPM3HNLP{v}YQ9DF(6gB;Mhm0vxl&I3!IjZ{ zZP;#ETSrG_rovU>9L=h#YGmHb?XAV=g`GvM(slYI4;yQ~*zu=`SwMJtm#pw||11RF z`iHda(-RGv_LRmSd@9Zbd(0CjvtMs{h@0)F!zbKRo3vl*gY{&`kLQ~;ZmjDWf0B?* zsIVQ~32;m?V$GL3b_JQqMpr#@c)Ns9uAr`*4fM0x(&g$ulZL)D*ED~ph#}&{pQ#ud zT;H=07aPF2HPwtz!AyDru=04Q-|^O#wo7fv$i>h~TGYerLeo{eStssPtH!PvMD5kl zdSpGatZW_cQInhM2d_ezBBnQf{iSE-bt#obr{nTqJ-Ht=Qw^47bjoeu9V6NDs-h%) z8B=Ya9J6=QEDG}DgIK?XDIG^as^1Ek!dj}?GKM?Wn$2+iD*yO<6i)w&xw_U zz6*S({r#(O3*fE1S(K`gJX&hxtHIKBXEJuqfP)P;QeU2L#sj1tc&xs8n&`avhbi+d z*0|ikue+s1RT)eQ_#A!>kG!I7%oP~lLsswrZrRsx2r{c-n;bdTR6)O)Jurt4KONgYzj7yTu4anwo9 zkIJAfcg*F-A{}8*6C)F3WMp3~7MDttr@eWUI{E6%-EcnTR1S3YwQ5fP{z7$I+n z&nnN$bp~{(5oU4$e$QIm<3tx*R~ZI9lF`W9m#K;+(nnJ<%MPXrIxCtZ26T&_BuUuy zx$yWLEX!8I@syYI-E~rJZzkfV;-W!d3VdHImiMf9{v_8k)j*h&!JsF^d;C#cLiz)rs1lDuV+4_!H6L?3PjHhqpfm`ZJOv!bG^Je31U=8{aK@Y>@PnUJN|hsh*2@s zUah99z}`@$AzGdSI+^Uf*@s(W1T2seVbQ>rogMqjODhYHtNm>G6hydrcHwdAel&K5DsGp?(+!cTjmRj! z)CNZ{zlK zZKFG#947d?{d$e=gK6^-1(zBGD2GV$6JTAD^4E zD9Ke19iH(etoY}9G|f!v7=yB6oUg>wIL0SbbW<3?=l z-7qwEK}@xIp(b|jJH+1(<$U7Pwc1PGw^Y}U5&~T(){d3Tlu#(vdnYHB)hn6}Y zgQ925q;tB2)_rCG!sqXA&xkf|07B14Jr1YT8rilt%4APbb)`Hn2 zw3#*zq{T|uzCA}pM$+fb@%E&V+u>X?8(OIRD9Ycp9bUwsURn5U99zc|QtZ2-Pk7D@ z-GPz^9-?`}^2T$;)5Q``-0m3|vekCWaMCe%Yff(G8a4$q%V7;GGWDiS>~`225rhWg zCzvj`C;2}e)}Eyl?B`OI8KELO<)(6tV9^2CJF+45}e;6w%(tH(-CtNMEQj(kZSc5MS$*0Ca z!h37K87zUaIEx%LVWU}4{2+?>`tf#A^0xk(^IlS+Y~B9tSZ6S+;jm?F=6t%Us~C2f zY(j(C<`PhsZU+a)Ssh4*S^e=cUXR3pcn0S86}>%w7vU}aw3{L2NjFEG0Td z?D}9eLj0E5WHz1C##vO<{43uzCz~@#&o3kb1dVE|?e`jxFcR+_gUU>h8PUp?0XXvs zhr>*HAB{PS%<*0s=6>IA^)hqishEBUd?Ao7OQG!ua%DF`LV~PD@yVH59EFGcSO_eC z#ag3*ebzchmiM;4b{S!!5lPBtzvG>m47dT(Ehg-p=HdW0Dd<(>ZmZ6#N@cuVyp-Ss zFv)2@Of{V#EP1JG)@cC?ekZxIc*UH>awBsa5ECC;4nwZC?JZmeg5yjWD~QyD6I4pz^KU(SF35+69452nF6TZ{sSXNn zz~VJmh}k_NRy#arW~3IM(g5Wf{XbY(g^F3fY1+rEDBEy^3Yhjt)-lUe&7Iuf{bCFy$qV zzhBRhV)+Qm&qco`)&#qlY>5OR-F)DebTCxF_1f$hS8X+BOvkvk#HenNFix@Wz~w%_ zV=JjVh?@1Mk_e2@c@NIy z$S?|~+3fVlljx(a94sJr2}NRA)Z1#N%U_E1sUDFAk^6WJ1vD#+Rk!$wp^j^blACU(X_X) z?d(TF3YAbHeOV5O7;LmT0DPzt;<=aXUC_Uw^6hR5ie3T+E0I+x9JxSz<2o=ictYujhPu zWzE(uQKS|i&>iSI?mIR=78L%mfcLacCmCv^w#@EJ>r8rytY+st5Fs?-_}0=fxZe~# zB56dYFhGSyyIO8_BQbD_#cZ@0!biTfv6&QJPGjPCLPN(v$mP!}x`isqo=?9*l{!B7 z@Kr7*F79PJwom6(DHQ>KxlKZy7hj)NC8EYxpxinyI%t;QV|RgXB<=xvg*xW1MUr4o zI2`R%3~%)fmV55}aaEr$2bo{Bw@YXP8X=)hUL0pW3W|zTAZBt^{dqnWv;l+xb0vIj z;8ZO13;xU_4rcOVfku|AmTZI*0@sY>sVaDI#>q)Cyio-U-TM;un!nP6Fw|KjiN{FZQGD(Q;z)~9KE7)O-v62dbIsDB(W8(Bn{ zX`84lEztKk2{$_1ZIeT-&6TTjSZ(!Ak2f(j9=t7cz~2`an}+S@->pGGet^yFC!$Bn9+*qS5W9fJQ_%mDFcQ`Xq$_S2t`O3cg^ce+}3fv z7umD>#r#9A+rYE=Ex>VZd)wc7I8!mn_>N|vveZH~XQJejLS&77$&5RX{fR)fp#!5- z&WzX57CZgBKLRKb0WVhY)EVy6Cz3`dDl>V_ASL2lcoH~QR9-3hnm)6SQ$jIGCEPh1 zO1vQXhN7L?=0p|3x~D(VE#o&(n^3lRY&|EtdPl`S>gw=SgX)by@z&MW6QT3f%hs0~ zhXu##3I*wrikX-U2pVpnKG_y!9;w(41x}4$oZ6j(=FDJGiFksYF?->y`Jdff8)|;? z8jVfd*!W@hip0t3b`{Qd=y)%776tzy;`sAXt+vBUaOs|4$xNmx>Di?< z?Fac9ztm{lg;R!XJhXfVUm2z@Z>QiC7d@%4&jl;bdaAp_!cg+fbwVfkzisLztOh3) zn~t|tjqR*1HX(GGI*a=x+B^`JBkTz#oLK_){RpYI)A}mJ+U6=#JQizgq9ZCzHM6!z ziW2ee(yDIyIe(cvW=8}t{r3f)U^58PMPiZ4NLch%fEOkawN8Z_aj`tQXcPD4MufCJ z+t$v+JYzJ5ob){s!Jk7~fG3R>8Vmbm_tYn#hd`ajyp>7(hlzoQgC9 zMj?M~70x6FD-?6)jnMh86X%$Bb|My2xitS>l#UR2HW0<6c-!kcRdv^$KZRTs{r7ql z=!-A8gStF3!gfJ%aq{Qmb=le3^JceaAC1EP{N)D%@`jn=hK5?d6~tNH_UPYkepDi6 z(qft_#vcD2f|r+=XS42x2FG~n-=-n30pq*u1)A{w8iLUXnEF2&IcTRnI5tYHBi6%W^%{ zWQ;SDGcPA|F{JAgPQTPhctA)dC!lBM=Fa}vFA3Nn=Eb$x=ZEvrYkFn!ZeMS&>#rel zXJ=>Q$3bRw$xqL>hT7kh%lNZr*UIxsa$W|&yS4?{$aG`zT4I?=9etaEyjBn{Pk)9e z$LB-T^hcnW%%H^DkAG@zG9U1JkQ3zmGNd}DR8<5kXF8zyJw8(~1~VA+Nh+&EEI!Ri z%mkY@(iC>&w^g4fp@GekaJsuECoA*v`ntL{0g)0o7?^2Y89>B!czj%=(K4&4iS4nW zuFlBh>Mb%k@>mkzu9<|e>c@76q^^GKCk*orgtqtSdk_1H6ilBgJS((&8qkGU6FZ)s zE!1#nD2=hIKNP2$O2pl@L)N8_E8v&84lYO&6QbI2neAWhG^9Iqy7=C3CXTovkv{wh^A9>fWp)ROcrd94xwrZ-X2dw{9J$7hTkl`F!%NlI+6twQ@vx85?#{axuF@6J%=6Xet=xj z#SiMFP5hJXL`_-Pn@Fb|Ss@{$gWXD9|BP_s_b~fi`J7dZL@W3otbDq9&>6=2c6+?T z{C=iLMKmxlqD(}qE7hHwI-96I6}0G(L9o*$QoayuQFJU;+IgsHDW9G|r%(5+%J_KB zBtUM_zjvY9-ro!D^5tM(bAMQluhA!a?0~j?I8#C*`N>%GECiRp)h%hYF+{~Cvp=Qj zLWz?v*}ub|CgrhNML=gj;<=j$Ek54v7ZW zdHZMR7kaOZaW$cXv5B7Bho*02$wn>9C`v@Ytl{-4v;pihw$xy;4(MxTiQNKuIn}00Q z0*)N-nUuKK0K>d?{)%#K8EtiDh4PxNteD%1jia-tJiA%TC@k@PCqp{kp?Lln<@_xw{dKqVwLmjhb_cX z9Gq=EW6w1bgXrYud@nY|K!I2SsJga+k!yiGjWC$)g8Kp1S0d-;r2slzUcSr->5kok z5_^Sy9vWIq>#?En6BR)teXnbHI37nrhTUWR9F&b60`%)k`zq(>E*Xl>pEu`iRT{Jk zBY3c_(JEkvKB9PkK5-~0@$|2lkDxHXtm7rybSwk(EjL3{~=bVrIZ9O}69rMevW`F%E2*0`ddSv>=d_m_x zf{kU@#`DjHQ6)wVGyYlaXV_8^Z!bAsTJ4f?%wd*nj;zYvNt>tIxI3g09TKCIadr9m zYVy~$@`t{dSak?GNm&I< zHHFJVQk}1-{3g|Y1qKk*>6;OsvXGMbOdnU7)={r~dR|g{u~5iDF~jF=6t8kCtR5k zWTSP|yUChSP|sQ3glD!1Ao@8!+m)tzVsj*V)-Vtq&rJ_^XIE9dr4PCjav=8&>qD|A z<*_f^Bvm9C1$;w{e%=o1x^Hpd*XY0&M=>#MW0Y3ZZdTHYNCrmYuW&EXmkAS+5_l|I z9qz{_rj~cDF;P7o6-@|Zzh z{Ve|yl#~}q9G~xxJivO+onX(|klkLTSDO+R&1`72s3HogWx2T_uYlCVu{tNa#pB_? zabGg7ITB#ZhM&>k*o>dtd0YN&q&Qd6>}KBpTn<4hTLSI3*Ykr1nMbXE`AH$$J8?io zTDlTo;s9JIIIigTZOvXBPIe8LjeNFtwkob@tl-a%#IG~WvmV8thRjS%U>_^$3~Xxc zYL_>pf9mdDU~cFqMazX3`kLzUo0nGN7Zg6nBYY~4M5}}qD#vD^l=jgLuYl_x$crP0 zLSw@j;bx_rml#tWg|x~R(NRYj`^Es-N^H&HmYZT3X;r3hsN}FSAP#@$St?^g+eao3 ziug5HgeI6h0qwvDo~C-=_UJ6;Q;wh#-y{VVqREF5V`;%ufl?Wj&i?Gn_dSGt?HuVd z3p2rH(g*&+k(8cWOIalh=7ujfi;%vf7p9E{_wg7L`{>9p3G49nHZB5IoSI?$ z{1Y(W5}U@K`?3}K!tl>4X)qNg!?ow<9A0IVr-zJ`6r1&y$y61zr;j&=dk!X*G+Dmu z%|+owe{fj|*4`N5MEV?4GqN=m9I%*3H1(dinH_26Tf}QJxJlqR#nN{bA949y(FGL@ zDOh^}NYFM8E}KF`HRgo{YILLMI)7;>HCExizF>kZT2gcN?q?`B9%XcWzRrSMwMCT)IJVYBvBBb3d*xox$}C?F#E}9wfA^p>E*qPax}@U z2q923lE+=(9m^!2ivNa(4t|UZTMc+Ke{R?>-bf(^KylLaMc@&NBya@@*v2yO0=6DZ zNKnw($!RkP!x~qAEEyFQ6`({~49#AdSs+T^spDv2apIcE@wv$rAi{TCXw3L0z=n<) zylQb8TRt}XqrtwA9)y;WT2Kcq*k3FexRgW5u`o{sI5p{gr8!{mGoCn3lRy5^;C!_Pm2u#G}RUb2h{ ztR16#WL4Kg<@l}$O~K%inSv=z;$~qhJe+04vTrO4Q^W>n$>llyoE^x;4%kRWB{8_`a|?ri|nn1(9na zdR@aS2n_~#xY9wmR10E6lt_!_3)bGIsGC*>g#+6ikrcrv2(C3k&XBSc2su%w75Skn zs>Hp*-?=$U>$5Ob?I!uc5SccsidB$ASnMb4T{aSAE*+6pA9v*Y?7_l6Y7ZuB+ z^31S&sSCvH8tfE^rW-8j$~!_SCI9WZJW274nH{`J`5VkczVkLjAUS#_U9*=kw~y}$ zSa+Tr42G{yN+iC!nhVpE5}P_9K#f*szrcuS;ad5~q9T8%62dVpVVQz*7VGE?X+EW% z9JILVEsZ4#d%3roSCeDY02xev(>SFK#PelgDoz9dp-H-XN?U}LfEcisLuHQt9XpQE3rgvTL z3L7E6xH$ON3jP_~=Rg2HGD*|XZnJ{k3|j%<^qjU{<-I!JfuA84AK=i z`x1ZtzysnF@y>2&qa|Mr@y|m6*6r^gGo=5ck&lk*))%fv6A28?j#}Xw!W`C{&hTND ztbZ=)Bp|NbE_s`on<-S8scT%QDezF39ezARY?Nx&-LJamcCUP!c>6lDGDRs~TUrdq zeE+Ga@N{j)PsTvI#$96YGzdJD6FxPZfSpNu9wWC z6_J!|L~xVD>Eo?Hh~(Ts z%P%MX3$_oAFKbW58L>Ahy&MkBg^kUFf;!2B4`vNSbC zBz`#Mcd$;Qadfs>XEZX;JGMoR|M1l85pHRRy!Pvc3mJIVGts!$uP>(z;zRMdH4an9 z(>YsixUuKk--4zT`b14^a*~_#sfxfGY(<7)U8rKVBn^ftf}A>f_jeLjY%9N{Ask?x z?SsGsWXsW!?$afsIc~UqSfPaX3s3zzuKu%H>FV@n<0v*NFe7TveyPE`u&-tmn)V;G z$rpUoav$eT6ZtYVuYO_aS(05MO&$?s_S97kl}K7YFf-Nb$s)QV6j+!Jg=TYa+Krk?(et{ zi2EEJ9b36q#yvYb`(ibFyv^G!0cK8=%vXc1UtntoFy z^1@MOH&ByRxcsR(?Ms$QK~r(k+4S84JtZ}}7&&c-f&;EM{`=f``UWwljD(M&!CjDV zcJLC=yo+gdMVMce5*hlE`&8qw?DzeG^5#m4RiABdLrGKNTBt@UE{2y!QJHi^T~crz$8;IeB3LN z?jr^^cF!~^d&`cn2nkXlT9fFF1!xw8v^u)<_Ms_p?V4TjqceNX>dE$W$VS#T5|e&V4JQGXKDol=j_`2Z*=)#BiCp)n z>sD)D&dnL8&kJtanI)|8;Qy={H(=iVeriEY-<4&!oQ`Vi>u-Bato;4`3ANx0@#~&v z=c#GR)K&%0UbM)aUmr)BGON1gzSL7h++xS1Ihkzxm8HG3K7@eDjnkGlu_*^ZUoI^z zk&^a#>F`DVl-1P4cYD2_(xc-`6VG@M)-Vrmh@ktWQd(w~&{l~9s=oSFR4fD>hVMZh zSaxx-szJp!Z)86}Ng&XQrhJ!wbYeX?G9-=#wfb!0?Vxdas`_j87#vDU7hQpuZx9x} zn-<}Hp~Zj)B^>Mb+YFI@8vLxJxqACv^WJa(LGf1dX-X+#WGrQ=ARr??L4l{f3>oPV z!E3Hs9UL7}DN~FL5#%0c?_Y#N{j}sbw9FF3C?EO`+J-|IA*nXr$;0gs@M0kATV=dw zp`}d`h;)68Va`mt@UpWzde^~@i}-n~YA7e{<24wJ>p%03G#`PX>0m@lM@a=BwG=0R zKisBw)n8vf*A&5Bqz{#BYn#U+dO7!9+OlV%m)|Oo-+IJlb#AWS_~G2m^@Uu@X<<-l z3HfNipmyEwfKfg!4qhtbWfwhVJcAQFBlrh@C7sI$fo;BGL3I?KNm5eEaPtEsJ~#D7 z%*c%Qhn}j{P|a0MyCX03jxD!&MiJt%5DLv!oiCOr+Rlv`SIwx5EB+#k!ZSE%WDtgB zh2;ZO-)l@Ap_1?|xbHSBdN>iUyS@;y5%1v+;Xu91YCp;Uo|Bj&Dq-YKAO}{?ZEA5V zfFRVble_usx=aA^?%jte5&m!Avr_js5AGfrV_F3l_y_j+2kva?qW2gLq<+6!7?WWA zVBqBB>eKTqp3*!xpEi2JJeBBiqO&|A#)YWNR$*s#A zLpmh{osZ5n)izJQX4mC`M`3;U_m{fg;(p`(0-gPJfzF#ceqB7dm^Gf6AX74-6W6;C zdnRa#&+v$n-w_6(+jE=$HhyL0quC^!0mRwe1!|B>Nc=Bd-jU`S}`;Od!OJuSv?pO&O9iNIfjJ~SU_wVDZaB9CLbbbO+ zFN*zcW=3-zJVnMF=cUn;thyw~zCMhL=$wpoOxyt$gtZ{yQw!Z{&W*q^w=5Ed+efyt zH_@x_;qkl#1qoyN;MID6X1Efqtb^*34Y6!n;OC@`JXW58oG;|KizW~KKq`DIptlpn z+IXUdLyeloLYNakY=kal?c)NVz=ToW>Oj_oY`e zPq~z(!`*IoPP}*UuF`UtB8#jxo{CcTK2dZe+dzrp^i#u3@kBt2x>b~ZuZE5FgFCW$*6wEJMC!LM?Bma)*I>@)qyGSEOPKVD zzN@PXKtn)H-yUwj6l%Aow&8-FN06)>Z(d>tbXH~|>dePjGbCWO8x1{AzxB$DI8mx{ zorfm)wu8sX%-{3S(?c@;@6?Ti!2}k&o>=u&j+v^9MC<&tL~_5G{Oty-Q$>2L4zpTo->LApTf;sE1o9-Pz;sx_}( z88Hp$#5ki&eSm5`_Mc40KQ417+xxg?FtQB>`eRlmmVxyDtt&H-mC2uGwm8Cnp#aI` z$nO*uCdJrawD1nvHU&swHT<+9_zMmqdFYTcX%F=>nS}nk$PB;>tAYbw|FzsE&eM!9 zG^MuKqEJ>^TI;;5e?A^L_dB=+q&4W@ulYlq_2qT^&lFWr2Y)e@(_d`(8-!^hXLjUP zRJ@z`^LT!@J}Cm=tQatln18^`JLqd>09CcOgM8)w&pvYCj3mR=?t(>X6X?t96r|ytmNtGX?LNbrdwTWtJ+Tx+o2-40+eHAzJDLr zXIrQP<9KbA~tL`7#+FM?rkUwZxSsj6*FJrPEvj2wX zB?b&n+tk$cU^1(=wzdj4V8s{|1O(6Z;@kK3+a(jA!8yA$XUU7mzIt zc0;)SrVBYY>8lushXMrkfKL6dRa0LU4Kn0$`mp}Hci(t;a$Z=s*b&XXAqCx|fwKz# z`t=LX@+{g%7IXO_}?M17^q@*kj zVGZSaQ)O%G>QdbJ|I*qJ$nV3RNjV(OdiOMh5nNoJ=ym!B%h#8dutdRw2!!?gA{XjR z=7DNs!sYjRx*5QBXt2vp2=GWL`s1k&fSql?Hcyijk<7f;kY$yiFJ5FE94+zj@lIEJ zU9W(+4_F=u-AGOZ7p2ONKZ(%4w1UxHtkyG&TsdHyNu~uOk9Av1IOooH@bz?-<{o<0C)F!ZOS^ zqMHOButLpu)pCCSYc~9$whJX%)K&;Dt!?=ec2o_xNhnlUK*{2ffaCX${Hy*m*1}5z z=7|k^7AGmJYb;iS70D7ZEGD0(^Ykq(5``=KI|o>L+V6LmBL}lp5m;6_zep-x2~`7} zBL@x?eda#$5;j01padMCY0)#A?gh=UR5GL?WIAx#u(WVmMc&>P=Uq+>M}9QEJoVhQ zTVA3r;YZ`eISl{z^31B&ztnxtHnyqphfK-W2LeK`g&6A+cC~QN_3Sya%cVhnhTg+R z_8Wf#50Sc{`X{*}_HWK6c+q=cufm1g`jdNS@Z=(bIg{y3wd|$o^SeRgjX=~`=jyP^ zTV>L^WoN#uRxL1shgEY6(1j@}DLj*|7D&QHnP)b%c&=yu`Kj<7RLo!n#~=G=<a-VX$C;fPmm7#Dx`sfPkw2$Cpqb-+x=2(Sd*nfFy(kl-+?ZG9kUu zhF+e4A7}{)fAd3ylaZ7icVZY}Tc=@1M|nM^@7s|(#0Y86YVWbv;x75uIzU=`UUdcm zDf|XQIqkgjl_O4G*w{Rg4R)RKx%O~RE^d}&IVrK)H z$nbh*^c3JnLIRV9f>VQ~TFQ<7DC5HJN{mtitGJ`ZzS^ z-Mw*a6oVpma51C7mb!E`X^pSSdeNBdVntgC7Eq21tfgEs=yV@>wqwl3mU7pXb;Q*` zdG=G6x58?~4SVdfef-K2=ep6rX^!kD{Y}MmKW@uhGaH#DG3#}izy>^k3OHc3)c%O! zX-ZYsxRWd7Xp6IA`!FEbORmk1XE%3{{-SYuwCzn!4Fmz+u0Lz?ll&~#<=i6 zW{hh8a^{*IeUi-rhC1o!B6xt|r3n6hpJBFN+0Y}ZcQ}B+zLHqJUClha#|_)3>Pl<5 zP)5U9U1L{+4eXPX>TX$M>B;X=t&b;Pa+fP{uz?~1hGvY`{>7s`g;(!!`JuHkXzc4+ z4rIKK^}8J1?tZT2D){8QZ0$D>tklZmxk%{jc?DnTdwa?AF%Xkkz&YsQH*+|0u?zUc|iVJn4Tm8Ul590Bu3}>biD&hI)z=4-=<$^EbBc6#Cp<6CA-XaJ# zPcxa^ti`)|gu8vIR%In<>40|m)Q4o?;?dw`BVRIMvUO5pul=gPYpJPF2@=EER;P)j zcP~APw|G+JY(A}G^=zP}q5q?J-3(#s3RCA#Kf7DyRh>>zy(ciK96W2xnaX^%lZ5hE z-8gp&xK^j{sv+a~i&mZCTC#%s#pU?jQF_JlMWcB;TPTdWcU!yY5#k>Tjjpx3vBoae z!@edwGN{bcIfjl%a{|ta4vV?FA*GGO$_E293|ZADD=$G70}3b*H8KzcuVA?Rf)0vi zp5Wmh2bRi60>a`hscD!(u!5kJix*GNTaA^-7>)Q+{h11WZ8V=~dkB*k9c5R_w9PmTVg;fdXUyc+oA zifP|kuD-^9GHq$sd`Qac8;#{iKVWo=x_Sl8>dceMZhP7NGab9Udt^7^YO%PVaL~0D z?YYiGhG;r}p1Xaa5e{7Qm3=u3^H{y^ymXSIJFTk!`{00cj(%7;3;ZsPP3G3 z!l!&GXMvEv)6tAfxaq8=y?W_ItZMAxv4k*ZB>$+H?P#5WDEsIiWW+}kzDsq>wwut< zi%3y8m-Ffcf4rBa>;thdu-DgfhXYRpy9?!p@aTWNYC{lY_$M`5zkBrMtn;Yy;o+vj z`#?=2-wVwGq*1nni~?L>`;W+Wp4NVBxLLhSvz~gaL(%C$YrufX0?8Qs%g|bVyJ)hF zL`%!@`Oc!TVRl~V7OX_&?A=QBlz?bSXZwY&`VkR$!bM}V6Ulpsf=bBbDfvWYx)mzjbE zQ<%%*aYw_s>9f*k^}9hmTjJ6TEO|*t(U~VkXZy6>h-?C^s)7Yp-(9ou)F&kpUIAH8 zd!1Li;@`ZSO0lerL$dF{8nB&{-bzwuQFUAAG3+HtSe-dKVt7I;OY(a zk+nkZnA?7J_8o5o$c6bswQKqzK0CG_kCa`4FHfbFe2COsuF)%mlo z|Lo$8$z9W$^E@|d{_(fvY5@V1b}f{8r-hA{wz6gdiKV0!xA;T2GU^({yte=%o`-{M z&aS|ups|5KQkooFMfphZx@Uu@ObBE7viBZd*;JVAfN+YG>)&4zKB`v_<8-!?AHgtp zU;Pe$AQ!?>ON&}Ix7G@3wTvyy^$*fMbTz#vaH|Z>D#aE~hiEqcKyoMtT#HG4pdprF zy^kXK?F|LZ)?E6BHbD%Olv;cB)~kZ7vPp=7UE@C>G`cQo0_%S<)5-4%{Ikr2wJKBg zS#m9O?~QHT6`&j;xuphf+q|IToNOYWcg1j2MQTAoD#=&-iKP#7rpQQV`ZNxKI#=Xu zZVla`FAk5i63&c@St_>zjvOksse@u(5r-D;!RtQVgE#0$~^{Nq4!7FD0FT+p~ z39L#TjCOMbA$3?2@|L2_1hSH&x{Sxg>Cwu~Bp5;!hx_93!}N|6N@i9wAdIDYcyX0# zM0gC`3MavILi!7iFSBp#^Ue6Rz&ALUR0iYfdR9r%KPG%piqPAGkLA{oXjazhln;Yi>n7mjKKqZPlJvIegF<0SQXPT43xRyfSgJ5KY>seO z1(sH{qqsKom45eE{Z^k%5fJNe^P!FLN>3gc&04VV-_SJ=nqPNMRk~X8b!Ln@{b_p= zyQw;nb0cTPN38V);tTsr2~Li%>AW}Dhm`vNd@I++DW_z+b#pV@&cU@iy8ZC!{esve zMn*tkx^=IF9Orz?0rxd}1F7XxOz2z0ac~uVpQQK$tjZD$r=@zHT?-=l%TtloL|2pn zPGq63?ol?}kxIL=`a-j>`KE>(Z|J^v;fm(AY4oZ%Rn@BzDv{Jku9>WpOS^!{IMAEQ zja=8eU#NO<;lSJ7Uc|F1BAMOEje(-SegDmU;f){Ekk+)zP&4ImmbCA_j8^K!IBhK? zD2o77;~>&V$Tq-Yw=amUY84wx^-O!d>_|Ue%RwWdSI3gXAapR*vGGk-2ksHxD@9UZ zn^Lf+r;@3jm$D)Q1E}<$oh7Q`X0Z^(S=klM{a&@qILX4!VLx6o98HUt^ro7X&+w=W zwsKGBzd*Vy9ot-;@wW|8yB~=Pj2%{SPIZOhyU49z zFEo?ybP=dHIyBAAQL1*M^DpI(A_-qQpu!zRygRuPGbZgh90#K?RMq!hiG&BuFezp4tCt5yg_zgQ*SMRQc{P1p_ zgq)hes%e}Qsq}PTar*SkP9=uPY9TZeNJ{4;>?+Y}ue)OPxZ^Y* zOl+tD@m6&=SV=?Q-17eU{zZ1MP1C3PZ-j(&>vM#U zY0GkW@NC+5v(+t{dYfmckgF5#-RxTH$!wF2HoF^RKPqFX8+zL$)8O}`1L^X6iXEuC zT}ZRyxN0noj7Rp~WH>f!C9T!7}Fe~Pv0`6JGJp}W2H2nLnUVI`9< z#5^a)2M#HYIoVJ+4UZ5HaVdU-AF#YE_#_&@^tB%*JGXQMA4u9zm2quvt_{t5@(PNv#jNZT_r~QiS(T$TayHTP%)}zpq5b)LZ+A=nSj=fmh19$COoKb{b)kuC7MqA@;XOpWgH3Dhh6hVnl-i^|k@~?3XfG*hE z5O4G6U93U~ijo7k#bhLaaWGAFMd|=}NEtE`WulY-;0O$O;`;`W zfPkT64;a7C3hlg1*Zpa~xLH>(rVMMZ@m|*#M&hh^5v%M)8iQ4gaJ)IE- z7+8QMn6{3#<$GQv5b=L(2bE}hb$oSDTox(>P6N%bPe4wlf%@qcjQDPHf`5fOPr2M+XJ+8AvS(N2ee@M|Z)#O2^}yjv zX7*T|j-S3-uO=461C(sb!%-oItyit-?md!>I&$(-Pv&aFUH+nRWU9)_uTl4~FV7X( z&|tIG<-_H6nZfJRR$N?spkb+gZ>PCnAu2jprcz!cn`ucgSbBbb?s~DRkrj^x=#U!W zoFA;U*7g>bi zjY4cV+D#|0TquA2GM_Dw2r*Qo0Q3d|;r8-X?j>IqgWbzc{OzAupz8MO2pi<%%=oOY zm))R`w%;_Hj(tmGAGsqNU(NUWikh3tU2%JFjs1x{Lz6z8jJ;$1F3wj?A4XayQDak6 zPY*21*(C)H-fuheLq+%RV;MFpFQoQFm5NNTyw4VT0tyyK!-q-MlgbY-_eUQ#{qJr# zvN95MEe7@t#pz=)XVEX7?R<_-F$8>5GH+8E6V!is>k=3p<(IC(u~9*NxbZU9J6pTW zL}h2pA`lPvQ@~z4xH)+6d?00w$F&??2-*mdiA;w*&d2a(&_$4hp;&GIo z`K!apJ7w9mchvO3#^$`Vw6v|FNos$a(yaz1>my+2@3v)lIGdm~ z8yIdvUBVG%FM%m=r6!}V)^8g_LqB{jo7lQHZ2V#G=Ct52n2?x$ zy#(aq)2+9^Y->1It)-*j?Y{XnKP&{U%lYxr8}QGUY}%|iv})8BC9R0SRmw>(W6MQ4 zTP#+X4J8}BHucAoZ)rPx^gMPNY1(l7U2U~-O7Kn~<9f{I$cn#vuL$V<)J0E98oqmd zeeI+;4lTv7<5;Qu7!cnYTy}UhG&Ic1&u_L_6OF~jva|%Zi+@Y!_F!yKQBVLri^AcE z_XLB1g~cQ|fX8NazF0*c6y~rV4kbua;^5$rkdR>8@@DGeay|EFQ&Lo{tE;nZJ0T;n zC^58wxkU!czyvM_cKm9&j=MpAC*COpl9Zl2gg8T+$w)V|Z(ig=ogH2N*&%`YPWSUq zIv;zFmHJfJBF+2tkCpeITNmbztlnUDzV(#3bt5mt#r4b;3XIOj^0xY8ZXMyAH~0~f zqtCip=mCAIUZ`^%1u9F&NNh_HrgJQ`mf|@nQuRejA#Q<|T43%6Xxi&rIuAd$_ZWPX z%@Elzj_IhT8p2JajjvBLCAKO#6jg0^U-OIWYdnOjp?-m3DOCQ%O4J;%lIaWdnKZ$$ zT~Fy-8K;Do6!qB)Ksd)jKy)nGrx?ehbh_L7i*ZdfRag1&CDStBDM zF_+oh+p}r0Ln5`=k!NNS)1-nSm(AG54vh=LPO0zc$f#*6DKS47O#%l8A3h5%q=sUr z0jXNF4H>5YQtd4|UTDboO1hF))IE_sR(H8;X>CKw+_7;G4=xjiLntj{*w9CV;^SG@ zc~u!0@D62pnjt~9cXHb6A}yLz%aR=<&eCaNi1Qof=M0rIX@q{Dp{DGYS|BNZD?JUC zG!cAQITcdMWDJ7A!04jIQC<0ofqpym2GKt=@X+_0KKI(qk2GaO=96i*t(2Hisb%TTmTTPA zIf-jA+w&+UjH<@fnhoZ;d?J#zyFJGvLR@8|az8F~>Zwk8(>DPcrHiG%%3w~X@1vG5@phk{8#ZIUj5I`L8FFbS$-=vV9d?lP zlCmNy>l`Bz6rHj=t@M$@XC+;ESTMKu4ziTw%jV<5v|EWS#A8@1>h+*v%iH#uEz8=f zn<`KJLX7ylpRY?4^85Pw68GOkTSQTmMuwXSwymMmiv?|oal7RQFIrrrQ!se7 zF|hU%)YNa68#Yylim-Jg(mwrm9@UZre05`_PaYQ4)&zq(#;7Kzexid0P=>V{Y2cWz z)D`00%-;T)cvCXvV?PhpN1X8cfY&iDPX7kN;XeBaGtauRdc-3<)cmC^X5k_=1U+6e;hM`O> zA^!BM>}RcILx>Ui@W7Pq!s+fP3JDYk%A>CGLIL!==xB@y>TGaYFd75`&hY4Ml}iUqMzN9=cdas;ob4c<#JgwJXbFFJ0D6|8r-KoZ;>V@Znb(@3Y7%b zvvQ|NIOs4)EoJ_1Pi-k86Bz?ybU=Ee+2RDlKke+0evzz4tRMHRT?CUrVBwpQR!0B_ zCKu-SEf|9)*9oBp;D|%0d376RK8m6L6k~Qs5W(O{hABqLZ@dx?KzlOo`vu<;^#AL` z8DQGjzzj?Lhxw{}z|;jcT`yu3MSuMV)MUU&_?w_(GSC3v?}(qjWSZkpL-ZT9umUh+ z!~ge*n%Ad}HAkvNlZwgFDQ5>4=tl%hTt?o(;^`tvj_+#&dlvrqk4e$-=$pQ!#;Z=* zexAK|esQ@z+B+VW&Yh5>Oya0}pBICQr21$|-&iMGkX7TVDl3~B@4u+O(16NF!$kc7 z>Yi?Mzq#!6l5;s<9$h#NC1gLlxY#*5N=i_AzMJA|cedClc{Cqw^lePjgdG?t-16&g z6oT{(Jf97wThyCAN0LHUio-SAR@MU*UwCi#9^fbIaE`3bQs&Jaz7S7B^=Xr z%UZroS3x{TbgSVOnZVGic$v#SoXTv!EKKRzY&hASqQiz+YP}7{IK0^O$(x+0ehDco zJh1C*Eb)43>hx@!^Nm=uoWQsPDE6U#huV zg@?4VH*Y07BjaV57+>>D1ht+_U)}Fi`L)TWXp1s=>OioEoB#7J`KbO+wTX*7m zLHFg?+&NyeMw-{h!;OGIk=_TFH~RtKm&Ol#{0rT+Y6}{@uBYKRCPW_QNyYTWw5<=@ zI4SGgBNnK11_=K|y|< zugkMSW=}SJO(*#sG&ElBrl!()efXa)? z5H5RBJ|T!Pq_x+3F>6iM9GdFtv!zPqV)&W<_{J7fmmB05G^=rGX{h-459NCBVo59R zb8~mRFUBUix(u;_67KF!LZJ7o<+pxc?L?ey2VuxX@Q;7~NPaywXJ>hyipotzaP0sO z)ELyVzllX>;4+e!-XB*<|4U_BK_$D#VJV}kgt|2($bZy?(X|! zEr^NqW?*l6ukpXOP#vGIPtBX0>M~E*cGNAOubbP|Sa_U?)?^hF|4bV?pLd-tsmX&S zs{#=Dw4*}+WNEt8(%hq!@k$$-%atVL_j99-Ro6=DVkR0bJO~TOKFbJIqwX5Gn3ajU zy<$XqV}BCnSVNtO!A6^qacK@>y;8XQpPLZP)HilldS%Go0`+bU5QON?9C`J}Z2rDTm znUo*+v?x*rE`RJA7+_#R5(Gb*w`@Zc@x81;rtD~~E#c^vqOehAGsO1{pv$<`kP@a$ zgmWO!i%fLa13w-Fu70u3yVtZ-c!G0~-Zij9xSiA2j)NjbHlb~&T%e-PF*4q2HC&rD zqcXn!Zu5S7^J;HxwcYdvYdYQPCVoO;TUe=IfubU1$6DVpFbMF>e0z*DLdYfTQW;AQ zFwW$#S`603>*_!1w={H`9rYr z*+Jgb*B2TAO#-OXae?I$BHBMDhFplAs~ne%v@n}D(_=5H6O%fAG%4ilq|oikJS0EF z!U8cGF`rltCUsZ%SFq_GILxAuII>|45mu$%T5y-on$u8Ox@&*P@7^Gp_R_yT?vio$ z+XzqB_Jz`LlYH9ZKQLo|e|VG`@NJRH6u$Z3dy%Ilj)3cO4c1KhYifKvCafeHIZQE^ z+%nNWm(}*Cp-LY!X}+yEw$8LpvFuJLLLtEj&-uZDY{}%rT)r3y76QJkDXGEOkA$`1 z11a&z1qn>1MBkDhPe=7rizWTj@_s7QT@1db^PkJ0shciK_B1*<&?O*gtrq$DLJtqh zeFU#qLSC2MesBgDa#(prN$?E}x}8m$WAaJDsT4|)d=tk!3C=gr6xxd+G;pX>rPd`j>zWqkbW2O zMt>Mu-0UW#g90=Ak`n@HFK!8570Tf0)F>-WfZ*Wb%S1&1qZr}^+^Y3Q*xB{}@dEVq z2ZwTc4ViPd+)PtW#LIu#S5}~lhx)wVP`bwsHL8}<_3%Dz@x30;u=kY%q=mcXrR;-C z#VI+;8x}_=SQ;VgPHs248v=u(u31+M&`GIWf6FNiFnne>$+IKrfLjmEmM_Yo^hne40J3eZbLX00#&0w zo!Ctn*++p1w~5#a-sLyNYNZu+ob_3&BOQKZ{#*5&NVJf84UfP^nm-m4e81t8xIj$; zAa_sF>U(O9I&1@5zJu{uF5AhGgrh{fE}Dy85}@A&Ih0_D!O5{@y7SGsz@ET9tzt2* z>g|RDT>?RzCubKF{bU>*BHWw)qznuy2R887DB1}E{rGCo8bZpcMSDX^J6wl}1?h!& z{6af(9>0i!Lckw=HiuFbAwsB2O9;EdtW>atZ*w0(swf>vYs!ujZN-p z-EX)dM4auYl~ya5C|bW0UlbyYc%+*=Z6hZm5W@bH4;J5O6M4TKn>@;TyPf0;-Kmqw zSUyWq>k-eLF{!^0Eio@_jP{p}m(& zn->GmZ5kT;NdrHn+~%DZkGj86%W+fyPDTtWEH+Tyi`Q;11?jn->mDrM+%U{aY324j*MI zZ(mQ{jnqlkgCG_%VD=;)nf@ltBMEPg50*0OEOls$la}CF5y=U8nF|O#fVwQ_P%wT{@h2zgc0|`IN&@Y+RUpKv; zY8gY~3dERg{Kp6Kn}4Ll>ZgX+pe<^iz&56pL2Kz&v05)`O6<%M*xwW>e%$nJpU>F+ zeo56`XL=J3c78uQ_FGJaoEgkl_baFRXbHfIb0A)s3SvG|i0N2d*Tg8x*X(YU7!Mn- zgLca#L?kM$BtO=|yd?l(_xHprfG{5Ksck4eO;s=&g(GYn0_ub_(IquVN+>k0_k2TS z(2ashk({D_G_Mg}a4azOL_<^zFW@u@>lUFx~oy1Azn=iW~uN4~4xo;t2wCzU0Kz`ex8gHg7+V{N+mSd^pK=)fhh z9oP})QLAGIiqS)9v9W@@_l}qZAG$A4Kb3Zp?3&g8qrUv0MDoO|mi(Q0lHY}qfO06{ z+`e-2x_`<5rfg0Prj3d!?=;yS zsR@X(Bp^^$2SwocuHGC8e-rp49Z?!U6blj(Wd(33&UdBAiHJb?IOC2s{|Defu>g0t z=r4o-DC6{S09d~B|DVTL!V+`E**`kU=5Q)1C?IJ$-MhTB6(dd<*899osi>%k#^u^{ zn&NR~ZJqt6sHpgOwH2e{^Y#c22UoGsQEoE4)@+x}=f~Ucahw}UL`-ajn*zA?B|H!W zc>@cJ6E!`bMu3zxIw}bt_WAL)CSkL(vXYvb+S=NxxmI3ff4eUsf;lrY^C-Wwz-Vlm zTx!5MIyP3_CV?CT9snWG15BC0WHvb%g?-ZK^>lf$);z?+?Rx&xO(AG`rz9Q}5`l!2 zR7F`?I)i;?rs;7Q`_yi;!=qZa)BJL8&nPDTb?j%Ff?s3acjasxsU`(xY;?5AdPQ)L zR;P{o^*BE*dF()pTC?GJIF2|sH#bT+IOLT}&8MMOFEB6{GbKmwL=efI73{pQ@9t=- zy`^P4IPR(+bzbpzRTTyc8aleq*T+*RB0kxwt&yRA%+-Zd-9M87x*mCaZIOsRPo@ z*_{r@7b~@>;E#(KIXEzl0|Nu;0eR)B;Ri1lSX`6=Be?UQsbL@=A0LJgF+pYJMBnpd z6EA6zqdJo@6jW5q;fv-i1V+i25AjX!y9sRwK%KW00#I06t*9gc3b~ zF_2`1s|bsTNM1P@C5GfPC&Q*O_kK6U=QL%X%#|9_?t0NT<8@M)^8WErx}WTSsGgUL zgM_rbv*Ymo>>#L&pithSP?wC93O|KrsnudnCOb55=^o`ROP%5uZ_;dH_2Xi{Y=yrza;xv;Ph{l^b1#oT-(4W|5@obARX z-F)IKA$oC7&sTsXos*?y*Xi;v40)7(%+5=i$NichA@AB4qW9FT`z=%P04d(mAv~nU z5&d_5}_%iZmjNFXc48dj)m<9^vs@OxX^+Lr0);QKg{OCD`Z`}_B& zEuTmZ(jwCocTgOlKr>w;FOgR!67i#X!xejyI?}ZG6VM$1A!Vs#Aad*dkgcLbNmfQ? zfNdUb8UrVII1Lk!@@crk4u|l(BE7+@Ua{>udXdy;PgWbtH!BxCo96%`-+GI~$Lqr$ za--cAue)Ar+i@+v1U*S`;MT`w`_*jhfrUsg>aw9IJ1 z0+z%;0(m*GGGYv`24P|0_s^&9BadkMC+>`3$YKx)TB{_9ebwKP$nt7@`y$N7B^fPA zsu@_!=15zco13E(`w06;OVS%8_Ec=evp^S)>7{jjT=Kjq6;*f-sM~1 zQeaGLMU72m*o}urOEJ0K>AA(>b$SXy=Jg=MsJ#kAg&;C&ld&?WbuV*s4xDJPS1l-G zNb&5X8x+IvSh~^9h2zaLjlqG1n{7AT+oqAawO0BgmGn`(?vDk4>-YBd7APJziwLr~xVZaHFvhPEA(-S2AyWJf zK@|c<74U-_3XhJT=tU0As18&CZ9b?RRa(u_m(vyCO{Qh{PpvBy*rBUPEf+RnrAm>* z3Sn?Ad15?*Xq8l{2)LXjfu`{qT9unkrItP!P28#D%=YGp@zW)T$(G2VHmTYVb-*!- zG%n_gB0>VjMXCTL+6&1(5@u%DMcO=aPV8CeG|Kx2G}!^FL$a2*mzbK#jRPv&#dHe_ z)@*g>uU)0hAMo%z)Iz}_Nfo~VD3y-gs(|iBz+Qnxkv`F_MA0iwTuZoaMjN~O0}0b| zF81dBejY55d@{GOp;V~I(()nLlW2tNMKV1KQcFmnfK6*O&_5H3T{>8jPJz64o;=&T zZ)YZ7>}DfJ+=b$9kY!%U>Zww*QIYYt6et87eZtU(Fth=C7L+VfZ(^!H=e#XmfBiHw z_b>%x5R>V6PET5ekg~ZJUv15)+tcNSCYqf&bQ1M2Crn(Cq+LizB<~%+s2sGG6al+& z*k%$HLNuJUbO7{4nNBfNSkE#J{sua2=*GEY}4Tw2ZMJL`y6=R%rGm zoy{(!Z=T}PU^ZA2)hEC?-f;Y2EX4{o461~&3-s#RLEI_HXtueqB0~nm=~A&?X_b(Q zq-?#(MhoUysZ7)sSwv%PT#Xqbm+0-A7>SuZb8=Hj|WmZ^$NWjbL_)$cX|V~fgpC=eI^p5Q`6ue937A(*Sl zOs>{24RvD{B(<=R7VHkVVG-%JKR86dSXcjI(}`UVg9SDRw-Y^0;6{U%#0#nLW0z!x zS$BzJ`-D&o2Y^Xn_7D`#2L3Ib#=IYg&2z~jU3usOou%JL;Qw=E1#b&NYnh-Xrt1@B z2eBm$8p%eiryPZiWDaPmQL5zlt1+PQz%Vc8)F$Nbg7R+2YY$chA5O2!)E5cJXK^B= zdPR&7rlVHD$AU)~Uv}mYXHqbjn_>pk6?CTM<%{$*N+o3b<@g&&$$cz(P zFcaxX-T*0wY72=~yVa4Dy21p{Lq^kq$oH3Uj>+^m=PnDbW1Kzh=#7wpQzKy4L8?R= z?rLdvbEyZ)wVOM;c}I&FU2Fw!M6sF{WEk885GDjbTPD{v+8^mfg;KX|JxJ!dY!TYZ z)L0=@npkyIHzQ$DktVH%y!ma!0Y3ED^>(RrAL^fsTm>qe<_mCCKM3?B#?*K7%=~J` zmyONN#>X*xE^fKTk_wO~&qCgM{1sYeUmd$t&0z_j%Yo(yBc>>#xJt1G&%%(lmtHae z&luCfeO+y!r-`akqr}(O!sieAVCB}BO8N0$asxGXN1?|!Ep+z5U>!wBwJa*06i+)* z2)*jKi}rN;lYMOvc+Ut}j_s&S1ac6d*hPa~j`;zeBp6^Ev&9vAQ;EOZp9zSB&f6pN z?QU0)c<%i2C}6a7ATHVhT^*uQS8Bu{?4R!vaQK}!1$0ms4=ezWBP}e*F1qfI<;+?o zMTXyZ6^u6lKO|spVJxSzcrM2C*olo952eEV<#$NIwAxSrIFxILMrN_lU9_CJh{ z1pxrnA-xI>-vua@C_tnNkB?yo06a6pfjm|b+?6}Q&*hr`XS&)R73$HY{O7p&)pmQ1 z5%Hg+$s7oP>1~0KM1ldAXkP$OvhD@bug7nI4@$7j@4pVz84d$zsm0%amGQh!q5nNQ zgSQ)TmcA`RIzm1-L8b-nNm7vi;%LtQ$H=W|W-l6L1(WlvJW>fA2*L-gvJY3nBP(*t zVoB@Vt*i~+bgS9*;!9sT6k zQUD0gIQvHw3~r|wPEs>u^5C?ET(??#BmgV(1{wMh6ZNRVG&S}Ic>;@B42N**-1SGF zZ%u^2ztURLtW9fJX_@^n7pK&2vc_yj=)4`JS~ZnU9`0bqCZ-X-o=4 zky<2??Gr=b4pvN_YKQQQ$XS}Wjjk|Q5$3Kubf_3ZIIV0*LEt5VtwSK8>nP~8YOsl< zn0qH(&oQSo@Z10b0>1GoTtNJ2IU2B7wV9B|BbhPe-|DVcNe+1W{M^4RVK^vFMgO4B zup?nS!fFd~(pBmp$+lod`$G}y8yUPF>WGhp5P2HRXNX3tyFWR-eCxUq6DDk9#ql_u zkJs$FO^RJvJKHas)|T0#PmZY9yl8bh{&r5!iV0h9FwL6Fqilo#2q~ zQQA*0qVDTHSnH&A`yD|_jtG>dz8)vzeAy=Q;&_?>GTDrhH1#*v)$qgsHaw8K!wI83 zjgL^Q{>uuTZRPpsIJ`4&ZloW@Z6! zG!r@T2Ru-(w{Jg{t*!03|0LdBgJHP~E3(=|r4Lo(Ee#Yi0m%8t6F{ZZQW9TTrS(~) zmFo8dhz(Ok3wGTftcLCpzf}P#IcK4=dGi&3*irR0SeuLH6csJUk<9-BrFsx?egdu@ zlfI5L+`CS<>q>Q|ODH}Jv8(h4e`VC#?Q<(b@kpDC6&fv5gtjtCAcEv0m&-C{5VKtI)K}G`x>nR|&j@m5r*h z*4so=;t^y#VXvQx4>JZZ11-m8oKmsu%`#blY~(qB#;#;k!BbtZjaZQ4<_JcykM|gQ zvIDd;kdL>gtp*DvfHe|ezbc~Zes>&+C;itE!}rNv%5Qp;>9*PW@wkR;I;o5bvg7x@ zhd+%Jz?vvYihn^3u43vE{NysNGzSzk?jRFDMxS{gXD>8uYPfIy{8vSjbsrs)39~;W z`IQD63(K@f8=)Ep%=(VN)p0|&rs;7EMS}Jm_7>yoYeo$J12D@%kVaTCdAyQ0@{ozc z?`Ngxdi`L~y#0w!_VhC^`Qj!%%W(fwc4e7xU$OzC6>h7;OaTncPnq zq33AGGz}#w1}0!G#9Yl6Y-6rk6b5XllHV?s)#?fN{3G>Kykl&BLEOIY?%13YvW3OP zYwkX2YXJTqTquZMSJDI~`g^>BKdp2YL9J5rAQoOzh=P3^UDFF@1tJa{l}RqEv{BiF z!c?m=10Af2AB>vLdQEiodV81*Kx48FJuz)j1Ic}&3Vp?F{u>{Fb@&3>UiR4-Aot(_ zi(SPH=Fo`QOs`#yn?RP-FceLs=BCu@?Ld`eYuXQsi(MfDHsCQ7F*vWzT`kZOE4w#N zN_L`KF14)T7P{5Yreh`2oC^-saW zi`oJ%^YPxRaZEc%`B(ReU!z{4TJ=Ztth|xCee|mRL*7PBG9ZaWG1$O!{cM28{%(k|q>%Xg z1EBl^L@6yU{(Rk!xN#VZbbTXrK>r!ZoO^e`0s{4GkuzG&flhT;(rPb-kvRFA_BJ1<9 zxx4de(<=xPQI$__;*spVg#&-}_U&{2uKmcga$g?sgkZp-NPq`Ta>R zD4tqLf(Gty`ZcrkbUY(;@mcHr;`kV4h=|H9;M;^zo1*8dVdEjh)7{iFxa=OLsa2Vy zK??p~hv5hU5lZaZKdzELEDy`UNn zkf4$i4-({fTLCMS$K7FVBc7L|B}{qK`7G{=%D%7tJ+TZJLnu-#>>yzalCAK+Q>kG3 zxIDq805+Ut8_I%K1FE^4a^A8WV`j(%pT29H#wIk^^JU+*YE~tKkZI*sL`j zWMVJd-yhFteiTR~s26|U&&d%Ah^MA_QM+X>4O0K_B?45!C9mC~A$YwlfL6G>krGEN z77`koUaA@jFen>P>=mWk)^Vq2WJr@(Y3Nx2>d45gg4zm95`hl9VCSX}j{kFI*?^s% zp#z+^07(^KT2E8XG>|*3xV@qVKTV!dvRhMc8UQ^o(y&{p1~5Fp;LZ`t=~5Qw=hW{3 z+B~U$7eZcur)yjXSe+M3rZqb^g((0S)}LT{8HyPQ{qb}m0!`P`vyHS`Ad!%eu;7AP zt~5ZS-0s{U!uP*{cn`(SAIu6OHyjM&co6g@CMj?;0hD=I9X5Eu~E4eV=O0KA=Xi&`RY79HGyI9`;rVjRZ zY(TC8E84=2vg_&%#F7RiQ$aXjVF4K3>?mM~{+qMxgaB<^mi>g$`1|dO0n}|AWDbCa z{q2k^nf`x_aZu=K#6;h|xb5G7#e$RdL@a(brr4NcQIuRqUEA~eU+eujh>p(1Kz!Zp z{P_E9-JeyFi`bHMc-LRH!g1#vc~@%q%h`V=@zwG_qM)KmYze?{+*6AJJS%i29)(7- zY1c6q*Pl`~h%HkVCAjZ4MX5^I>A<&?UXQ?PDU-~mkiT6T=mY$Nx0EwdEuH3|H>4I0HvprqP`Rowj z%f6at{ij#~Y~=_mrD`oDY@qEt_PVy^9}-?>1rY#|(`O`xdg%RWpc`FVy^p{A-jw>( zbfw=x+Cdnd{*>+NWaG-p^tHcoK1@G{=l5|)F+kN$e?IFr$iReYUDm^_y=D{`7c^<+_5TAg1ZW_{uPgfP3-%@dgxWR%ae}V>N3jb9^z)^N83&E zb+RRNFhVxF+t~7@9_>cGhhDsBaJRMevg@yxrdpmKHtgQC<@(NBk0GCN0Pi(T`kSTY z7!~DKdN=0QX`ID}!N`=~=E2?xtBElV;gMT8!a0ZI;y~d_jqb(Z;~78d!ayRjLj)_y zKjDyddJ>2LJyy~JlONBX?Q&R)q+L$WL$IzrHTI~t;UlSIO*lNBoT#A5eicAKXnWuP z7r;`e#m*X=a;o6Sr_=kYbUq^%$W9a)9j^dWh>(=$>Mq+YIxGfnmKY_kn7czwSyQz) zT4;n_wK?i6AiFR{Zi-@`zB=b8*P5<7q?2aMGXlZ_jv}S+>emn-1P*0drmXM=25^Z3$q`P`Hv8^TlkLWJ5Dj(Kn*Xgr+MTX(GWVb? zG*rCXsh$_ph(DgTq7H2Qw&B~r6r6j7@+uhkc#WBsdw=rX{=hJ-7vr#== z@p!r(?Ih;%tSNk5jDBE0Wwcqir(EJ{=1CTQDP!o-uAt&d_7pJx4t5CqbuD0KsoE=~^t!>!&C1?$KerM)!*^sC=xn-y6m$1H5_s`flT_DvNiiY*6>xtr|yO1qPLy{XItYi!H=*6zmjL*-suLU{krdws+*g1RA7xNVQ1 z(5hFKw@t_Gd%vzNmqv7URr&buq^7HKs zhXmd6TY5CG$7h{O4uy%K`oH@4%DAZ7|7p6r7m$#UMp#l`1f4F~do>$Myjq};P&YrW^%zS5t&QzG-$fL?BRHbMw82DjSyjgPp zJ|kV@6|x$6V1CKVTdOzv#{}5ZdagNy$povoCS0)1n|V9_Q6^ZpfW>7aa|oF)D?Mtj zcXrl&ex!FT*=o0bzwd8TCBf*Pa5g{<|e62Jf=;4!yx9H9m84#>j z;6O!zSnGg$<)kCmYjfZBXCbhl|9n{_{R}gVgf9k!8AVDlC_OA%TSVM_U#Un1Z4>PN zrnKK7YrvQ)(dE>!n}KVpZSN#}!2W=T8y8P7`jc}fc&@d9OV&P#9j}*tuScygC}-!x zI29J|53!Jf#>9S7btIJD7EnpMw&(SX=nrYAo_@``=hW&fx-iAW0RCP@u7vt<6r?P5 zedxMEgP5^sH0X+6*qu){wENW0T^rQaTJD~YoN;tA?6m?J#W&EMW}T4~7^1wH_IG!8 zyg69L*z`E-sgryAc&Zn=Kl>T_SVKIm&~M48U;lNpD5dPk#=Z5{%MoI&Cb;bM>JojV z@?F)t=$w%YzE$6{Gm%MLRih^WfsA64ndqXhpx0N2+DzE6$v&de0jAryC7C8kX`Dg9 z@{=`GPSHE%HxL!veUFGHI5`bG?b6Qf;HdNMRX2I9LqM*RqN|nA=>1A?H}a8dn`ltw ztD}lYajBy&MB?WyINoiRy{pJ~YyE4yPsr(rt3w7qexpac2s-gLnzoc@ux0FAUtMAE z+CH-dK4F6~ZE}$J+=*!X)uE`r_u5paF>BM;Zb}mFaP>tbta&%BB*EM%A#oyAyZ{4? z$DlF)xB8LaCac305Q)ak_tysnHK2)V?ykm+Xm%%|Tk)XXmkQC83AOiIK_&kjf1<+( zk09yUStdi&A0)I#K3(7HMK7dR*zIIWF}dx~=wVx*($?P%efH7~f~LC6H7^z|LZval z?1Is$&&|GMeWJxdnFG`<@?rz+q9g9j>bGz7yZu6RDlNM88Ou5gQ0~OPc49ZTapHMG z?Oa@FYu5a7*=nmgJT4PS=kfO=Qu?`NC=MfU6&Bk}?v#^CcieRQ6(XrQ;B#HJw*d-7 z@zQ}@Umg&NjA2mGX5v%rh+k3a`}(ow3SSNg;oJkd;Y$}PL*yPxuYS2{aYRc6BVA4O zTN!XsW8Cxl%0@mf} z*Tbn$y`)E-M~!?w+X<|CWf};dHA4{}c2(1_K%KLQrmU~>zI+k6gBtb>Zx+!RY0^H8 zpFrK)f616my_-NI7#doe6j7%;W>pj{GsWE!s|XXsbakA=G$g8=g{BA3H7(*2$EUg6>UEa2%t=tCdBVGPdLAyiXW1=qv*lZsV z9q-T6k63Png1ih?D*r4eZVqj{B=|d7quNekm#wV5=~=ztogk={J2?EKoy`Xt`!Oe+ z#i)!KE8YR*yPZU}A7uDCf5rZI3c>~TR7x76Mu`Q3M-lYL5+3@K0t!}}m$E!vtjY@T zG);Z?Xu>1V=Q$^Q%(o+%u3ROgz2_fR*5SvrW$;de@{^0ANL8U{ey}wK3=)z(a3Kth zV98(vA)U<(YWKl9pqy7&m*X5a>Yy=Pio`@5O+{T(CYrP#{GK1VYIXr`MOmuxSU=UP z718<()><6E-o*8NH?Q613RX}p9c7v9DL`HLGoPC70 zVVOwplP0Fbp{Uk76lAyme5o)pMI`=^lc(e2-pQi{$1GAuHuE^orwlS{PduMcY|mo! zIX>F8d(RNj=Ss`hiSa|)@&&GnajyoiW)=%W3_0C=IhngW1oHY)(5@Pfu$}^v`;(LF zyHU&B3K7(~2O|k*f@Yh;e8-E=QLIeNqhKmXIv{KlI?R|*I7F=$7GwN!};;5WBCop47*vRHlpQ=|$ zYj+tH-e6bmgga?yXFWWZ<=C>T1%`G8*i^?de&KL*nCoky16v*?371mTNdC$eIk-iV z+1K^eLRG|x!kWpz3A(>aPS#CHu5!f&k{BNR!xwP{fz&u*-yzTa6vvZ0s8t>X3+?sR z5q2?5o0Y5QPNK zSU8VUUQIQpsg6nXYgG>5-Cv?gO(@oWk}M7vD}4qv_&|UF-CE0ZhC|?J*(C2#LZFiR zA2-{eLb)uJ@&CbhQF$XW*QoU0lWzSC45F!=b{u^~PxLDANe7WQEVX}nU+Wj zx;d@cM$evH7EA;SwR62yyXCX7<5$Tl<{)U!`E1cw0b;Zo=C-Eln3%rHO zOKdj3$+(w%e=Scp!DY579zhVh61N&Lh0#p+bnBc^M~#H$~wHTaoGCy_FygOYPEGec=@Et z($=leXF2`h?tqN=;>0|IYsKv_>ie?={=eJk0cUY4<{kD})rCsp&A(Q*fNZp`qF18B zU(G-ClE%Bc+Y(KReI2%#AG={z^N4Eo+DP6dH$2IhpzHwztkvIsD&{)o0P}5O@cBb3 ze2zJHgaZ%%tjo{0Jh-0nDu1`?Mh!Fx65j9q`27(^uBG4aLvZUoySctL$IH)UF<=iKAqV z*q4ifQ7#d$=i51H(bgq0B)SbDYne(xI!?#ArI-ubTZyOjVg%*I9IZVzgpPwgJ8!PM zJpF7`#Kn+1?=QWVQ_H_q{1DcECjRmCM>f;yQmX}F3uJKW;rvHUBC+(9#(q7R@v?mu z+wN8Owe922qZe;zD3`@po!8F76*Y3aJ$ZPl_vn}RKWCx7&KA7C%Lr;K)++{W#-s-) zZCpQSAJ>#E0&7mOI1dBge`}&%Xud2;8F*;V0s-2N2sv<@_ukzFBBXg*A2x;Wk5)+5 zh^~P6a>d|hKCytS%heli$H?eej)kj2SMJE;V4vO40N1@^_%;E}6i&_WfTf`^ksu$9 znCT0yb^mu~7n7zIxu6pDb}HV=FOz80b)m91Tb<$Xf*H~)&p!xVhy0feuXs4xpfT{8 z2)!W1GDy4+i+l^t-Xfou1K~7ApNfUry0`VMynNIqyD;W^QhQI4v=sBeF6ER6-7IG% z$#0rs-Z_nPf$)0I!^2?sdx#s@J?;_$7*~kUr2Yq%*DrWEIl*YDx^AVL>;W8&XK}>1 z_-B*22KkbYvuELLzQ0M0zFkhc=q|3Tr!Wv(3I&K0YG!sw>iSkbcbGnn9RGUwJT567 z@xBH31AXIoLRQg zi8uP5pIQ$aIiY`6o}DP{KfsWCY+ygl3aO@}TSp+=>m%iD(?_wh92X?SlemSU<-pJi z3&jp=rj2lPY|*cx;$%g1J2LV{ixHWDhcI7H$>M!p3;+zx>K+!Vt-rk%et9!q;QU*i zjHaxG;B?hffLpLJ6n*-FgIcFItc#9p%#O^K^SV2&<<3JMyjPZ1l?xsl%$s}ByQ~9O>dc7I`YXM2 zaR^VJEw|>`dJcW``XuCS<iw_U$T zUu$o)lj!bF&CUkdw`1k#;}g&MsM%wvV@D?qgLu2V@rV2=DL@3)`$~=wr{?JYQ@G#i zWp6y}q4DMrIHp`yft8X^RFQZBVTB@Q2Np>A$?+nt`Mq;Vay{9^5;X;eH%$FLLzOT& zTRbnl)xVtGY)-<4`*xtE*Sh~mU3pwFYQ+lV4*z#9*G6>HB5O}P_ET7NWKg$ZSSSk3 zw=t=g5hZF=nzzMN$vb#BV}#(ubz;d>+1Lj#(V!tmN}>8jYBAOnl-s{Wa)@%4mPRIX z)OWlk8k6Z+U(TyQoc`?;YsK$kGw-+JVvsT`;8~)4Mn^?Y8YO$NjvheY^>tpJ&y<~q z!^S4igd*P3Fg@0&!OiFG=G}&CHK6P?wsp9h$K}8pbE4l;udJr#fN#Z5mUvHy(j==O zz1@_@pp}(zQm3tg}t$1gkwaRLVPE z!Snu^$snfoR>J;Ola@LnG}SVV^@W@zEG+~^ zn}8#*AhkA2$Bc zV$N_DeE{;@s9#>o{ASF{eMi!(10I~#f}-l9h`CZDOu#gnFS{!e*z(y7FJZ;vK4x`} zQS`=UmT`X2ATvvB+-W<1t{m{xyDk18vu5OIrZ0do`dJvp367uXK=;0N47!Yn!(wiXQx>7UsOwdk{SN^qSN;V z*xwhADP3{Tz<#w$nBNR+eaB_5SfAI@I`SKTZxCggC7SL7s@VV&N!X1JW5Dad8p%gQO*z`J;?~<~wC8Cc^US;xidG z^qzL!VG;rm|LxwpqoY3;{T}t*ypM-BIDHNL*nKqze;9JcAxXaQ7YuN7zoS{5{GmB)~ h7+^hGG$@bs{0TAA)zZ#tKpQHOvb=^|jf};+{{u_FRO$c# literal 0 HcmV?d00001 diff --git a/docs/ref/contrib/_images/flatfiles_admin.png b/docs/ref/contrib/admin/_images/flatfiles_admin.png similarity index 100% rename from docs/ref/contrib/_images/flatfiles_admin.png rename to docs/ref/contrib/admin/_images/flatfiles_admin.png diff --git a/docs/ref/contrib/admin/_images/user_actions.png b/docs/ref/contrib/admin/_images/user_actions.png new file mode 100644 index 0000000000000000000000000000000000000000..9a62f7068568ad62c503ccf68721aa0eaaef851d GIT binary patch literal 25950 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 literal 0 HcmV?d00001 diff --git a/docs/ref/contrib/_images/users_changelist.png b/docs/ref/contrib/admin/_images/users_changelist.png similarity index 100% rename from docs/ref/contrib/_images/users_changelist.png rename to docs/ref/contrib/admin/_images/users_changelist.png diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt new file mode 100644 index 0000000000..4969e97a99 --- /dev/null +++ b/docs/ref/contrib/admin/actions.txt @@ -0,0 +1,239 @@ +.. _ref-contrib-admin-actions: + +============= +Admin actions +============= + +.. versionadded:: 1.1 + +.. currentmodule:: django.contrib.admin + +The basic workflow of Django's admin is, in a nutshell, "select an object, +then change it." This works well for a majority of use cases. However, if you +need to make the same change to many objects at once, this workflow can be +quite tedious. + +In these cases, Django's admin lets you write and register "actions" -- simple +functions that get called with a list of objects selected on the change list +page. + +If you look at any change list in the admin, you'll see this feature in +action; Django ships with a "delete selected objects" action available to all +models. For example, here's the user module from Django's built-in +:mod:`django.contrib.auth` app: + +.. image:: _images/user_actions.png + +Read on to find out how to add your own actions to this list. + +Writing actions +=============== + +The easiest way to explain actions is by example, so let's dive in. + +A common use case for admin actions is the bulk updating of a model. Imagine a simple +news application with an ``Article`` model:: + + from django.db import models + + STATUS_CHOICES = ( + ('d', 'Draft'), + ('p', 'Published'), + ('w', 'Withdrawn'), + ) + + class Article(models.Model): + title = models.CharField(max_length=100) + body = models.TextField() + status = models.CharField(max_length=1, choices=STATUS_CHOICES) + + def __unicode__(self): + return self.title + +A common task we might perform with a model like this is to update an +article's status from "draft" to "published". We could easily do this in the +admin one article at a time, but if we wanted to bulk-publish a group of +articles, it'd be tedious. So, let's write an action that lets us change an +article's status to "published." + +Writing action functions +------------------------ + +First, we'll need to write a function that gets called when the action is +trigged from the admin. Action functions are just regular functions that take +two arguments: an :class:`~django.http.HttpRequest` representing the current +request, and a :class:`~django.db.models.QuerySet` containing the set of +objects selected by the user. Our publish-these-articles function won't need +the request object, but we will use the queryset:: + + def make_published(request, queryset): + queryset.update(status='p') + +.. note:: + + For the best performance, we're using the queryset's :ref:`update method + `. Other types of actions might need to deal + with each object individually; in these cases we'd just iterate over the + queryset:: + + for obj in queryset: + do_something_with(obj) + +That's actually all there is to writing an action! However, we'll take one +more optional-but-useful step and give the action a "nice" title in the admin. +By default, this action would appear in the action list as "Make published" -- +the function name, with underscores replaced by spaces. That's fine, but we +can provide a better, more human-friendly name by giving the +``make_published`` function a ``short_description`` attribute:: + + def make_published(request, queryset): + queryset.update(status='p') + make_published.short_description = "Mark selected stories as published" + +.. note:: + + This might look familiar; the admin's ``list_display`` option uses the + same technique to provide human-readable descriptions for callback + functions registered there, too. + +Adding actions to the :class:`ModelAdmin` +----------------------------------------- + +Next, we'll need to inform our :class:`ModelAdmin` of the action. This works +just like any other configuration option. So, the complete ``admin.py`` with +the action and its registration would look like:: + + from django.contrib import admin + from myapp.models import Article + + def make_published(request, queryset): + queryset.update(status='p') + make_published.short_description = "Mark selected stories as published" + + class ArticleAdmin(admin.ModelAdmin): + list_display = ['title', 'status'] + ordering = ['title'] + actions = [make_published] + + admin.site.register(Article, ArticleAdmin) + +That code will give us an admin change list that looks something like this: + +.. image:: _images/article_actions.png + +That's really all there is to it! If you're itching to write your own actions, +you now know enough to get started. The rest of this document just covers more +advanced techniques. + +Advanced action techniques +========================== + +There's a couple of extra options and possibilities you can exploit for more +advanced options. + +Actions as :class:`ModelAdmin` methods +-------------------------------------- + +The example above shows the ``make_published`` action defined as a simple +function. That's perfectly fine, but it's not perfect from a code design point +of view: since the action is tightly coupled to the ``Article`` object, it +makes sense to hook the action to the ``ArticleAdmin`` object itself. + +That's easy enough to do:: + + class ArticleAdmin(admin.ModelAdmin): + ... + + actions = ['make_published'] + + def make_published(self, request, queryset): + queryset.update(status='p') + make_published.short_description = "Mark selected stories as published" + +Notice first that we've moved ``make_published`` into a method (remembering to +add the ``self`` argument!), and second that we've now put the string +``'make_published'`` in ``actions`` instead of a direct function reference. +This tells the :class:`ModelAdmin` to look up the action as a method. + +Defining actions as methods is especially nice because it gives the action +access to the :class:`ModelAdmin` itself, allowing the action to call any of +the methods provided by the admin. + +For example, we can use ``self`` to flash a message to the user informing her +that the action was successful:: + + class ArticleAdmin(admin.ModelAdmin): + ... + + def make_published(self, request, queryset): + rows_updated = queryset.update(status='p') + if rows_updated == 1: + message_bit = "1 story was" + else: + message_bit = "%s stories were" % rows_updated + self.message_user(request, "%s successfully marked as published." % message_bit) + +This make the action match what the admin itself does after successfully +performing an action: + +.. image:: _images/article_actions_message.png + +Actions that provide intermediate pages +--------------------------------------- + +By default, after an action is performed the user is simply redirected back +the the original change list page. However, some actions, especially more +complex ones, will need to return intermediate pages. For example, the +built-in delete action asks for confirmation before deleting the selected +objects. + +To provide an intermediary page, simply return an +:class:`~django.http.HttpResponse` (or subclass) from your action. For +example, you might write a simple export function that uses Django's +:ref:`serialization functions ` to dump some selected +objects as JSON:: + + from django.http import HttpResponse + from django.core import serializers + + def export_as_json(request, queryset): + response = HttpResponse(mimetype="text/javascript") + serialize.serialize(queryset, stream=response) + return response + +Generally, something like the above isn't considered a great idea. Most of the +time, the best practice will be to return an +:class:`~django.http.HttpResponseRedirect` and redirect the user to a view +you've written, passing the list of selected objects in the GET query string. +This allows you to provide complex interaction logic on the intermediary +pages. For example, if you wanted to provide a more complete export function, +you'd want to let the user choose a format, and possibly a list of fields to +include in the export. The best thing to do would be to write a small action that simply redirects +to your custom export view:: + + from django.contrib import admin + from django.contrib.contenttypes.models import ContentType + from django.http import HttpResponseRedirect + + def export_selected_objects(request, queryset): + selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME) + ct = ContentType.objects.get_for_model(queryset.model) + return HttpResponseRedirect("/export/?ct=%s&ids=%s" % (ct.pk, ",".join(selected))) + +As you can see, the action is the simple part; all the complex logic would +belong in your export view. This would need to deal with objects of any type, +hence the business with the ``ContentType``. + +Writing this view is left as an exercise to the reader. + +Making actions available globally +--------------------------------- + +Some actions are best if they're made available to *any* object in the admin +-- the export action defined above would be a good candidate. You can make an +action globally available using :meth:`AdminSite.add_action()`:: + + from django.contrib import admin + + admin.site.add_action(export_selected_objects) + diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin/index.txt similarity index 98% rename from docs/ref/contrib/admin.txt rename to docs/ref/contrib/admin/index.txt index 1813a191f6..1bcd31bc01 100644 --- a/docs/ref/contrib/admin.txt +++ b/docs/ref/contrib/admin/index.txt @@ -38,6 +38,14 @@ There are five steps in activating the Django admin site: ``ModelAdmin`` classes. 5. Hook the ``AdminSite`` instance into your URLconf. + +Other topics +------------ + +.. toctree:: + :maxdepth: 1 + + actions ``ModelAdmin`` objects ====================== @@ -664,6 +672,19 @@ The value is another dictionary; these arguments will be passed to that have ``raw_id_fields`` or ``radio_fields`` set. That's because ``raw_id_fields`` and ``radio_fields`` imply custom widgets of their own. +``actions`` +~~~~~~~~~~~ + +A list of actions to make available on the change list page. See +:ref:`ref-contrib-admin-actions` for details. + +``actions_on_top``, ``actions_on_buttom`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Controls where on the page the actions bar appears. By default, the admin +changelist displays actions at the top of the page (``actions_on_top = True; +actions_on_bottom = False``). + ``ModelAdmin`` methods ---------------------- @@ -1138,7 +1159,6 @@ or add anything you like. Then, simply create an instance of your Python class), and register your models and ``ModelAdmin`` subclasses with it instead of using the default. - Hooking ``AdminSite`` instances into your URLconf ------------------------------------------------- @@ -1177,7 +1197,6 @@ There is really no need to use autodiscover when using your own ``AdminSite`` instance since you will likely be importing all the per-app admin.py modules in your ``myproject.admin`` module. - Multiple admin sites in the same URLconf ---------------------------------------- diff --git a/docs/ref/contrib/index.txt b/docs/ref/contrib/index.txt index 82a895523c..4f401d6836 100644 --- a/docs/ref/contrib/index.txt +++ b/docs/ref/contrib/index.txt @@ -24,7 +24,7 @@ those packages have. .. toctree:: :maxdepth: 1 - admin + admin/index auth comments/index contenttypes diff --git a/tests/regressiontests/admin_registration/models.py b/tests/regressiontests/admin_registration/models.py index fdfa3691b8..35cf8afce8 100644 --- a/tests/regressiontests/admin_registration/models.py +++ b/tests/regressiontests/admin_registration/models.py @@ -49,7 +49,7 @@ AlreadyRegistered: The model Person is already registered >>> site._registry[Person].search_fields ['name'] >>> site._registry[Person].list_display -['__str__'] +['action_checkbox', '__str__'] >>> site._registry[Person].save_on_top True diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml b/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml new file mode 100644 index 0000000000..316e750577 --- /dev/null +++ b/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml @@ -0,0 +1,15 @@ + + + + John Doe + john@example.org + + + Max Mustermann + max@example.org + + + John Doe + john@example.org + + diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index eeaf039444..e5e112fa43 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from django.db import models from django.contrib import admin +from django.core.mail import EmailMessage class Section(models.Model): """ @@ -199,6 +200,41 @@ class PersonaAdmin(admin.ModelAdmin): BarAccountAdmin ) +class Subscriber(models.Model): + name = models.CharField(blank=False, max_length=80) + email = models.EmailField(blank=False, max_length=175) + + def __unicode__(self): + return "%s (%s)" % (self.name, self.email) + +class SubscriberAdmin(admin.ModelAdmin): + actions = ['delete_selected', 'mail_admin'] + + def mail_admin(self, request, selected): + EmailMessage( + 'Greetings from a ModelAdmin action', + 'This is the test email from a admin action', + 'from@example.com', + ['to@example.com'] + ).send() + +class ExternalSubscriber(Subscriber): + pass + +def external_mail(request, selected): + EmailMessage( + 'Greetings from a function action', + 'This is the test email from a function action', + 'from@example.com', + ['to@example.com'] + ).send() + +def redirect_to(request, selected): + from django.http import HttpResponseRedirect + return HttpResponseRedirect('/some-where-else/') + +class ExternalSubscriberAdmin(admin.ModelAdmin): + actions = [external_mail, redirect_to] admin.site.register(Article, ArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin) @@ -208,6 +244,8 @@ admin.site.register(Color) admin.site.register(Thing, ThingAdmin) admin.site.register(Person, PersonAdmin) admin.site.register(Persona, PersonaAdmin) +admin.site.register(Subscriber, SubscriberAdmin) +admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin) # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # That way we cover all four cases: diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 33000d4f5a..ce6356726f 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -8,10 +8,11 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin.models import LogEntry from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.util import quote +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.utils.html import escape # local test models -from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Person, Persona, FooAccount, BarAccount +from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Person, Persona, FooAccount, BarAccount, Subscriber, ExternalSubscriber try: set @@ -516,7 +517,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): def test_changelist_to_changeform_link(self): "The link from the changelist referring to the changeform of the object should be quoted" response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/') - should_contain = """%s""" % (quote(self.pk), escape(self.pk)) + should_contain = """%s""" % (quote(self.pk), escape(self.pk)) self.assertContains(response, should_contain) def test_recentactions_link(self): @@ -738,29 +739,30 @@ class AdminViewListEditable(TestCase): def tearDown(self): self.client.logout() - + def test_changelist_input_html(self): response = self.client.get('/test_admin/admin/admin_views/person/') # 2 inputs per object(the field and the hidden id field) = 6 # 2 management hidden fields = 2 + # 4 action inputs (3 regular checkboxes, 1 checkbox to select all) # main form submit button = 1 # search field and search submit button = 2 # 6 + 2 + 1 + 2 = 11 inputs - self.failUnlessEqual(response.content.count("