From 99b14f9c9f11b5a609ca5ced3caaad3ceebd733f Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Sep 2014 15:05:20 +0000 Subject: [PATCH] Major cleanup --- TODO.md | 19 +- orchestra/admin/decorators.py | 4 +- orchestra/admin/utils.py | 1 - orchestra/api/options.py | 1 - orchestra/apps/accounts/admin.py | 3 - orchestra/apps/accounts/filters.py | 1 - .../commands/createinitialaccount.py | 6 +- .../migrations/0003_auto_20140926_1325.py | 25 + orchestra/apps/accounts/models.py | 3 +- orchestra/apps/bills/admin.py | 6 +- .../migrations/0008_auto_20140926_1218.py | 18 + .../migrations/0009_auto_20140926_1220.py | 46 ++ .../migrations/0010_auto_20140926_1326.py | 29 ++ .../migrations/0011_auto_20140926_1334.py | 25 + .../migrations/0012_auto_20140926_1458.py | 19 + orchestra/apps/bills/models.py | 45 +- orchestra/apps/bills/settings.py | 3 + .../bills/templates/bills/microspective.html | 4 +- orchestra/apps/contacts/settings.py | 1 - orchestra/apps/databases/admin.py | 2 - orchestra/apps/databases/api.py | 3 - orchestra/apps/databases/backends.py | 6 +- orchestra/apps/databases/forms.py | 3 +- orchestra/apps/domains/actions.py | 14 + orchestra/apps/domains/admin.py | 34 +- orchestra/apps/domains/backends.py | 2 - .../apps/domains/migrations/0001_initial.py | 42 ++ .../domains/migrations/0002_record_ttl.py | 21 + orchestra/apps/domains/migrations/__init__.py | 0 orchestra/apps/domains/models.py | 52 +- .../domains/tests/functional_tests/tests.py | 2 +- orchestra/apps/domains/tests/test_domains.py | 1 - orchestra/apps/issues/admin.py | 24 +- orchestra/apps/issues/forms.py | 3 - orchestra/apps/issues/models.py | 7 +- orchestra/apps/lists/backends.py | 2 - orchestra/apps/mails/admin.py | 5 +- orchestra/apps/mails/backends.py | 3 +- orchestra/apps/mails/models.py | 3 - orchestra/apps/mails/validators.py | 1 - orchestra/apps/orchestration/admin.py | 12 +- orchestra/apps/orchestration/backends.py | 1 - orchestra/apps/orchestration/models.py | 8 +- .../apps/orchestration/tests/test_route.py | 2 - orchestra/apps/orders/actions.py | 2 +- orchestra/apps/orders/admin.py | 6 +- orchestra/apps/orders/billing.py | 10 +- orchestra/apps/orders/filters.py | 7 - orchestra/apps/orders/models.py | 15 +- orchestra/apps/orders/settings.py | 1 - orchestra/apps/payments/actions.py | 30 +- orchestra/apps/payments/admin.py | 2 - orchestra/apps/payments/models.py | 5 +- orchestra/apps/resources/admin.py | 16 +- orchestra/apps/resources/apps.py | 1 - orchestra/apps/resources/forms.py | 4 +- orchestra/apps/resources/helpers.py | 10 +- .../apps/resources/migrations/0001_initial.py | 78 +++ .../migrations/0002_auto_20140926_1143.py | 19 + .../migrations/0003_auto_20140926_1325.py | 70 +++ .../apps/resources/migrations/__init__.py | 0 orchestra/apps/resources/models.py | 23 +- orchestra/apps/resources/serializers.py | 6 +- orchestra/apps/resources/tasks.py | 3 +- orchestra/apps/services/actions.py | 19 +- orchestra/apps/services/admin.py | 4 +- orchestra/apps/services/handlers.py | 21 +- orchestra/apps/services/models.py | 12 +- orchestra/apps/services/rating.py | 8 +- .../services/static/services/img/services.png | Bin 0 -> 93392 bytes .../services/static/services/img/services.svg | 485 ++++++++++++++++++ .../admin/services/service/help.html | 14 + .../tests/functional_tests/test_domain.py | 8 +- .../tests/functional_tests/test_ftp.py | 6 +- .../tests/functional_tests/test_job.py | 6 +- .../tests/functional_tests/test_mailbox.py | 4 +- .../tests/functional_tests/test_plan.py | 2 +- .../tests/functional_tests/test_traffic.py | 25 +- orchestra/apps/services/tests/test_handler.py | 8 +- orchestra/apps/users/admin.py | 1 - orchestra/apps/users/api.py | 3 - orchestra/apps/users/models.py | 1 + orchestra/apps/users/roles/__init__.py | 2 - orchestra/apps/users/roles/admin.py | 1 - orchestra/apps/users/roles/jabber/admin.py | 2 - orchestra/apps/users/roles/mail/admin.py | 3 - orchestra/apps/users/roles/mail/backends.py | 3 +- orchestra/apps/users/roles/mail/models.py | 4 - orchestra/apps/users/roles/mail/validators.py | 1 - orchestra/apps/users/roles/posix/admin.py | 2 - orchestra/apps/vps/admin.py | 1 - orchestra/apps/vps/backends.py | 2 - orchestra/apps/vps/forms.py | 2 +- orchestra/apps/webapps/admin.py | 1 - orchestra/apps/webapps/api.py | 1 - orchestra/apps/webapps/backends/dokuwikimu.py | 4 +- .../apps/webapps/backends/wordpressmu.py | 2 +- orchestra/apps/websites/admin.py | 3 - orchestra/apps/websites/api.py | 1 - orchestra/apps/websites/backends/apache.py | 2 + orchestra/apps/websites/backends/webalizer.py | 1 - orchestra/conf/base_settings.py | 2 +- .../commands/postupgradeorchestra.py | 1 - orchestra/management/commands/setuppostfix.py | 1 - orchestra/management/commands/staticcheck.py | 102 ++++ orchestra/models/fields.py | 1 + orchestra/permissions/api.py | 1 - orchestra/utils/humanize.py | 7 +- orchestra/utils/options.py | 2 +- orchestra/utils/python.py | 4 +- orchestra/utils/tests.py | 1 - 111 files changed, 1279 insertions(+), 328 deletions(-) create mode 100644 orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py create mode 100644 orchestra/apps/bills/migrations/0008_auto_20140926_1218.py create mode 100644 orchestra/apps/bills/migrations/0009_auto_20140926_1220.py create mode 100644 orchestra/apps/bills/migrations/0010_auto_20140926_1326.py create mode 100644 orchestra/apps/bills/migrations/0011_auto_20140926_1334.py create mode 100644 orchestra/apps/bills/migrations/0012_auto_20140926_1458.py create mode 100644 orchestra/apps/domains/actions.py create mode 100644 orchestra/apps/domains/migrations/0001_initial.py create mode 100644 orchestra/apps/domains/migrations/0002_record_ttl.py create mode 100644 orchestra/apps/domains/migrations/__init__.py create mode 100644 orchestra/apps/resources/migrations/0001_initial.py create mode 100644 orchestra/apps/resources/migrations/0002_auto_20140926_1143.py create mode 100644 orchestra/apps/resources/migrations/0003_auto_20140926_1325.py create mode 100644 orchestra/apps/resources/migrations/__init__.py create mode 100644 orchestra/apps/services/static/services/img/services.png create mode 100644 orchestra/apps/services/static/services/img/services.svg create mode 100644 orchestra/apps/services/templates/admin/services/service/help.html create mode 100644 orchestra/management/commands/staticcheck.py diff --git a/TODO.md b/TODO.md index 46271e8f..f1a1f3fd 100644 --- a/TODO.md +++ b/TODO.md @@ -59,12 +59,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im dependency collector with max_recursion that matches the number of dots on service.match and service.metric -* Be consistent with dates: - * created_on date - * created_at datetime - -at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon - * backend logs with hal logo * Use logs for storing monitored values @@ -82,13 +76,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon * help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla ) -* Create ProForma from orders orders.bill(proforma=True) - -* generic confirmation breadcrumbs for single objects - -* DirectDebit due date = bill.due_date - -* settings.ENABLED_PLUGINS = ('path.module.ClassPlugin',) * Transaction states: CREATED, PROCESSED, EXECUTED, COMMITED, ABORTED (SECURED, REJECTED?) * bill.send() -> transacction.EXECUTED when source=None @@ -108,5 +95,7 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon return order.register_at.date() -* latest by 'id' *always* -* replace add_now by default=lambda: timezone.now() +* mail backend related_models = ('resources__content_type') ?? +* ignore orders + +* Redmine, BSCW and other applications management diff --git a/orchestra/admin/decorators.py b/orchestra/admin/decorators.py index 37bda702..029884a7 100644 --- a/orchestra/admin/decorators.py +++ b/orchestra/admin/decorators.py @@ -1,6 +1,5 @@ from functools import wraps, partial -from django.contrib import messages from django.contrib.admin import helpers from django.template.response import TemplateResponse from django.utils.decorators import available_attrs @@ -61,8 +60,10 @@ def action_with_confirmation(action_name=None, extra_context={}, if len(queryset) == 1: objects_name = force_text(opts.verbose_name) + obj = queryset.get() else: objects_name = force_text(opts.verbose_name_plural) + obj = None if not action_name: action_name = func.__name__ context = { @@ -73,6 +74,7 @@ def action_with_confirmation(action_name=None, extra_context={}, 'action_value': action_value, 'queryset': queryset, 'opts': opts, + 'obj': obj, 'app_label': app_label, 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, } diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 2807a5b0..9be768dd 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -9,7 +9,6 @@ from django.shortcuts import redirect from django.utils import importlib from django.utils.html import escape from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ from orchestra.models.utils import get_field_value from orchestra.utils import humanize diff --git a/orchestra/api/options.py b/orchestra/api/options.py index c56b8e33..d27aeb9e 100644 --- a/orchestra/api/options.py +++ b/orchestra/api/options.py @@ -6,7 +6,6 @@ from orchestra.utils.apps import autodiscover as module_autodiscover from orchestra.utils.python import import_class from .helpers import insert_links, replace_collectionmethodname -from .root import APIRoot def collectionlink(**kwargs): diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index ea5eadf3..70bdb23d 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -2,7 +2,6 @@ from django import forms from django.conf.urls import patterns, url from django.contrib import admin, messages from django.contrib.admin.util import unquote -from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.utils.safestring import mark_safe from django.utils.six.moves.urllib.parse import parse_qsl @@ -115,7 +114,6 @@ class AccountListAdmin(AccountAdmin): select_account.order_admin_field = 'user__username' def changelist_view(self, request, extra_context=None): - opts = self.model._meta original_app_label = request.META['PATH_INFO'].split('/')[-5] original_model = request.META['PATH_INFO'].split('/')[-4] context = { @@ -182,7 +180,6 @@ class AccountAdminMixin(object): def changeform_view(self, request, object_id=None, form_url='', extra_context=None): account_id = self.get_account_from_preserve_filters(request) - verb = 'change' if object_id else 'add' if not object_id: if account_id: # Preselect account diff --git a/orchestra/apps/accounts/filters.py b/orchestra/apps/accounts/filters.py index 078d44aa..84a27831 100644 --- a/orchestra/apps/accounts/filters.py +++ b/orchestra/apps/accounts/filters.py @@ -1,5 +1,4 @@ from django.contrib.admin import SimpleListFilter -from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/accounts/management/commands/createinitialaccount.py b/orchestra/apps/accounts/management/commands/createinitialaccount.py index 5ebde3bb..8e5b1bc5 100644 --- a/orchestra/apps/accounts/management/commands/createinitialaccount.py +++ b/orchestra/apps/accounts/management/commands/createinitialaccount.py @@ -1,10 +1,9 @@ from optparse import make_option -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.db import transaction from orchestra.apps.accounts.models import Account -from orchestra.apps.users.models import User class Command(BaseCommand): @@ -29,5 +28,4 @@ class Command(BaseCommand): username = options.get('username') password = options.get('password') account = Account.objects.create() - user = User.objects.create_superuser(username, email, password, - account=account, is_main=True) + account.users.create_superuser(username, email, password, is_main=True) diff --git a/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py b/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py new file mode 100644 index 00000000..735351a3 --- /dev/null +++ b/orchestra/apps/accounts/migrations/0003_auto_20140926_1325.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_auto_20140909_1850'), + ] + + operations = [ + migrations.RemoveField( + model_name='account', + name='register_date', + ), + migrations.AddField( + model_name='account', + name='registered_on', + field=models.DateField(default=datetime.datetime(2014, 9, 26, 13, 25, 49, 42008), verbose_name='registered', auto_now_add=True), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index a35d9b02..08f10d74 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -1,6 +1,5 @@ from django.conf import settings as djsettings from django.db import models -from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services @@ -18,7 +17,7 @@ class Account(models.Model): language = models.CharField(_("language"), max_length=2, choices=settings.ACCOUNTS_LANGUAGES, default=settings.ACCOUNTS_DEFAULT_LANGUAGE) - register_date = models.DateTimeField(_("register date"), auto_now_add=True) + registered_on = models.DateField(_("registered"), auto_now_add=True) comments = models.TextField(_("comments"), max_length=256, blank=True) is_active = models.BooleanField(default=True) diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py index 7a196297..b0942c83 100644 --- a/orchestra/apps/bills/admin.py +++ b/orchestra/apps/bills/admin.py @@ -5,7 +5,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin -from orchestra.admin.utils import admin_link, admin_date +from orchestra.admin.utils import admin_date from orchestra.apps.accounts.admin import AccountAdminMixin from . import settings @@ -53,8 +53,8 @@ class BillLineInline(admin.TabularInline): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): list_display = ( - 'number', 'is_open', 'type_link', 'account_link', 'created_on_display', - 'num_lines', 'display_total', 'display_payment_state' + 'number', 'type_link', 'account_link', 'created_on_display', + 'num_lines', 'display_total', 'display_payment_state', 'is_open' ) list_filter = (BillTypeListFilter, 'is_open',) add_fields = ('account', 'type', 'is_open', 'due_on', 'comments') diff --git a/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py b/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py new file mode 100644 index 00000000..561885ee --- /dev/null +++ b/orchestra/apps/bills/migrations/0008_auto_20140926_1218.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0007_auto_20140918_1454'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bill', + options={'get_latest_by': 'id'}, + ), + ] diff --git a/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py b/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py new file mode 100644 index 00000000..1382c851 --- /dev/null +++ b/orchestra/apps/bills/migrations/0009_auto_20140926_1220.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('orders', '__first__'), + ('bills', '0008_auto_20140926_1218'), + ] + + operations = [ + migrations.AddField( + model_name='billline', + name='created_on', + field=models.DateField(default=datetime.datetime(2014, 9, 26, 12, 20, 24, 908200), verbose_name='created', auto_now_add=True), + preserve_default=False, + ), + migrations.AddField( + model_name='billline', + name='order_billed_on', + field=models.DateField(null=True, verbose_name='order billed', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='billline', + name='order_billed_until', + field=models.DateField(null=True, verbose_name='order billed until', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='billline', + name='order_id', + field=models.ForeignKey(blank=True, to='orders.Order', help_text='Informative link back to the order', null=True), + preserve_default=True, + ), + migrations.AddField( + model_name='billsubline', + name='type', + field=models.CharField(default=b'OTHER', max_length=16, verbose_name='type', choices=[(b'VOLUME', 'Volume'), (b'COMPENSATION', 'Compensation'), (b'OTHER', 'Other')]), + preserve_default=True, + ), + ] diff --git a/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py b/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py new file mode 100644 index 00000000..e4b5c90e --- /dev/null +++ b/orchestra/apps/bills/migrations/0010_auto_20140926_1326.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0009_auto_20140926_1220'), + ] + + operations = [ + migrations.AlterField( + model_name='bill', + name='closed_on', + field=models.DateField(null=True, verbose_name='closed on', blank=True), + ), + migrations.AlterField( + model_name='bill', + name='created_on', + field=models.DateField(auto_now_add=True, verbose_name='created on'), + ), + migrations.AlterField( + model_name='bill', + name='last_modified_on', + field=models.DateField(auto_now=True, verbose_name='last modified on'), + ), + ] diff --git a/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py b/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py new file mode 100644 index 00000000..a31d489e --- /dev/null +++ b/orchestra/apps/bills/migrations/0011_auto_20140926_1334.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0010_auto_20140926_1326'), + ] + + operations = [ + migrations.RemoveField( + model_name='bill', + name='last_modified_on', + ), + migrations.AddField( + model_name='bill', + name='updated_on', + field=models.DateField(default=datetime.date(2014, 9, 26), verbose_name='updated on', auto_now=True), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py b/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py new file mode 100644 index 00000000..321a4086 --- /dev/null +++ b/orchestra/apps/bills/migrations/0012_auto_20140926_1458.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0011_auto_20140926_1334'), + ] + + operations = [ + migrations.RenameField( + model_name='billline', + old_name='order_id', + new_name='order', + ), + ] diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index ffe54030..0044d82b 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -1,6 +1,6 @@ -import inspect from dateutil.relativedelta import relativedelta +from django.core.validators import ValidationError from django.db import models from django.template import loader, Context from django.utils import timezone @@ -9,8 +9,8 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.apps.accounts.models import Account +from orchestra.apps.contacts.models import Contact from orchestra.core import accounts -from orchestra.utils.functional import cached from orchestra.utils.html import html_to_pdf from . import settings @@ -53,13 +53,12 @@ class Bill(models.Model): account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='%(class)s') type = models.CharField(_("type"), max_length=16, choices=TYPES) - created_on = models.DateTimeField(_("created on"), auto_now_add=True) - closed_on = models.DateTimeField(_("closed on"), blank=True, null=True) - # TODO rename to is_closed + created_on = models.DateField(_("created on"), auto_now_add=True) + closed_on = models.DateField(_("closed on"), blank=True, null=True) is_open = models.BooleanField(_("is open"), default=True) is_sent = models.BooleanField(_("is sent"), default=False) due_on = models.DateField(_("due on"), null=True, blank=True) - last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True) + updated_on = models.DateField(_("updated on"), auto_now=True) total = models.DecimalField(max_digits=12, decimal_places=2, default=0) comments = models.TextField(_("comments"), blank=True) html = models.TextField(_("HTML"), blank=True) @@ -145,7 +144,6 @@ class Bill(models.Model): self.save() def send(self): - from orchestra.apps.contacts.models import Contact self.account.send_email( template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE, context={ @@ -241,9 +239,13 @@ class BillLine(models.Model): quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2) subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2) tax = models.PositiveIntegerField(_("tax")) - # TODO -# order_id = models.ForeignKey('orders.Order', null=True, blank=True, -# help_text=_("Informative link back to the order")) + # Undo + order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True, + help_text=_("Informative link back to the order")) + order_billed_on = models.DateField(_("order billed"), null=True, blank=True) + order_billed_until = models.DateField(_("order billed until"), null=True, blank=True) + created_on = models.DateField(_("created"), auto_now_add=True) + # Amendment amended_line = models.ForeignKey('self', verbose_name=_("amended line"), related_name='amendment_lines', null=True, blank=True) @@ -262,6 +264,17 @@ class BillLine(models.Model): total += subline.total return total + def undo(self): + # TODO warn user that undoing bills with compensations lead to compensation lost + for attr in ['order_id', 'order_billed_on', 'order_billed_until']: + if not getattr(self, attr): + raise ValidationError(_("Not enough information stored for undoing")) + if self.created_on != self.order.billed_on: + raise ValidationError(_("Dates don't match")) + self.order.billed_until = self.order_billed_until + self.order.billed_on = self.order_billed_on + self.delete() + def save(self, *args, **kwargs): # TODO cost and consistency of this shit super(BillLine, self).save(*args, **kwargs) @@ -272,10 +285,20 @@ class BillLine(models.Model): class BillSubline(models.Model): """ Subline used for describing an item discount """ + VOLUME = 'VOLUME' + COMPENSATION = 'COMPENSATION' + OTHER = 'OTHER' + TYPES = ( + (VOLUME, _("Volume")), + (COMPENSATION, _("Compensation")), + (OTHER, _("Other")), + ) + + # TODO: order info for undoing line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines') description = models.CharField(_("description"), max_length=256) total = models.DecimalField(max_digits=12, decimal_places=2) - # TODO type ? Volume and Compensation + type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER) def save(self, *args, **kwargs): # TODO cost of this shit diff --git a/orchestra/apps/bills/settings.py b/orchestra/apps/bills/settings.py index ac590005..75702302 100644 --- a/orchestra/apps/bills/settings.py +++ b/orchestra/apps/bills/settings.py @@ -36,3 +36,6 @@ BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000 BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE', 'bills/bill-notification.email') + + +BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order') diff --git a/orchestra/apps/bills/templates/bills/microspective.html b/orchestra/apps/bills/templates/bills/microspective.html index a2c4f776..6cbf2cfc 100644 --- a/orchestra/apps/bills/templates/bills/microspective.html +++ b/orchestra/apps/bills/templates/bills/microspective.html @@ -78,9 +78,9 @@ {% with sublines=line.sublines.all %} {{ line.id }} {{ line.description }} - {{ line.amount|default:" " }} + {{ line.quantity|default:" " }} {% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %} - {{ line.total }} &{{ currency.lower }}; + {{ line.subtotal }} &{{ currency.lower }};
{% for subline in sublines %}   diff --git a/orchestra/apps/contacts/settings.py b/orchestra/apps/contacts/settings.py index 96ac58c4..0190d989 100644 --- a/orchestra/apps/contacts/settings.py +++ b/orchestra/apps/contacts/settings.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.utils.translation import ugettext_lazy as _ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', diff --git a/orchestra/apps/databases/admin.py b/orchestra/apps/databases/admin.py index 30c45af4..c54a9bfe 100644 --- a/orchestra/apps/databases/admin.py +++ b/orchestra/apps/databases/admin.py @@ -1,8 +1,6 @@ -from django.db import models from django.conf.urls import patterns from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/databases/api.py b/orchestra/apps/databases/api.py index 2cec89c4..a04ee569 100644 --- a/orchestra/apps/databases/api.py +++ b/orchestra/apps/databases/api.py @@ -1,7 +1,4 @@ from rest_framework import viewsets -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response from orchestra.api import router, SetPasswordApiMixin from orchestra.apps.accounts.api import AccountApiMixin diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 5579adb5..89cfc80e 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -77,21 +77,21 @@ class MysqlDisk(ServiceMonitor): verbose_name = _("MySQL disk") def exceeded(self, db): - context = self.get_context(obj) + context = self.get_context(db) self.append("mysql -e '" "UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\"" " WHERE Db=\"%(db_name)s\";'" % context ) def recovery(self, db): - context = self.get_context(obj) + context = self.get_context(db) self.append("mysql -e '" "UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\"" " WHERE Db=\"%(db_name)s\";'" % context ) def monitor(self, db): - context = self.get_context(obj) + context = self.get_context(db) self.append( "echo %(db_id)s $(mysql -B -e '" " SELECT sum( data_length + index_length ) \"Size\"\n" diff --git a/orchestra/apps/databases/forms.py b/orchestra/apps/databases/forms.py index 5e5f13c1..a9ff2d38 100644 --- a/orchestra/apps/databases/forms.py +++ b/orchestra/apps/databases/forms.py @@ -1,5 +1,5 @@ from django import forms -from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField +from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -109,7 +109,6 @@ class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField): if 'Invalid' not in original: return original encoded = value - final_attrs = self.build_attrs(attrs) if not encoded: summary = mark_safe("%s" % _("No password set.")) else: diff --git a/orchestra/apps/domains/actions.py b/orchestra/apps/domains/actions.py new file mode 100644 index 00000000..7818e953 --- /dev/null +++ b/orchestra/apps/domains/actions.py @@ -0,0 +1,14 @@ +from django.template.response import TemplateResponse +from django.utils.translation import ugettext_lazy as _ + + +def view_zone(modeladmin, request, queryset): + zone = queryset.get() + context = { + 'opts': modeladmin.model._meta, + 'object': zone, + 'title': _("%s zone content") % zone.origin.name + } + return TemplateResponse(request, 'admin/domains/domain/view_zone.html', context) +view_zone.url_name = 'view-zone' +view_zone.verbose_name = _("View zone") diff --git a/orchestra/apps/domains/admin.py b/orchestra/apps/domains/admin.py index 2f7fafcc..0ee63fac 100644 --- a/orchestra/apps/domains/admin.py +++ b/orchestra/apps/domains/admin.py @@ -1,17 +1,13 @@ from django import forms -from django.conf.urls import patterns, url from django.contrib import admin -from django.contrib.admin.util import unquote -from django.core.urlresolvers import reverse -from django.db.models import F -from django.template.response import TemplateResponse from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin -from orchestra.admin.utils import wrap_admin_view, admin_link, change_url +from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.utils import apps +from .actions import view_zone from .forms import RecordInlineFormSet, DomainAdminForm from .filters import TopDomainListFilter from .models import Domain, Record @@ -61,6 +57,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin ('top_domain', 'True'), ) form = DomainAdminForm + change_view_actions = [view_zone] def structured_name(self, domain): if not domain.is_top: @@ -89,35 +86,12 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin websites.short_description = _("Websites") websites.allow_tags = True - def get_urls(self): - """ Returns the additional urls for the change view links """ - urls = super(DomainAdmin, self).get_urls() - admin_site = self.admin_site - opts = self.model._meta - urls = patterns("", - url('^(\d+)/view-zone/$', - wrap_admin_view(self, self.view_zone_view), - name='domains_domain_view_zone') - ) + urls - return urls - - def view_zone_view(self, request, object_id): - zone = self.get_object(request, unquote(object_id)) - context = { - 'opts': self.model._meta, - 'object': zone, - 'title': _("%s zone content") % zone.origin.name - } - return TemplateResponse(request, 'admin/domains/domain/view_zone.html', - context) - def get_queryset(self, request): """ Order by structured name and imporve performance """ qs = super(DomainAdmin, self).get_queryset(request) qs = qs.select_related('top') -# qs = qs.select_related('top') # For some reason if we do this we know for sure that join table will be called T4 - __ = str(qs.query) + str(qs.query) qs = qs.extra( select={'structured_name': 'CONCAT(T4.name, domains_domain.name)'}, ).order_by('structured_name') diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index acc5592b..460d03e1 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -1,5 +1,3 @@ -import os - from django.utils.translation import ugettext_lazy as _ from . import settings diff --git a/orchestra/apps/domains/migrations/0001_initial.py b/orchestra/apps/domains/migrations/0001_initial.py new file mode 100644 index 00000000..5fb4bd30 --- /dev/null +++ b/orchestra/apps/domains/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.core.validators +import orchestra.apps.domains.validators +import orchestra.apps.domains.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_auto_20140909_1850'), + ] + + operations = [ + migrations.CreateModel( + name='Domain', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(unique=True, max_length=256, verbose_name='name', validators=[orchestra.core.validators.validate_hostname, orchestra.apps.domains.validators.validate_allowed_domain])), + ('serial', models.IntegerField(default=orchestra.apps.domains.utils.generate_zone_serial, help_text='Serial number', verbose_name='serial')), + ('account', models.ForeignKey(related_name=b'domains', verbose_name='Account', blank=True, to='accounts.Account')), + ('top', models.ForeignKey(related_name=b'subdomains', to='domains.Domain', null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Record', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'MX', b'MX'), (b'NS', b'NS'), (b'CNAME', b'CNAME'), (b'A', 'A (IPv4 address)'), (b'AAAA', 'AAAA (IPv6 address)'), (b'SRV', b'SRV'), (b'TXT', b'TXT'), (b'SOA', b'SOA')])), + ('value', models.CharField(max_length=256, verbose_name='value')), + ('domain', models.ForeignKey(related_name=b'records', verbose_name='domain', to='domains.Domain')), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/orchestra/apps/domains/migrations/0002_record_ttl.py b/orchestra/apps/domains/migrations/0002_record_ttl.py new file mode 100644 index 00000000..8ce4f8ff --- /dev/null +++ b/orchestra/apps/domains/migrations/0002_record_ttl.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.apps.domains.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('domains', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='record', + name='ttl', + field=models.CharField(default='', validators=[orchestra.apps.domains.validators.validate_zone_interval], max_length=8, blank=True, help_text='Record TTL, defaults to 1h', verbose_name='TTL'), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/domains/migrations/__init__.py b/orchestra/apps/domains/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 1f7cf33a..0f7ca93b 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -1,11 +1,11 @@ -from django.core.exceptions import ValidationError from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address, - validate_hostname, validate_ascii) + validate_hostname, validate_ascii) +from orchestra.utils.python import AttrDict from . import settings, validators, utils @@ -69,13 +69,17 @@ class Domain(models.Model): # Update serial and insert at 0 value = record.value.split() value[2] = str(self.serial) - records.insert(0, (record.SOA, ' '.join(value))) + records.insert(0, + AttrDict(type=record.SOA, ttl=record.get_ttl(), value=' '.join(value)) + ) else: - records.append((record.type, record.value)) + records.append( + AttrDict(type=record.type, ttl=record.get_ttl(), value=record.value) + ) if not self.top: if Record.NS not in types: for ns in settings.DOMAINS_DEFAULT_NS: - records.append((Record.NS, ns)) + records.append(AttrDict(type=Record.NS, value=ns)) if Record.SOA not in types: soa = [ "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER, @@ -86,18 +90,28 @@ class Domain(models.Model): settings.DOMAINS_DEFAULT_EXPIRATION, settings.DOMAINS_DEFAULT_MIN_CACHING_TIME ] - records.insert(0, (Record.SOA, ' '.join(soa))) + records.insert(0, AttrDict(type=Record.SOA, value=' '.join(soa))) no_cname = Record.CNAME not in types if Record.MX not in types and no_cname: for mx in settings.DOMAINS_DEFAULT_MX: - records.append((Record.MX, mx)) + records.append(AttrDict(type=Record.MX, value=mx)) if (Record.A not in types and Record.AAAA not in types) and no_cname: - records.append((Record.A, settings.DOMAINS_DEFAULT_A)) + records.append(AttrDict(type=Record.A, value=settings.DOMAINS_DEFAULT_A)) result = '' - for type, value in records: - name = '%s.%s' % (self.name, ' '*(37-len(self.name))) - type = '%s %s' % (type, ' '*(7-len(type))) - result += '%s IN %s %s\n' % (name, type, value) + for record in records: + name = '{name}.{spaces}'.format( + name=self.name, spaces=' ' * (37-len(self.name)) + ) + ttl = record.get('ttl', settings.DOMAINS_DEFAULT_TTL) + ttl = '{spaces}{ttl}'.format( + spaces=' ' * (7-len(ttl)), ttl=ttl + ) + type = '{type} {spaces}'.format( + type=record.type, spaces=' ' * (7-len(record.type)) + ) + result += '{name} {ttl} IN {type} {value}\n'.format( + name=name, ttl=ttl, type=type, value=record.value + ) return result def save(self, *args, **kwargs): @@ -150,13 +164,15 @@ class Record(models.Model): (SOA, "SOA"), ) - # TODO TTL domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') - type = models.CharField(max_length=32, choices=TYPE_CHOICES) - value = models.CharField(max_length=256) + ttl = models.CharField(_("TTL"), max_length=8, blank=True, + help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, + validators=[validators.validate_zone_interval]) + type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES) + value = models.CharField(_("value"), max_length=256) def __unicode__(self): - return "%s IN %s %s" % (self.domain, self.type, self.value) + return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value) def clean(self): """ validates record value based on its type """ @@ -172,6 +188,8 @@ class Record(models.Model): self.SOA: validators.validate_soa_record, } mapp[self.type](self.value) - + + def get_ttl(self): + return self.ttl or settings.DOMAINS_DEFAULT_TTL services.register(Domain) diff --git a/orchestra/apps/domains/tests/functional_tests/tests.py b/orchestra/apps/domains/tests/functional_tests/tests.py index b0681725..a5268ae6 100644 --- a/orchestra/apps/domains/tests/functional_tests/tests.py +++ b/orchestra/apps/domains/tests/functional_tests/tests.py @@ -124,7 +124,7 @@ class DomainTestMixin(object): self.assertNotEqual(hostmaster, soa[5]) def validate_update(self, server_addr, domain_name): - domain = Domain.objects.get(name=domain_name) + Domain.objects.get(name=domain_name) context = { 'domain_name': domain_name, 'server_addr': server_addr diff --git a/orchestra/apps/domains/tests/test_domains.py b/orchestra/apps/domains/tests/test_domains.py index e370bda3..82acc3ac 100644 --- a/orchestra/apps/domains/tests/test_domains.py +++ b/orchestra/apps/domains/tests/test_domains.py @@ -1,4 +1,3 @@ -from django.db import IntegrityError, transaction from django.test import TestCase from ..models import Domain diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py index 0f4896ed..80096cca 100644 --- a/orchestra/apps/issues/admin.py +++ b/orchestra/apps/issues/admin.py @@ -56,7 +56,7 @@ class MessageReadOnlyInline(admin.TabularInline): def content_html(self, msg): context = { 'number': msg.number, - 'time': admin_date('created_on')(msg), + 'time': admin_date('created_at')(msg), 'author': admin_link('author')(msg) if msg.author else msg.author_name, } summary = _("#%(number)i Updated by %(author)s about %(time)s") % context @@ -98,11 +98,11 @@ class MessageInline(admin.TabularInline): class TicketInline(admin.TabularInline): fields = [ 'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state', - 'colored_priority', 'created', 'last_modified' + 'colored_priority', 'created', 'updated' ] readonly_fields = [ 'ticket_id', 'subject', 'creator_link', 'owner_link', 'colored_state', - 'colored_priority', 'created', 'last_modified' + 'colored_priority', 'created', 'updated' ] model = Ticket extra = 0 @@ -110,8 +110,8 @@ class TicketInline(admin.TabularInline): creator_link = admin_link('creator') owner_link = admin_link('owner') - created = admin_link('created_on') - last_modified = admin_link('last_modified_on') + created = admin_link('created_at') + updated = admin_link('updated_at') colored_state = admin_colored('state', colors=STATE_COLORS, bold=False) colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) @@ -121,10 +121,10 @@ class TicketInline(admin.TabularInline): ticket_id.allow_tags = True -class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeViewActions, +class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): list_display = [ 'unbold_id', 'bold_subject', 'display_creator', 'display_owner', - 'display_queue', 'display_priority', 'display_state', 'last_modified' + 'display_queue', 'display_priority', 'display_state', 'updated' ] list_display_links = ('unbold_id', 'bold_subject') list_filter = [ @@ -134,7 +134,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView ('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'), ('state', 'OPEN') ) - date_hierarchy = 'created_on' + date_hierarchy = 'created_at' search_fields = [ 'id', 'subject', 'creator__username', 'creator__email', 'queue__name', 'owner__username' @@ -192,20 +192,20 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView display_creator = admin_link('creator') display_queue = admin_link('queue') display_owner = admin_link('owner') - last_modified = admin_date('last_modified_on') + updated = admin_date('updated') display_state = admin_colored('state', colors=STATE_COLORS, bold=False) display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) def display_summary(self, ticket): context = { 'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name, - 'created': admin_date('created_on')(ticket), + 'created': admin_date('created_at')(ticket), 'updated': '', } msg = ticket.messages.last() if msg: context.update({ - 'updated': admin_date('created_on')(msg), + 'updated': admin_date('created_at')(msg), 'updater': admin_link('author')(self, msg) if msg.author else msg.author_name, }) context['updated'] = '. Updated by %(updater)s about %(updated)s' % context @@ -283,7 +283,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView def message_preview_view(self, request): """ markdown preview render via ajax """ data = request.POST.get("data") - data_formated = markdowt_tn(strip_tags(data)) + data_formated = markdown(strip_tags(data)) return HttpResponse(data_formated) def get_queryset(self, request): diff --git a/orchestra/apps/issues/forms.py b/orchestra/apps/issues/forms.py index 5d26db9c..ae0dcafc 100644 --- a/orchestra/apps/issues/forms.py +++ b/orchestra/apps/issues/forms.py @@ -1,11 +1,9 @@ from django import forms -from django.core.urlresolvers import reverse from django.utils.html import strip_tags from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from markdown import markdown -from orchestra.admin.utils import change_url from orchestra.apps.users.models import User from orchestra.forms.widgets import ReadOnlyWidget @@ -42,7 +40,6 @@ class MessageInlineForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(MessageInlineForm, self).__init__(*args, **kwargs) - admin_link = change_url(self.user) self.fields['created_on'].widget = ReadOnlyWidget('') def clean_content(self): diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py index 2341da5b..0a83ecc2 100644 --- a/orchestra/apps/issues/models.py +++ b/orchestra/apps/issues/models.py @@ -1,6 +1,5 @@ from django.conf import settings as djsettings from django.db import models -from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from orchestra.apps.contacts import settings as contacts_settings @@ -68,13 +67,13 @@ class Ticket(models.Model): priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES, default=MEDIUM) state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW) - created_on = models.DateTimeField(_("created on"), auto_now_add=True) - last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True) + created_at = models.DateTimeField(_("created"), auto_now_add=True) + updated_at = models.DateTimeField(_("modified"), auto_now=True) cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True) class Meta: - ordering = ["-last_modified_on"] + ordering = ['-updated_at'] def __unicode__(self): return unicode(self.pk) diff --git a/orchestra/apps/lists/backends.py b/orchestra/apps/lists/backends.py index 73b3e40b..d23b686a 100644 --- a/orchestra/apps/lists/backends.py +++ b/orchestra/apps/lists/backends.py @@ -1,8 +1,6 @@ import textwrap -from django.template import Template, Context from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ from orchestra.apps.orchestration import ServiceController from orchestra.apps.resources import ServiceMonitor diff --git a/orchestra/apps/mails/admin.py b/orchestra/apps/mails/admin.py index 280e02fe..c00656ae 100644 --- a/orchestra/apps/mails/admin.py +++ b/orchestra/apps/mails/admin.py @@ -1,15 +1,12 @@ from django import forms from django.contrib import admin -from django.contrib.auth import get_user_model -from django.contrib.auth.admin import UserAdmin from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin -from orchestra.admin.utils import insertattr, admin_link, change_url +from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin -from orchestra.apps.domains.forms import DomainIterator from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter from .models import Mailbox, Address, Autoresponse diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py index 24a10276..b8724c27 100644 --- a/orchestra/apps/mails/backends.py +++ b/orchestra/apps/mails/backends.py @@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController from orchestra.apps.resources import ServiceMonitor from . import settings +from .models import Address class MailSystemUserBackend(ServiceController): @@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor): ) def get_context(self, mailbox): - context = MailSystemUserBackend().get_context(site) + context = MailSystemUserBackend().get_context(mailbox) context['home'] = settings.EMAILS_HOME % context context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize') context['object_id'] = mailbox.pk diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py index 78637dad..70d5778c 100644 --- a/orchestra/apps/mails/models.py +++ b/orchestra/apps/mails/models.py @@ -1,6 +1,3 @@ -import re - -from django.contrib.auth.hashers import check_password, make_password from django.core.validators import RegexValidator from django.db import models from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/mails/validators.py b/orchestra/apps/mails/validators.py index 55d241a4..eab400fa 100644 --- a/orchestra/apps/mails/validators.py +++ b/orchestra/apps/mails/validators.py @@ -44,7 +44,6 @@ def validate_forward(value): def validate_sieve(value): - from .models import Mailbox sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest() path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name) with open(path, 'wb') as f: diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index db1632ae..862f12d4 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from django.core.urlresolvers import reverse from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ @@ -89,18 +88,18 @@ class BackendLogAdmin(admin.ModelAdmin): ) list_display_links = ('id', 'backend') list_filter = ('state', 'backend') - date_hierarchy = 'last_update' + date_hierarchy = 'updated_at' inlines = [BackendOperationInline] fields = [ 'backend', 'server_link', 'state', 'mono_script', 'mono_stdout', 'mono_stderr', 'mono_traceback', 'exit_code', 'task_id', 'display_created', - 'display_last_update', 'execution_time' + 'display_updated', 'execution_time' ] readonly_fields = fields server_link = admin_link('server') - display_last_update = admin_date('last_update') - display_created = admin_date('created') + display_updated = admin_date('updated_at') + display_created = admin_date('created_at') display_state = admin_colored('state', colors=STATE_COLORS) mono_script = display_mono('script') mono_stdout = display_mono('stdout') @@ -111,6 +110,9 @@ class BackendLogAdmin(admin.ModelAdmin): """ Order by structured name and imporve performance """ qs = super(BackendLogAdmin, self).get_queryset(request) return qs.select_related('server').defer('script', 'stdout') + + def has_add_permission(self, *args, **kwargs): + return False class ServerAdmin(admin.ModelAdmin): diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 7f351d89..73f68a53 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -1,7 +1,6 @@ from functools import partial from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ from orchestra.utils import plugins diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index dcd68c0c..91bf1f93 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -1,7 +1,5 @@ from django.contrib.contenttypes import generic -from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -62,8 +60,8 @@ class BackendLog(models.Model): exit_code = models.IntegerField(_("exit code"), null=True) task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True, help_text="Celery task ID when used as execution backend") - created = models.DateTimeField(_("created"), auto_now_add=True) - last_update = models.DateTimeField(_("last update"), auto_now=True) + created_at = models.DateTimeField(_("created"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated"), auto_now=True) class Meta: get_latest_by = 'id' @@ -89,7 +87,7 @@ class BackendOperation(models.Model): action = models.CharField(_("action"), max_length=64) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() - # TODO rename to content_object + instance = generic.GenericForeignKey('content_type', 'object_id') class Meta: diff --git a/orchestra/apps/orchestration/tests/test_route.py b/orchestra/apps/orchestration/tests/test_route.py index 34767203..9a30647d 100644 --- a/orchestra/apps/orchestration/tests/test_route.py +++ b/orchestra/apps/orchestration/tests/test_route.py @@ -1,5 +1,3 @@ -from django.db import IntegrityError, transaction - from orchestra.utils.tests import BaseTestCase from .. import operations, backends diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py index d1317aea..8b031ce8 100644 --- a/orchestra/apps/orders/actions.py +++ b/orchestra/apps/orders/actions.py @@ -78,7 +78,7 @@ class BillSelectedOrders(object): if int(request.POST.get('step')) >= 3: bills = self.queryset.bill(commit=True, **self.options) for order in self.queryset: - modeladmin.log_change(request, order, 'Billed') + self.modeladmin.log_change(request, order, 'Billed') if not bills: msg = _("Selected orders do not have pending billing") self.modeladmin.message_user(request, msg, messages.WARNING) diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index 363f2305..6cf277cb 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -3,7 +3,6 @@ from django.utils import timezone from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ -from orchestra.admin import ChangeListDefaultFilter from orchestra.admin.utils import admin_link, admin_date from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.utils.humanize import naturaldate @@ -13,7 +12,7 @@ from .filters import ActiveOrderListFilter, BilledOrderListFilter from .models import Order, MetricStorage -class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin): +class OrderAdmin(AccountAdminMixin, admin.ModelAdmin): list_display = ( 'id', 'service', 'account_link', 'content_object_link', 'display_registered_on', 'display_billed_until', 'display_cancelled_on' @@ -22,9 +21,6 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin): list_filter = (ActiveOrderListFilter, BilledOrderListFilter, 'service',) actions = (BillSelectedOrders(),) date_hierarchy = 'registered_on' - default_changelist_filters = ( - ('is_active', 'True'), - ) content_object_link = admin_link('content_object', order=False) display_registered_on = admin_date('registered_on') diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py index 39942379..0ab28688 100644 --- a/orchestra/apps/orders/billing.py +++ b/orchestra/apps/orders/billing.py @@ -2,7 +2,7 @@ import datetime from django.utils.translation import ugettext_lazy as _ -from orchestra.apps.bills.models import Invoice, Fee, ProForma, BillLine, BillSubline +from orchestra.apps.bills.models import Invoice, Fee, ProForma class BillsBackend(object): @@ -39,6 +39,10 @@ class BillsBackend(object): subtotal=line.subtotal, tax=service.tax, description=self.get_line_description(line), + + order=line.order, + order_billed_on=line.order.old_billed_on, + order_billed_until=line.order.old_billed_until ) self.create_sublines(billine, line.discounts) return bills @@ -46,7 +50,6 @@ class BillsBackend(object): def format_period(self, ini, end): ini = ini.strftime("%b, %Y") end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y") - # TODO if diff is less than a month: write the month only if ini == end: return ini return _("{ini} to {end}").format(ini=ini, end=end) @@ -67,6 +70,7 @@ class BillsBackend(object): def create_sublines(self, line, discounts): for discount in discounts: line.sublines.create( - description=_("Discount per %s") % discount.type, + description=_("Discount per %s") % discount.type.lower(), total=discount.total, + type=discount.type, ) diff --git a/orchestra/apps/orders/filters.py b/orchestra/apps/orders/filters.py index 87d1c7a7..303d0538 100644 --- a/orchestra/apps/orders/filters.py +++ b/orchestra/apps/orders/filters.py @@ -13,7 +13,6 @@ class ActiveOrderListFilter(SimpleListFilter): return ( ('True', _("Active")), ('False', _("Inactive")), - ('None', _("All")), ) def queryset(self, request, queryset): @@ -23,12 +22,6 @@ class ActiveOrderListFilter(SimpleListFilter): return queryset.inactive() return queryset - def choices(self, cl): - """ Remove default All """ - choices = iter(super(ActiveOrderListFilter, self).choices(cl)) - choices.next() - return choices - class BilledOrderListFilter(SimpleListFilter): """ Filter tickets by created_by according to request.user """ diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 651d5126..dbbaa764 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -1,28 +1,24 @@ import datetime import decimal import logging -import sys from django.db import models from django.db.migrations.recorder import MigrationRecorder from django.db.models import F, Q from django.db.models.loading import get_model -from django.db.models.signals import pre_delete, post_delete, post_save +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.contrib.admin.models import LogEntry from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.utils import timezone -from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from orchestra.core import caches, services, accounts +from orchestra.core import accounts from orchestra.models import queryset -from orchestra.utils.apps import autodiscover from orchestra.utils.python import import_class from . import helpers, settings -from .handlers import ServiceHandler logger = logging.getLogger(__name__) @@ -39,8 +35,13 @@ class OrderQuerySet(models.QuerySet): for account, services in qs.group_by('account', 'service').iteritems(): bill_lines = [] for service, orders in services.iteritems(): + for order in orders: + # Saved for undoing support + order.old_billed_on = order.billed_on + order.old_billed_until = order.billed_until lines = service.handler.generate_bill_lines(orders, account, **options) bill_lines.extend(lines) + # TODO make this consistent always returning the same fucking objects if commit: bills += bill_backend.create_bills(account, bill_lines, **options) else: @@ -73,7 +74,7 @@ class OrderQuerySet(models.QuerySet): def inactive(self, **kwargs): """ return inactive orders """ - return self.filter(cancelled_on__lt=timezone.now(), **kwargs) + return self.filter(cancelled_on__lte=timezone.now(), **kwargs) class Order(models.Model): diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index beb06d73..539a8614 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.utils.translation import ugettext_lazy as _ ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND', diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py index 4896a5f4..258ade55 100644 --- a/orchestra/apps/payments/actions.py +++ b/orchestra/apps/payments/actions.py @@ -26,8 +26,8 @@ def process_transactions(modeladmin, request, queryset): method = PaymentMethod.get_plugin(method) procs = method.process(transactions) processes += procs - for transaction in transactions: - modeladmin.log_change(request, transaction, 'Processed') + for trans in transactions: + modeladmin.log_change(request, trans, 'Processed') if not processes: return opts = modeladmin.model._meta @@ -44,9 +44,9 @@ def process_transactions(modeladmin, request, queryset): @transaction.atomic @action_with_confirmation() def mark_as_executed(modeladmin, request, queryset, extra_context={}): - for transaction in queryset: - transaction.mark_as_executed() - modeladmin.log_change(request, transaction, 'Executed') + for trans in queryset: + trans.mark_as_executed() + modeladmin.log_change(request, trans, 'Executed') msg = _("%s selected transactions have been marked as executed.") % queryset.count() modeladmin.message_user(request, msg) mark_as_executed.url_name = 'execute' @@ -56,9 +56,9 @@ mark_as_executed.verbose_name = _("Mark as executed") @transaction.atomic @action_with_confirmation() def mark_as_secured(modeladmin, request, queryset): - for transaction in queryset: - transaction.mark_as_secured() - modeladmin.log_change(request, transaction, 'Secured') + for trans in queryset: + trans.mark_as_secured() + modeladmin.log_change(request, trans, 'Secured') msg = _("%s selected transactions have been marked as secured.") % queryset.count() modeladmin.message_user(request, msg) mark_as_secured.url_name = 'secure' @@ -68,9 +68,9 @@ mark_as_secured.verbose_name = _("Mark as secured") @transaction.atomic @action_with_confirmation() def mark_as_rejected(modeladmin, request, queryset): - for transaction in queryset: - transaction.mark_as_rejected() - modeladmin.log_change(request, transaction, 'Rejected') + for trans in queryset: + trans.mark_as_rejected() + modeladmin.log_change(request, trans, 'Rejected') msg = _("%s selected transactions have been marked as rejected.") % queryset.count() modeladmin.message_user(request, msg) mark_as_rejected.url_name = 'reject' @@ -90,7 +90,7 @@ def _format_display_objects(modeladmin, request, queryset, related): for related in getattr(obj.transactions, attr)(): subobjects.append( mark_safe('{0}: {2} will be marked as {3}'.format( - capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb)) + capfirst(related.get_type().lower()), change_url(related), related, verb)) ) objects.append(subobjects) return {'display_objects': objects} @@ -127,9 +127,9 @@ abort.verbose_name = _("Abort") @transaction.atomic @action_with_confirmation(extra_context=_format_commit) def commit(modeladmin, request, queryset): - for transaction in queryset: - transaction.mark_as_rejected() - modeladmin.log_change(request, transaction, 'Rejected') + for trans in queryset: + trans.mark_as_rejected() + modeladmin.log_change(request, trans, 'Rejected') msg = _("%s selected transactions have been marked as rejected.") % queryset.count() modeladmin.message_user(request, msg) commit.url_name = 'commit' diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 68473204..18335ef5 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -1,4 +1,3 @@ -from django import forms from django.conf.urls import patterns, url from django.contrib import admin from django.core.urlresolvers import reverse @@ -37,7 +36,6 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): def get_urls(self): """ Hooks select account url """ urls = super(PaymentSourceAdmin, self).get_urls() - admin_site = self.admin_site opts = self.model._meta info = opts.app_label, opts.model_name select_urls = patterns("", diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 8cb0d92b..5eb55601 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -1,6 +1,5 @@ from django.core.exceptions import ValidationError from django.db import models -from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField @@ -99,8 +98,8 @@ class Transaction(models.Model): default=WAITTING_PROCESSING) amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) - created_on = models.DateTimeField(auto_now_add=True) - modified_on = models.DateTimeField(auto_now=True) + created_at = models.DateTimeField(_("created"), auto_now_add=True) + modified_at = models.DateTimeField(_("modified"), auto_now=True) objects = TransactionQuerySet.as_manager() diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index 2914d265..9c558246 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -15,16 +15,16 @@ from .models import Resource, ResourceData, MonitorData class ResourceAdmin(ExtendedModelAdmin): list_display = ( - 'id', 'verbose_name', 'content_type', 'period', 'ondemand', + 'id', 'verbose_name', 'content_type', 'period', 'on_demand', 'default_allocation', 'unit', 'disable_trigger', 'crontab', ) - list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger') + list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger') fieldsets = ( (None, { 'fields': ('name', 'content_type', 'period'), }), (_("Configuration"), { - 'fields': ('verbose_name', 'unit', 'scale', 'ondemand', + 'fields': ('verbose_name', 'unit', 'scale', 'on_demand', 'default_allocation', 'disable_trigger', 'is_active'), }), (_("Monitoring"), { @@ -64,7 +64,7 @@ class ResourceAdmin(ExtendedModelAdmin): class ResourceDataAdmin(admin.ModelAdmin): list_display = ( - 'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link' + 'id', 'resource', 'used', 'allocated', 'updated_at', 'content_object_link' ) list_filter = ('resource',) readonly_fields = ('content_object_link',) @@ -77,7 +77,7 @@ class ResourceDataAdmin(admin.ModelAdmin): class MonitorDataAdmin(admin.ModelAdmin): - list_display = ('id', 'monitor', 'date', 'value', 'content_object_link') + list_display = ('id', 'monitor', 'created_at', 'value', 'content_object_link') list_filter = ('monitor',) readonly_fields = ('content_object_link',) @@ -118,16 +118,16 @@ def resource_inline_factory(resources): formset = ResourceInlineFormSet can_delete = False fields = ( - 'verbose_name', 'used', 'display_last_update', 'allocated', 'unit' + 'verbose_name', 'used', 'display_updated', 'allocated', 'unit' ) - readonly_fields = ('used', 'display_last_update') + readonly_fields = ('used', 'display_updated') class Media: css = { 'all': ('orchestra/css/hide-inline-id.css',) } - display_last_update = admin_date('last_update', default=_("Never")) + display_updated = admin_date('updated_at', default=_("Never")) def has_add_permission(self, *args, **kwargs): """ Hidde add another """ diff --git a/orchestra/apps/resources/apps.py b/orchestra/apps/resources/apps.py index 36fc3aed..1b5d8199 100644 --- a/orchestra/apps/resources/apps.py +++ b/orchestra/apps/resources/apps.py @@ -1,5 +1,4 @@ from django.apps import AppConfig -from django.contrib.contenttypes import generic from orchestra.utils import running_syncdb diff --git a/orchestra/apps/resources/forms.py b/orchestra/apps/resources/forms.py index 7caa529d..abc4d149 100644 --- a/orchestra/apps/resources/forms.py +++ b/orchestra/apps/resources/forms.py @@ -1,7 +1,5 @@ from django import forms -from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ -from djcelery.humanize import naturaldate from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget @@ -21,7 +19,7 @@ class ResourceForm(forms.ModelForm): if self.resource: self.fields['verbose_name'].initial = self.resource.verbose_name self.fields['unit'].initial = self.resource.unit - if self.resource.ondemand: + if self.resource.on_demand: self.fields['allocated'].required = False self.fields['allocated'].widget = ReadOnlyWidget(None, '') else: diff --git a/orchestra/apps/resources/helpers.py b/orchestra/apps/resources/helpers.py index be1d96ac..c4da0d9d 100644 --- a/orchestra/apps/resources/helpers.py +++ b/orchestra/apps/resources/helpers.py @@ -38,13 +38,15 @@ def compute_resource_usage(data): continue has_result = True epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc) - total = (epoch-last.date).total_seconds() - dataset = dataset.filter(date__year=today.year, date__month=today.month) + total = (last.created_at-epoch).total_seconds() + dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month) + ini = epoch for data in dataset: - slot = (previous-data.date).total_seconds() + slot = (data.created_at-ini).total_seconds() result += data.value * slot/total + ini = data.created_at elif resource.period == resource.MONTHLY_SUM: - dataset = dataset.filter(date__year=today.year, date__month=today.month) + dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month) # FIXME Aggregation of 0s returns None! django bug? # value = dataset.aggregate(models.Sum('value'))['value__sum'] values = dataset.values_list('value', flat=True) diff --git a/orchestra/apps/resources/migrations/0001_initial.py b/orchestra/apps/resources/migrations/0001_initial.py new file mode 100644 index 00000000..17be9bb7 --- /dev/null +++ b/orchestra/apps/resources/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.models.fields +import django.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('djcelery', '__first__'), + ('contenttypes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MonitorData', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('monitor', models.CharField(max_length=256, verbose_name='monitor', choices=[(b'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])), + ('object_id', models.PositiveIntegerField()), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='date')), + ('value', models.DecimalField(verbose_name='value', max_digits=16, decimal_places=2)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ], + options={ + 'get_latest_by': 'id', + 'verbose_name_plural': 'monitor data', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(help_text='Required. 32 characters or fewer. Lowercase letters, digits and hyphen only.', max_length=32, verbose_name='name', validators=[django.core.validators.RegexValidator(b'^[a-z0-9_\\-]+$', 'Enter a valid name.', b'invalid')])), + ('verbose_name', models.CharField(max_length=256, verbose_name='verbose name')), + ('period', models.CharField(default=b'LAST', help_text='Operation used for aggregating this resource monitoreddata.', max_length=16, verbose_name='period', choices=[(b'LAST', 'Last'), (b'MONTHLY_SUM', 'Monthly Sum'), (b'MONTHLY_AVG', 'Monthly Average')])), + ('ondemand', models.BooleanField(default=False, help_text='If enabled the resource will not be pre-allocated, but allocated under the application demand', verbose_name='on demand')), + ('default_allocation', models.PositiveIntegerField(help_text='Default allocation value used when this is not an on demand resource', null=True, verbose_name='default allocation', blank=True)), + ('unit', models.CharField(help_text='The unit in which this resource is measured. For example GB, KB or subscribers', max_length=16, verbose_name='unit')), + ('scale', models.PositiveIntegerField(help_text='Scale in which this resource monitoring resoults should be prorcessed to match with unit.', verbose_name='scale')), + ('disable_trigger', models.BooleanField(default=False, help_text='Disables monitors exeeded and recovery triggers', verbose_name='disable trigger')), + ('monitors', orchestra.models.fields.MultiSelectField(blank=True, help_text='Monitor backends used for monitoring this resource.', max_length=256, verbose_name='monitors', choices=[(b'Apache2Backend', 'Apache 2'), (b'Apache2Traffic', 'Apache 2 Traffic'), (b'AutoresponseBackend', 'Mail autoresponse'), (b'AwstatsBackend', 'Awstats'), (b'Bind9MasterDomainBackend', 'Bind9 master domain'), (b'Bind9SlaveDomainBackend', 'Bind9 slave domain'), (b'DokuWikiMuBackend', 'DokuWiki multisite'), (b'DrupalMuBackend', 'Drupal multisite'), (b'FTPTraffic', 'FTP traffic'), (b'MailSystemUserBackend', 'Mail system user'), (b'MaildirDisk', 'Maildir disk usage'), (b'MailmanBackend', b'Mailman'), (b'MailmanTraffic', b'MailmanTraffic'), (b'MySQLDBBackend', b'MySQL database'), (b'MySQLPermissionBackend', b'MySQL permission'), (b'MySQLUserBackend', b'MySQL user'), (b'MysqlDisk', 'MySQL disk'), (b'OpenVZTraffic', b'OpenVZTraffic'), (b'PHPFPMBackend', 'PHP-FPM'), (b'PHPFcgidBackend', 'PHP-Fcgid'), (b'PostfixAddressBackend', 'Postfix address'), (b'ServiceController', b'ServiceController'), (b'ServiceMonitor', b'ServiceMonitor'), (b'StaticBackend', 'Static'), (b'SystemUserBackend', 'System User'), (b'SystemUserDisk', 'System user disk'), (b'WebalizerBackend', 'Webalizer'), (b'WordpressMuBackend', 'Wordpress multisite')])), + ('is_active', models.BooleanField(default=True, verbose_name='is active')), + ('content_type', models.ForeignKey(help_text='Model where this resource will be hooked.', to='contenttypes.ContentType')), + ('crontab', models.ForeignKey(blank=True, to='djcelery.CrontabSchedule', help_text='Crontab for periodic execution. Leave it empty to disable periodic monitoring', null=True, verbose_name='crontab')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ResourceData', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField()), + ('used', models.PositiveIntegerField(null=True)), + ('last_update', models.DateTimeField(null=True)), + ('allocated', models.PositiveIntegerField(null=True, blank=True)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ('resource', models.ForeignKey(related_name=b'dataset', to='resources.Resource')), + ], + options={ + 'verbose_name_plural': 'resource data', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='resourcedata', + unique_together=set([('resource', 'content_type', 'object_id')]), + ), + migrations.AlterUniqueTogether( + name='resource', + unique_together=set([('name', 'content_type'), ('verbose_name', 'content_type')]), + ), + ] diff --git a/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py b/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py new file mode 100644 index 00000000..5e2bc3f6 --- /dev/null +++ b/orchestra/apps/resources/migrations/0002_auto_20140926_1143.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='resource', + old_name='ondemand', + new_name='on_demand', + ), + ] diff --git a/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py b/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py new file mode 100644 index 00000000..ee2eab56 --- /dev/null +++ b/orchestra/apps/resources/migrations/0003_auto_20140926_1325.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0002_auto_20140926_1143'), + ] + + operations = [ + migrations.RemoveField( + model_name='monitordata', + name='date', + ), + migrations.RemoveField( + model_name='resourcedata', + name='last_update', + ), + migrations.AddField( + model_name='monitordata', + name='created_at', + field=models.DateTimeField(default=datetime.datetime(2014, 9, 26, 13, 25, 33, 290000), verbose_name='created', auto_now_add=True), + preserve_default=False, + ), + migrations.AddField( + model_name='resourcedata', + name='updated_at', + field=models.DateTimeField(null=True, verbose_name='updated'), + preserve_default=True, + ), + migrations.AlterField( + model_name='monitordata', + name='content_type', + field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='monitordata', + name='object_id', + field=models.PositiveIntegerField(verbose_name='object id'), + ), + migrations.AlterField( + model_name='resourcedata', + name='allocated', + field=models.PositiveIntegerField(null=True, verbose_name='allocated', blank=True), + ), + migrations.AlterField( + model_name='resourcedata', + name='content_type', + field=models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='resourcedata', + name='object_id', + field=models.PositiveIntegerField(verbose_name='object id'), + ), + migrations.AlterField( + model_name='resourcedata', + name='resource', + field=models.ForeignKey(related_name=b'dataset', verbose_name='resource', to='resources.Resource'), + ), + migrations.AlterField( + model_name='resourcedata', + name='used', + field=models.PositiveIntegerField(null=True, verbose_name='used'), + ), + ] diff --git a/orchestra/apps/resources/migrations/__init__.py b/orchestra/apps/resources/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 108e0b92..af2de256 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -43,8 +43,7 @@ class Resource(models.Model): default=LAST, help_text=_("Operation used for aggregating this resource monitored" "data.")) - # TODO rename to on_deman - ondemand = models.BooleanField(_("on demand"), default=False, + on_demand = models.BooleanField(_("on demand"), default=False, help_text=_("If enabled the resource will not be pre-allocated, " "but allocated under the application demand")) default_allocation = models.PositiveIntegerField(_("default allocation"), @@ -116,12 +115,12 @@ class Resource(models.Model): class ResourceData(models.Model): """ Stores computed resource usage and allocation """ - resource = models.ForeignKey(Resource, related_name='dataset') - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - used = models.PositiveIntegerField(null=True) - last_update = models.DateTimeField(null=True) - allocated = models.PositiveIntegerField(null=True, blank=True) + resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource")) + content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) + object_id = models.PositiveIntegerField(_("object id")) + used = models.PositiveIntegerField(_("used"), null=True) + updated_at = models.DateTimeField(_("updated"), null=True) + allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True) content_object = GenericForeignKey() @@ -146,7 +145,7 @@ class ResourceData(models.Model): if current is None: current = self.get_used() self.used = current or 0 - self.last_update = timezone.now() + self.updated_at = timezone.now() self.save() @@ -154,9 +153,9 @@ class MonitorData(models.Model): """ Stores monitored data """ monitor = models.CharField(_("monitor"), max_length=256, choices=ServiceMonitor.get_plugin_choices()) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - date = models.DateTimeField(_("date"), auto_now_add=True) + content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) + object_id = models.PositiveIntegerField(_("object id")) + created_at = models.DateTimeField(_("created"), auto_now_add=True) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) content_object = GenericForeignKey() diff --git a/orchestra/apps/resources/serializers.py b/orchestra/apps/resources/serializers.py index 64fbaa4d..a427fa62 100644 --- a/orchestra/apps/resources/serializers.py +++ b/orchestra/apps/resources/serializers.py @@ -44,12 +44,12 @@ if not running_syncdb(): msg = "Unknown or duplicated resource '%s'." % resource raise serializers.ValidationError(msg) resources.remove(resource) - if not resource.ondemand and not data.allocated: + if not resource.on_demand and not data.allocated: data.allocated = resource.default_allocation result.append(data) for resource in resources: data = ResourceData(resource=resource) - if not resource.ondemand: + if not resource.on_demand: data.allocated = resource.default_allocation result.append(data) attrs[source] = result @@ -64,7 +64,7 @@ if not running_syncdb(): ret['available_resources'] = [ { 'name': resource.name, - 'ondemand': resource.ondemand, + 'on_demand': resource.on_demand, 'default_allocation': resource.default_allocation } for resource in resources ] diff --git a/orchestra/apps/resources/tasks.py b/orchestra/apps/resources/tasks.py index 414e7edf..5719f8bd 100644 --- a/orchestra/apps/resources/tasks.py +++ b/orchestra/apps/resources/tasks.py @@ -1,6 +1,5 @@ from celery import shared_task from django.db.models.loading import get_model -from django.utils import timezone from orchestra.apps.orchestration.models import BackendOperation as Operation @@ -34,7 +33,7 @@ def monitor(resource_id): operations.append(op) elif data.used < data.allocated: op = Operation.create(backend, obj, Operation.RECOVERY) - operation.append(op) + operations.append(op) # data = ResourceData.get_or_create(obj, resource) # current = data.get_used() # if not resource.disable_trigger: diff --git a/orchestra/apps/services/actions.py b/orchestra/apps/services/actions.py index 0b84330f..33186f31 100644 --- a/orchestra/apps/services/actions.py +++ b/orchestra/apps/services/actions.py @@ -1,5 +1,5 @@ -from django.contrib import messages from django.db import transaction +from django.template.response import TemplateResponse from django.utils.translation import ugettext_lazy as _ @@ -7,8 +7,23 @@ from django.utils.translation import ugettext_lazy as _ def update_orders(modeladmin, request, queryset): for service in queryset: service.update_orders() - modeladmin.log_change(request, transaction, 'Update orders') + modeladmin.log_change(request, service, 'Update orders') msg = _("Orders for %s selected services have been updated.") % queryset.count() modeladmin.message_user(request, msg) update_orders.url_name = 'update-orders' update_orders.verbose_name = _("Update orders") + + +def view_help(modeladmin, request, queryset): + opts = modeladmin.model._meta + context = { + 'title': _("Need some help?"), + 'opts': opts, + 'queryset': queryset, + 'obj': queryset.get(), + 'action_name': _("help"), + 'app_label': opts.app_label, + } + return TemplateResponse(request, 'admin/services/service/help.html', context) +view_help.url_name = 'help' +view_help.verbose_name = _("Help") diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py index 555995b2..3334b5c9 100644 --- a/orchestra/apps/services/admin.py +++ b/orchestra/apps/services/admin.py @@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.core import services -from .actions import update_orders +from .actions import update_orders, view_help from .models import Plan, ContractedPlan, Rate, Service @@ -52,7 +52,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): ) inlines = [RateInline] actions = [update_orders] - change_view_actions = actions + change_view_actions = actions + [view_help] def formfield_for_dbfield(self, db_field, **kwargs): """ Improve performance of account field and filter by account """ diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 3434efce..c26eff6d 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -4,12 +4,11 @@ import decimal from dateutil import relativedelta from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from orchestra.utils import plugins -from orchestra.utils.python import AttributeDict +from orchestra.utils.python import AttrDict from . import settings, helpers @@ -21,6 +20,8 @@ class ServiceHandler(plugins.Plugin): Relax and enjoy the journey. """ + _VOLUME = 'VOLUME' + _COMPENSATION = 'COMPENSATION' model = None @@ -160,7 +161,7 @@ class ServiceHandler(plugins.Plugin): return None def generate_discount(self, line, dtype, price): - line.discounts.append(AttributeDict(**{ + line.discounts.append(AttrDict(**{ 'type': dtype, 'total': price, })) @@ -182,7 +183,7 @@ class ServiceHandler(plugins.Plugin): if not computed: price = price * size subtotal = self.nominal_price * size * metric - line = AttributeDict(**{ + line = AttrDict(**{ 'order': order, 'subtotal': subtotal, 'ini': ini, @@ -197,7 +198,7 @@ class ServiceHandler(plugins.Plugin): discounted += dprice subtotal += discounted if subtotal > price: - self.generate_discount(line, 'volume', price-subtotal) + self.generate_discount(line, self._VOLUME, price-subtotal) return line def assign_compensations(self, givers, receivers, **options): @@ -225,7 +226,6 @@ class ServiceHandler(plugins.Plugin): def apply_compensations(self, order, only_beyond=False): dsize = 0 - discounts = () ini = order.billed_until or order.registered_on end = order.new_billed_until beyond = end @@ -296,7 +296,7 @@ class ServiceHandler(plugins.Plugin): cprice += dsize*price if cprice: discounts = ( - ('compensation', -cprice), + (self._COMPENSATION, -cprice), ) if new_end: size = self.get_price_size(order.new_billed_until, new_end) @@ -323,11 +323,12 @@ class ServiceHandler(plugins.Plugin): discounts = () dsize, new_end = self.apply_compensations(order) if dsize: - discounts=(('compensation', -dsize*price),) + discounts=( + (self._COMPENSATION, -dsize*price), + ) if new_end: order.new_billed_until = new_end end = new_end - size = self.get_price_size(ini, end) line = self.generate_line(order, price, ini, end, discounts=discounts) lines.append(line) return lines @@ -395,7 +396,7 @@ class ServiceHandler(plugins.Plugin): dsize, new_end = self.apply_compensations(order) if dsize: discounts=( - ('compensation', -dsize*price), + (self._COMPENSATION, -dsize*price), ) if new_end: order.new_billed_until = new_end diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 80e72eb7..e8dec4f1 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -1,24 +1,18 @@ import decimal -import sys from django.db import models -from django.db.models import F, Q +from django.db.models import Q from django.db.models.loading import get_model -from django.db.models.signals import pre_delete, post_delete, post_save -from django.dispatch import receiver -from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError -from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import caches, services, accounts from orchestra.models import queryset from orchestra.utils.apps import autodiscover -from orchestra.utils.python import import_class -from . import helpers, settings, rating +from . import settings, rating from .handlers import ServiceHandler @@ -329,7 +323,7 @@ class Service(models.Model): def update_orders(self): order_model = get_model(settings.SERVICES_ORDER_MODEL) related_model = self.content_type.model_class() - for instance in related_model.objects.all(): + for instance in related_model.objects.all().select_related('account__user'): order_model.update_orders(instance, service=self) diff --git a/orchestra/apps/services/rating.py b/orchestra/apps/services/rating.py index 369e5a57..4b6bc0b6 100644 --- a/orchestra/apps/services/rating.py +++ b/orchestra/apps/services/rating.py @@ -1,6 +1,6 @@ import sys -from orchestra.utils.python import AttributeDict +from orchestra.utils.python import AttrDict def _compute(rates, metric): @@ -30,7 +30,7 @@ def _compute(rates, metric): quantity = metric - accumulated end = True price = rates[ix].price - steps.append(AttributeDict(**{ + steps.append(AttrDict(**{ 'quantity': quantity, 'price': price, 'barrier': barrier, @@ -109,7 +109,7 @@ def step_price(rates, metric): if result and result[-1].price == price: result[-1].quantity += quantity else: - result.append(AttributeDict(quantity=quantity, price=price)) + result.append(AttrDict(quantity=quantity, price=price)) ix = 0 targets = [] else: @@ -139,7 +139,7 @@ def match_price(rates, metric): candidates.append(prev) candidates.sort(key=lambda r: r.price) if candidates: - return [AttributeDict(**{ + return [AttrDict(**{ 'quantity': metric, 'price': candidates[0].price, })] diff --git a/orchestra/apps/services/static/services/img/services.png b/orchestra/apps/services/static/services/img/services.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf8792cb54ab665bc1ed5fde4e532f2008aa889 GIT binary patch literal 93392 zcmeEt_g7O*)NVpBh*FHG^kS$=C$vcQl@_XnE=@|14x#rT3Q?qm-c?Xlodd^$m*kR>hs70 z>Er(dL_8{m{A_FN{bEzfmIoHs7 ze2us_3bme07Jy}hOqCCZ_WkniZ*`o4$t8_0ue78>sbK##ydT@QaZZW<`Eb?=r1_sO zTohdW@r|1GEHjV z6FRo0nZ!yr>J{a2u^4h$nq5e5L7ni}G|KL_kExRGPt#6!TdlpyFe~@`cR9O;`~CtcUv%)8Pnb)20g;D9YMFq*@RQT%$c&7P z(SuY_?ngy#Q76y2Lt_G_dwUi-leWv8D6K_$hMiwrd&xWV6(g?z?O*;{$*tBQ3h|QJ z(C{CSyv5fi;Dp0v1dq5^;2`{_gCj#rE?SwD7HxNxp2D<9o`^q)bAc$_5}zo~2Yg!* z`)T_7wuY1_Bb9KamOUi0h4}C8!@t6?(1aL{N>jfV%NQvdBDa5)!_KGp6yrM4`&~;r z<%l2z=~L;JQt~hQ^OPO6xy{kq=C=qwCmF;7BIrWEhjLj5rrua+=ng zzBL_+7)CTw2ULYAX}iBx;#Z=2&0jthj9p9g0dExJ9@NMVBF65K+6($%gelB*tam2~h-G2MmBbw1V!FG6>t))}cR*SsAl)s!{ykV5N#!Tr>1I+ZY7(4Bb)%%^tP3^CUd+xP zdZ|8fGi~oZN0VA$>!l63-rghmfq$K+%PI>B+#W*M^XYZ3=c>;qZVDzuyzroMNdT=C zbp4_Oj1RB>(d=jS)G(bDC)X9i6oc2SRj6q zCEOsZ9xM~SLn{3XO3~tp5m6BGkK_F%mJl%(h=DI^O)laS!5re{CU+c64?nhz#~c{# zPdeTOMr2eWo0PCEl6|up4Zjq6E>~M4R>izS2wCYAVIrJWhhs%=T~IF5RqI z=t(W#{~1XSfNW;sYS3o9GksNo2&s{cn;XXhZpx|EkcN_(uT_BsTe~lUD&AHf-Y31Z zCp?)mUiX0hvQ`*q!hXd9qt}vQtKAvo-lzfWUEF}2{)Jyrk@QH}>t6r5uM#H6<{R8) zv|dl)EY#Mb<;p(@WB{v#f|<;WGYT#L+=8uQ?%E&Jcrq?QFzN1#qOQ3s@d=O4Ol(dR zFVM$2Tqv&#n!wbTs%KR~Ht)vB#slMxWM!@fGq|Dx?%5q~)a%DBJ%ZZ4#{KLXm9gfF zH0enSyk!hFPL<|^hE2vyNW+$I{09~q_a50Wpr-G9;ykd`q>S-W#X~j4(UAvNf%?8c z8XE(t5l>KszM7#hDkNzh82$C?ixfUl(-sCJ>=3{`9wa2s&dl zy;l|Z9=8ge=^d+#T#Ik_8_@RR~|Qw29Ddj!2uzgUZwG{A0QEI91lpM zNv1nLm7hN!JxH8xe@dYE$>r_82nMfQR1(3h%iyDo!T38LJHE5~vlZ+>Agv?DqT>_+ zF}|rZHdhB=;@?>KxY!^nIUEa1ANfiRCiTE0^@jh{^FW8hj+s-cWhzYJNGC*0;wNwy zaMhMSn(*0PIoeXVXvCN_#nu4QKI|DFc@?!L8lrln2vNE>0uZ-GU*gn>9hg|{8|^*A zH9sYHXZWXQFag0(@eAY?x-O1uJqWgY3?X$uS@C1A&KOsHH8qtO*ow#Tt_APXQxt1Q zjQuwxMCwX~*v4ZUA2mgRrQ0@~)8*K(3nw|fMG~-#%XYz4mpb|g#gNTn9243DRHS7N zl#ix65V_2uD)1~M-r@#GRV{>XNyYwZPafjvLdJ8idp1+u!ROAm?y(X~bf#_!TmLdT zJPge!F$4yldcjlPWHkLlu1Xq} zgk`;wcfw_4?A~&i_Pfjp%1MjhXLm{E&(SN(@t`aI3hdPAU@Y{pE|NwZ1EbT5cpp!| z-P;4BzDwS}BM)WBpx8O4TlYC~cy}D)WecA-pZ2af9R{iJ_1y6*%AT`?RE9KopR#f0 z?f1K0frN{P*2m4`FaOz##qc&Nm%tIhRGR+V8wOZMZF9~8K~|Bc{Af$wnf(I2j}J3_ zJJmb(wX;|wdLnAfIgYL1ol8Gs`Bk?%1v=aGHEn<>);=HmnXknPP+N6G42bdPUg(xb zA$7R>yFjz=QKuh!c==k?mP1$*i1KaMA>oT;7RoaE1fxLXl5*Cr1DC33sy3=x(Sahz z6Fy%Mu_ufh<(^QWW7}_&0>&Kr#XLb37?UV8 zE^!CUs%!SFS)e?jfrATYNBg;lV3y)1r)_?9gna~HnhOmtf?T z=+ds+EV11T9!YS?GZsjAX?36h=?;ORVB%px-=O`KMFRHt?OCgS+Kw1J|H-Yb1s^y( za?#&jg4{MI0|pc&pJ7f8WvE!r`Fm`|9>)deYI{PV@A#S&lSN*@>zns_=GbztLzsUy z)`_#eVbPxVd`g`^$)l6V#RM@Ko=VCvvnI1rfZEVP@`uGD>|hbQ>`Tn$p0hx7TY|q# zI2Yu&E`T%fQJ=MQ3Z^VJlnc;nfb2bg|8X{}npxtpTJz7n{<&pF!L+<)^V|Uo>tfQp zcgt1Cy8rCR67j?SfK2M;^o$I%bBQ<0_b6Vuumqc4*1C40hQOztiUZeESDEOX`9oWE zgTu3$2jRw~7XH~YzQi4RguhPEJH=MMrGXmniHBQ&ALH%ea$o#oJ_p|L=*CGzfm;*KhO z%@G^Mv-A^HR*4l8ZUAT2oMz+3yPCcj0N1sT@VIv2g;wK-ZMVdokVx6mP3$GgQM&BT zQ&cxV!=XM@?*L5iyVub-N7%0DLArm#}6C(*7I-?7wyIyp++v_3sQy6Mx)VOCS?;b2oRR-0Nt zPjq0;@7`B=!#jGDEHetI*CjbZZCwjI@*`$h!<~9}?A_yvFAzV>?fa7cp6$!|itUt` z@2K${K$E)ekKHLSrqLdI9%s|uU~k)Y7%`CGGN~evMBtRBJsrJ1@_s9Xr(r&L4psJ3 zXn8y0R2zI8rPOuz;KkhGyMjI@d&HOP=s~owa}Pw8XZYOXURl<HsKMD=4nS zvaKAKr>|S~LSwWot~>HX0P0tsvn+rZOhn9_MN0{Z!nwR@+^U(n)WmK2@oIAbxc;q3<$ zj-(SHJlDCvrlvWG_A^p5efMQAacT%ps*zs(q{j|XommHt zvG-7MiCVW6c8|s1M&Nad1u@xI*KS+aB}!|S8f;TpXKh*j;X-(8XO`9-KRJ!Waq{`r zC>AS}d&$-)z=geLM7>C5$7As!qER6p&g{^6eo1+-o{qc-`f6tCbCO z`hs8P8SgB*iFan~PfS`Bc9{y6s&_fL+{MV2RvgHzsg|F-xK89tvIdK#>?myAd3_-& z@ulcKa6YYCL+zK46H=c30gxxP+B`FqX*2R}u2RK&iNUsXLXh!cK zIQ{Mfh-kpCp zNbBMP!H*y^cQ|fu^dc$`>p!$0praiIg)*f1z%$mL@NdPPvJ;dD5QrBd>o`bc<6ad9 zdB#5F6=hm$$Gi-%b3AE9ffDu{o;ork_fIlI&SCzf=!GS%hqS6{E0$<m%&P>NOyD zBA&CKChjFh!?rU#aZXXw2bJp|f$v#vgD!cZYC8y|b9Nnddi1@BzOLtqc3o!7v8|`- zqzUrtWFwuaT)xP=Aatn0QhTzG&_+4t;EAuDS~Km6H5DUL=^;`yG@@k0HdIZ`;=yg! zRo&DFvUu5zzdmbun6K{+e9hPhDKEke@%Ih#m+u>CRs^t4)Tv`Ssco-ec!!C+wfTR1V z87LdseCNp(`1p*DeZ^Cej2`k8n-`b^Z@4>;r$ylDW$4Tgb8B|IbpI<%+mHUr+pN@A zJM;Nw{#*bGR7owG#%qf1~c+6eBpUdny zoxPH&RroW7ee51jWN2qX#FxB;p438K9U_;Zne&f&t4@$NkR6aVY_-K}Q;rG9B&w0T z2nm=)>1iG(K2M1XDhZkmDo@tY^Xt$V(%9xg0`OY~aa2zS=#d!F!JB26=u~~oRspII zl6aoo;B(c}HdG)T_Y=(xJO}Fo!VqF@Y#L|5hY!>gzs)GN6Gt6CVpWE-hy&zpGAaQO zW==npntdssIMAylwHPSw`Bd%K)Xrz$<|3u;;Gp8G)@xF z_3EO^$woUPtWj+8WS&qtsl2Y^ znptMvG$^OhxH1fWQIYDvD`}gK%J?RTy)y}hT;uC0W)O6&vLN9mUd`Q^R0ZED6L1t1 zy;MSb!kONLIssIP{4pcj1Om+Z?A=gKJ$Lo`C0}#g&(rT8V>PNRR|UI>xsZ@Ax5BJN ztv|-66cQYl5jMwSafi1z01Xx{60E9br6;eyQiZ#b<~dN)VK-S9*#IR#3u=zx8AV~> zaoQ|Wc)^UbzBp;%bJ?6@5NpK)_&^9@(tg7xAZFlmIMIv~pMZa73@qdGUjAj7YhdjJ zyPV;(Z#q4gGVtfCy{BF0jddq>sq((Bh6?twk+=~c0^<5%SMJbr=w1p=c=VM?^4 zJO$A0qH&Q#h3`LcDZmprw00>)U+)EuyCx4q^89Ht-7Sl@HNlADo~q(zM0-Z?`$ z3|jl%O(ptd)YYuAO|3_0LT4}nq3KC-f$UHe2u2A6$g#akADwB^@tFG7i* zx~YxsciVV)ptOTvZKWO3LP?cwyc(o%q2EN{57C7As-I1%_N z^;49s=9c?0HFPEvCD6GhI+km}t(!LR&B({lZ|QPWT6vD>=mFSM8*g)OQ%UL^0n?Og zU>E@HC&FwqvC^ufB5ibx6lT3-=F9cnh%hf&Cad+7i1VRH`Z>k*1bFbD(+r4de2KJl z1STN1*8{JUG$9BK)-o?|*3a*Y$+^JZEon#0)3ZM6P1pOGE&Ugb&_HLJ2>!uM+>e^N ze8(b#nz(ZUV&k?7C*GAjx`S=b`qkY>AStcr;FsR_B>6`@mU~rSN%8z~%L=R)>$bGi zehrQS=i+ux2o5Qza$gWSBPd|4RW_w=Gb6k_$u3p!oSt`qy z7_m39g=e`Z7QFZ&KFjs-tKOz>-4lAXqIzIO_eJuy1PuPzUJ>F|iYq1BbUeUL-gKlr zy-+XRm^*oTTj@MI!|XD{-_@R>w`e^5Sjp)M}fG9#E&u#ESE_B66o-{T*|B!&U`ikbTLsWFyc9t2t{Im^Hmj1Sjvw|>Q{)4i}qerE& ze0>H2*Qo1x%?-uU=@vM31;*F`uVXR&YsP!bjKF(*p=G(SH`=7;8ilj?1lm95ePX83 zx;ye8)-Et4#5wGlet-H1 z5A@GZ1luZ2V$b|h$lOi*hZo)y%jsqL3BU@bWHPuk4I1jC^hP6^UG_bsgiq{E&KASt zIHXV54`!3)Jt!%Sc;fQuI7%=;QCkJ1U%GmSMg9S)L~pEDJ=H@IM}Hc8EgiPRi`t)5H09W1k@xGDX69)8?)W1M}~| zSEQ#O7pyqrTSbMI21?5wG4|>v{esQi$y3X6*VX7)wukq&X|qwhB^@?QU(DKh{+M9K zuc{10c&7}w{oEp`W9OYn_5wEUHqXMduJ>SpZ;6#0MAyuJBj>>~Ca7)OTJFFY*TjA7!lzHERF(5hqSp{b^A-q7+Cl}n5)lx!N`|~t@4%r!*Dhaj`}&) zd^!6kU&HSSECZ-4%}2y6Xj8B1$HE_S@`1rs_tb3G$H`&zDLaO07LP{T!sZ&4Z$NhW zsfv<-1^HchgTDw^^1$8=sQ_tsHkjk4(-h|5eJc!VmAqp)&5b>aKl#<7556_T)04aQ z7`&yG8JAsJai!t|Zl7I61p!M5ZAY*2=Ws(}Ie5dV&cv23pk%+e+DkY)PRkKe=N9~Y z)f5e_*71Zw!avwrP`^8$R!X(}#s!<_I2q_nRCn-EB(9Q77ukuf>l&D(ApQ6vnFfZm zW1a8{&w!LHbV%YN8f;9&7Onr`Imm)h(`<=A3Re z0H^FNr0nj23@4GCsi)`OBqd}~N~frwesy6*2RgG}KsW7AtXXSE9}{m=G1orzuEq&> zp$Si=)+yNa2(p?a)XQITGfCK!&fBF`nRyFo6^b58uC`LxPXsFi|s!0xnZ9fd~L@(Yrl*9 zk}3J=YYd*>Qqwqpq}n3Bsf+LB^3XFdNPCLSKYziQv>wxyeFgtxKplK77vOh+fr^V) z_!-CKs8pt`Jk5cMjn0;%L5XRXatRj* z?29(+_X?*KI0cN*20-d#PgWEyZyVFuotRzmrm~B;+|#UN=?WZ*hUY2Xq%{zJAm`x|L^3PdtL%w2|S+SAf1Oluy$$PHn`uOOa?hvlnWaM<; z;bp&DmxBRvNWgijN`(^nnrgGqRZ1!=$Q*A0_I|8{p^ZraZ3QRZ4zRJh|-|4%)Kn*Y@T zjOGx`2nB>B?AcG((;6Ii(sBy7c>EN?Jf%4@STHc{_&TEL$C#!0s$<~FuP?z_1tsxe z8O_2$cRXb_usA!uz9C#66;|wac{nRv@7#P#>>w&9XP`HTEHurFP`ceRHkIrk*_70# zKshtVsiv0u!qRs&tsXu>A_uF2at^Ii%FD445f_L~pF5Mx=Jty@n}X*Ky~#eati;_f z{)i)2wO=Oi`Fp4%ui5jd8lrC(sLN@pJn>Ote5y~71E-hokx!a7w#KE)0+bNQlKa*! zXori?h&7JI_-ki1bnW)XepnYzd{m-|#Vci|l)>;XuR!c{;$Ax4HbLX&x~6U=G{F6M z(=<<7>yD`NlB+G7K8{)=hrDxtQ#RLN3C5J>AjDzt5jlCRf!9`23jIy1u`U`{1LSj4 z)2#S?%_b4UqA!+WnwA|a!L(6?y;kAX+TJ7CnWBeJI%m!^5D3YNx6nClaVAw^Mrt_} zxC?4Sm>KvZ>DRH#xip#`GZ4w=y02tYEk1kc7W93mkZMLY?U-~qXevL`(rGpOf;65B zgOM!VqTR7`0kS7|auB#6l}7A3P5RO_?7aev{|2m16OU8Y{fm(e@t8^iaIWlE6v3p+ zfp$FkUM^n`lp#gB+BKM2{uu{BoIWI~H)4b6lIpse0OQ(=!Q|f#J-CP0*XYQi@T|&< zUxUXQLnZ{F`eiNs8T=nlxx2b)@^wD9#dhBeHgWl(*+D~yz^2l){)+X^%8D=?< z?se@({bh+t+E+zurx0L{RG|TO1QRoLV>hymDg}M7ub{@Joi9n6#_aPI^YR#XLzDCi zriP?(AHJ&QV)ZPS|0wdHzdQ;I&#(*(FLf6pb_|IaoIh@DRReS9g4R}NWE6VZB;bXR zHg&O#^cN*Tn`%jRQ6~N8(2UvN_o~RhJgYL*Cqh=&-cx|iuE)QC$5;Ms@_R)U=wzBS zmHg{CMaEcGP+hW{H&wJo2YsKFE{T9)8Frl_Uxm!_P5d*2Re4Ba?j|K@9}%3Z!Ilzwu7N-!AzmaICunO)1$Z_lw_A`D34|g1zA18YJhlo|DK;Z z9>NYwYsGG7>Dm1EML7d~C=vfy?}cA_{a_Z*$>sW&B~qJCHczCSW+iM+G4-vz$p@@( znC&f_2T~1W(Yf7Dpuc*@+nO^s-TBw)A6(6oO6a~=47`8 zlZ{&7DkxJ~}{cLYi9HAt&h5k}(g?Aa`ecoT-sD%nki9Cdp&1}vw z@-0K|*QZ~PRr)dl*ws1)u{Yk?Av7GLG&{Ugwk;S-zQ_8}7}8i^WiRMc&%2>kTnPKT zGrfN_5KW*q0MeZ_f4$09y*&c*z5JtzR_2S|?_QTP(B8VE%G4wAB|<27wUDCyAg8&h zp(V)ooNz(Pp?2Z#*(OVmBD}pPGkpbedt4)-W`;Wkl?=MlR2VoHoAB#^s~EAe-$sj^k-O32*)3)8D)cEY?rR zuM6vVXN(Y+lZf2L&bPbu_o z((L>@yQon-LDdE>ML4GR^w{-xN!s%U^`!0}+=RC;*WOq!@+ymm&SZ?WvJx|#0pc1R zO{M)#$iDw;0X#a=jFn{qD8dd|ua-wj+E9?qH~XnHv{D!UEp8Aam8Ri#X!0DJU_yf` zgV0nwp#L={l5f?*YSv-80#n~uJq{e<2oQz(iS@X*(u&;cwAp-v33#vGrU_6HpozZM z1)P}DIiD=&Y`O?vo})z>cfKVwlF{1`6tXW?v>(P*q&fmF>hWJU&S${m*r~Nq~lCN3iHo-9^tUtO;4y11{C4TPW{7z{@h} zko*X7WYyzP6k_NmWYcD~K7!!Ub$6|{); zqN*4pc8d#=`byZLpZ}&|+T)#8fUm(}ljEn0t27cca1c+o-6NEQ%wv!JzIdy1S66+8 z6T!k&@vdyHkBKQNV4zEqgUS9!PK8(5fhcZxeRgCoEsDz%5*Y1w0aZvulaOUwV*|h@ zP+Ah~Avgi{L>3C-$dX+eH|c4pr9z61-{48RIzdZU%Bg}Di9WjMDV8x{7eyL4zvDtpA6sV?pqH!-Qm*?Y}bgdFiuPOX&WDM?JG#j;Yr^Y+>g7WZq zTh_^jopbO`5Y2x{uk+oiDOj4C@MX5Zw!O)(=)!{yq=V>z_9dGk;-flY6Z zli{TJ%d!$}$q)NubAp#rb6GG@vu333iswB>D;LQv_x)zky>P|@%PeKhU~G?4=4w_X z9a&_|eyWr_aeTCA6d^eNtvrH2OqmLF1a2Gdi6g*r??RMmekcd`{0wt!HC3(ybrHG^ zggyyRPj`E%xMoMw3ApKiWvJxy{oL5xz9s;rjE-7e4t2pB6XsKFI={fiwcVfXT@6PJIr?pX(L&nwIiwiCkRJj#!vHqQ$f4oWvR_YCo)gP?`87z zJ`u?okAb1T@b?Yd@3(w!K3V=^&y$~#gw8{lBS?bb-WnY;kjmf~SylZ{Y1Qv>qdkSu zM4+>Rs{vTi%C+)b-bM+LGpDyy*3vZxX2M{GiQO_&F5mZ_3%I%4ejd0RpEBS$yS*#a z@MF55`;4Y*KtCaJUtRc^4aUk=cGIyTn;F8b*`Wm4oL%hdt8hCE;)f4Dxrz9Q#RqeN z&hGDdm~G9d_9LIl^FYsj9b{{E)ME}f8?wQ7P(9K|LyO0G{1XC88d8hjO$A^ktCVbK z?*rI#x+}o@%6OGq1_rMlUg+u#LN-pWp($*w$4ks9(f13|w>*({r?E6ynG?|ueL9Ck zHK;1Qeu?S7o%c&mEc&S_gXsry$WsTFK>y=I_r14x3n~>s$Ezpl9QLi+VxjW4pleP; zswAX+8Rn2^Gfn4kdhSDLYZ@&o6QOa1E@kC#z|uLcOet3i-a4e7#9-%7GY4W z0328P&gZDErYO9`Uq$M>YX7Y$W0m*a(n3M9h@0V-Sz3R16fEIfcI~~J$1bcMA7c3q z)wBl&J>$(em){9@RFu-N962ZO=zJ!q9?({bSe&WTr?fX zOwH!MmitP3=~K64BvGXn$Q64d*3TZS0QyndlWeG_-XV) z7yoS&e=m!VtXC#>Z^AKax{i4%w;W^;c(slZ%7Gs<_`tXhPcU{o4Ug^(l@Mmd1Q#dS zJ#HBaQpJzT&Vx$s>(6qb>~Ss zKYbfp4v(PZg%fbUzc)6o1=(B0PfDwJ=He*Z;v%SvuPBzEGk)-}BqtR*V~422<{g_w zc+@oBz=CBTzoO>gS@NAGV_0~d7*HR&7pu;(TfXI>As98qbz+uqhfJQdtYznZBPLxS_N^W zt)^%-VV!DllAN7uZCUFn5l9ohi(z@>0%HzL^Iewp-M)@`*-;n4#P0HopPrv4#zeED zFJ<2+5x1^&q(K~(03tpD_x&sB-YC?9T5mhGU=HeQJ%lo@dD^%g_rA7vul!!b--XI9 zrIIO6Y7j)0T5prWR7)8hshYIMgff%oMckAbf3c-tW0;n%O#j$oJ*x1FQ%8p`)<2rc z8scS2X}u>Pm(%JR9#2OV3reU~Hhbp#R}rD(Y*A#=RrQY1aBuydSOE+0m%x%X%ISNj z{pSI7;?+`YwV~)m^`#z3f+Tj*_GF-I&c1+lvFliZ-P4BF=QUu+=AGFI*=$;lTcv$xQ|B)@9s&`!k_)N*mAdMhmpsIHGIMas^WHEKB~7a4T-QA+eo?P z`x#R&3sqJ}#;^C%+fU8sFFSQ2hvL;cCddFX3URDuZbs5VSo0RSmuqYO)2xx@f_@aC zz~?6H1VQL%jlJkS3O7~zMGL=@S}oHs8B%19{jAOx`eCR*kI#rnI!vp-!~90Q3F!j1 z3`#R^k_2jLr?Hs-_j3*7?Dyhk@~j83I8i=PAtxn7G?pkna3M9JItFJ}9NUfF2trLC zf7!}4#!8!$Waik6;<+`!YI|uu*|?wleS%IG5aWo(T#+m@#^fDSwQP;~%6fH@z}z;z z1lJM~o>0L1(SOEKG%w{XVGnzwbR|olv`5gz`eCJ?K0G$bzRZCzj!Dnpn3mJl7h{I} z%)T@wSl)M(;bU!w&$=~Krk~`9nzD4*3X< zXkXOYlBr63(M%SVPl&@A3Tt*y56boA)D=V+Bre}vkH^RIx>HzPBPk7CJ9}OoNSx_Sb1bJ{n|SxLyHbJ>gV8>u&_rOG3*i*^x_dqa0`M zo>q0@r+jp0&vfH=cP^B`>O*_NJ4w$oakxxa^S0qwXv>b9GK%dymQ3)vE2XvZV5!}u z#t)Vh`Fpo(YL+qS+$4MWkH2RFateBGVEWqPj;DfWXv`}PrqbT@k=&E;JyEIOzrXLk z(K&1jkmfvZvl?O95KeMj(-+eV$SbMd+~eC>8lKzLkHc7YpJ(NFH*h;kKDK`%RUz>> zCM^_r;DSF_=y`})Psp29UldXgR4{9qsnXc$-oiVHSeGsr{*r-U55C19v(6<&orW=c6bO{3k` zrE@{0eLQN_Nc6wkdVhQuT4ADg@&S=W-!;l{dC1T5u70^iwmIQ0=^Xp$?Z>lgka5yW zsg@l%!si-G4{YS?eW-1KWBGS>N>8#He^M#~l#CeVcl^SOGcRxyehXye_aG@Uh*(*J z?T|_f91O_8s86bPf93=qXgH`xJzioFk;CeSy=rc+RH;bC)j0Fx|G+MXscTv;*AZWT z+fHK5^?%>I_gI(3^C^OKi9AuNF=Z`4vvnDZr|G}Fti)JSJxV2CgzE3?-sfFX9QeYr z6`$pPZk1fk3-@K)YE-@_P?DLXE#QiPsddmygj!P4DZ~%d+swBOn3Jn%H|*7_VQ-hz zZsCoF-n*LDiSRcnLB}56E$}NdBC@g%Cg~^w#Q4uu!t-+$U|Bcd(UE57(uO1nM0=0< z>?0!CVip~49H)>=CmW&$2G767B$AVVxAaJUO!^+i!}LMDqnrFX>TBa*L)VAei?SoRXK^(-X~4^=91!m$2bqDg)({g43L3I>Abu$%yvE9k z#=mVD7tS)*GLN)yCu#W|sZWPY_g6hHtUFEY_4w{7a(KQ%?)vC+X+wPLH18gml_%|f zjudOZqio+M9S#0$Y3Rl{W0{CoI&h@QRfQ?w8cPb^C!$v<^wuE_QSQB{90p^;FEeye z1sI}q=TBjLL4KHTjUGTe#evn*EW%>@SZpK3nY9$F6e@?vqoQceu2|olEMU>@Hy(jP zttdjshopIXsXo@MoC?0zL7ET!wJ~8Yvf*3O6%NT&`EyFjLQV!Yc2fS&i|jMa6et3o zKgX936|Pj>QGL9AX5O~TlA#(Xc?Wg-LPp3@Tg%T6ufh`!wTj^>lfb|HS>sjH7qsVM zEcc8F5_klek}Ejhm(3_y>7tM(RiW1!c7RnKKjjnxV_)#mui^jc%{J*6E6FVzQ()7RI1+Kqk~qTtJJNwo`K~p zRlERnCQ#}DGS6^J_q41BR8-}iq4)9c-pn%=NKb#4*!EJkB4`RzQ5<5@~Nd(bu?0@B-1DgwZAoCM@4D8GcWoX zFqj2?rF#rx>SA}F8dK@mt7jp~G$-%XCBOx5BT`^_?IhVvaf$L^$&qJtni?NiHb}Q#yHe8#V{@rFwC5FN12``vO^=2zIC&``zTP;@Ytp6)c%FuJCWLkGY z4nv4RLoO5j4aAguK_6nov zyfA{%9eQ6_ii&FIgm`%+orOkaf3LV^*0=?nzKs%4A1vU)$^vBhm=IyA{mSxO2UHSQ z57KqD0wX8~!z)hjnniYU@2hS0PYD!BB%l_gtZCH*j~FHbdcN2vyc^(;^ZB=Sp;tie ze-oR3N$^p!?3+3TN^<|-b{bDH;D2q%??0BDa*x@#-Y}(RF)K6KcRiSjQLpfk1v5lb zzi?Rg-`d42+rO*50na-#{g*Ed$F}}sJdnJ>My{ z{QKI&5F5Pvj@y-u^{rD$g4M)Rm=Ok}8KEiI>7v61ex1$nUmwBxY)(T{O0+ux8tr_I z>@4H$vW%c|{ixQzR?09Fr&NBFP*SdTP+N(OdA`;k@#sVJlL`5SyAVkiwTty{6vVk8 zjv5`m3iNt1u85AcnjFb;Z_MUa%lx$wp4h2#ySGph8wC&s@S+{jG`us1H##q+bEmgf zxfNmp{Iz##gt16ql(8&5E{TN{t{%wu!3@rS=j$|;GIN8emgw?tiE3AQ_;RREgrLe9v=5%q?^Eggw6c5D2={)1 z!dBn(l$;&-5kz(16EkZ7_7gJ@PumfwFq%sk=3k|Z;O7`Upq252F8xE*a^SS=oh_pf zA*X1JeDKU<;=hJCpW(kwd65StcUVWX`J82cwaYR>nEz*Fg6R6eK5!kRl;N~_^UB;Y zeV0vpw0-X7u4R<=hd&Q|sXe7xT?1(9<9i=Sa?c8Oa+q-a+w-Wu9nXx9SO?-YJF3V$ z!OW=X7OIW*uMvqud}(yOnqqB>6y{yK>U{gVr1x9=tR_g)NcQv+%T)+3(Sf%-@w#{` zNnVxI?PJ&?4@_5prUK`|aV5<|t7I7^N873Rf4dz%i;f(vo=eBbi$Rs644z#Q9FzhT8jzWW1~42CF^_PGKIw2{qEsE>tDX^))zu>9~zoV zTclvi7qDQ&Sh?cRpVc#pj`sXFE|Fr%LfY#Qzvq@?9u@OGdiizN^v#zu2<3m|kb*YV z#5xwAXE=JDU`OC2=s~_OAR~4OuL-~Bj;#U1JGRz;<;9eR={mN>iwKXy7*Jit&Qmdn z7||-WO!$ezA_+s08=YD?ir)ks;M@X#o`(skSI>ByYJ%3mId1uk09BP}mzyaX6L`N;gPcsKXe~Flj_!%+IFUkZN*frJ^G*)ZY>s+^@ z?7P|rYCfUK89*Kk-}& z2EYaNj(NSYetm&COKi!>HXwt2zQ0=%26O>!WA6@)ccC{~rqxiiolkA3(QHdTjp~(Kmd0=6%TYW zWru&-o4noXS%1Q(aY(5g|&icxeT2Qd_(F-!J?;(ZL9s)fYr=!Vg=F3qB3-`aol*a<5tNvFDu%J+TNMxC!PJL@f9Cfh25!dEYv8+LnB-BZZD*B2gpM+#ma@+XEoaatKB{v{)Mq{OFn)=y-5n1R;vQ*sHl?oX2-5p-a0FHov$?jz^q9^fD zhC0KYA#klwQ@QhZ%FsCDv7ogtKtB4nc6)?8^tlr?P4}4_8rEe2Z_?=S;6G)N&!f{W zxeoC+NiEJ{bk*Z(=a>Zp&X$OtW|M4jq zzHj~f%hoOlWMf^CgSlUmzXRu%y9qW@!#(=Db99hRK|~&?RgdB5W1A)=Y24Ja&z)M% zn8sJfZ5Vfq({)4?9!Fl4X94O8E}d#0X9z4!=q-B0+WalC6IpVWf^3G=*p<^viiXOo zlO*lmkhxH_<5Ui2FDb8w{lzi9tCFZ2?Lu3OCyd1BP75GPm0D-Zh#YL0oel0lsgGdq zxy4peZV=l#CoGDxB`CrszNy+~&3S~JVWw{U^0Xiu~`>ExzuP*CoXBISOzq6g&+I@>y`J6$~ zG+dY3oH4Rzh;Bfi5=reX8l;Ohx@LQlG=ZhY0A(F9y{s4uE2-k$kLeksKf3-^0Z`Q1 zZ1%p_x<)1J1O@}a+vlSS>k=P|x8uWGeXiZQLCubV8h1}k&qf|xxnQVTnNM|zYJEL^|8;P~uZ#%?N3)Sol>zVE4ES8G z_xdk0`KjX@2kJ)$_OdqC-vV&biR1T^O6S9od&nRajsv7r6$QB22;ql+dL4m$df+&qamtU;-d5+&zTmr8 zsEq`uqj@eqdt=7?LGm36Q9&_xT=AO{-6-*7w4Tgz4xFE`18i8ON767_ln<*1mai@UQ%e>89(Rq z{Xpe?Y%nU?g<&-p56;I>^TLh5?_i}*2r?vXrY{YFJ@ER$BoaJVT(dY>EUn zR>ub;I%YjH7GMQ(8D!x;5+ciwwfZ}l5IgC{NB4s#1|#uKut}N05o&JHrjob5h@A|K z4!)4WzD$cMC3qkZQ}Gn`S&vwx2x5n;PT1A&aGCrzSgJ!2AAzsLCye)5Pki!9(Cv`h zdWN2MTpa5)+s@cG(VF~{vXw*@B{{(O?jp*4Mv=eCObgBUC~}|6XPIMd5_+xfbVo$L z_|7KB33O90L40q*k;(3xZs4L7TO}dC@dbr5)Pvg{ozb)ei8fT?q02L?s>l}C346iWzaOOdC_6!tn+tUV@s)rSk3b6hgLSNv|gs!IfP{>m68b zf7-&2&a8=D_B8zlh{N%^X_VuIPW^;l-;P&ID)&1%NcYY=J^ZKHXVw243>)57F@fhZ zK|RpD_2)XX`Rk7mL34MRxWVo4VHlE~u%-lpzaz=T!(aSjlWy}YQ8PJY(Of?6j#Kyiz?3fI4M;B&6Xl~ z_;ZfbdWN>17mDg2AmADT0|q`8oQqPv5pWNKZR6};{DrbFTuNpamw(5Wk3H`OQpcU# z%*FnBENi$aunmFd8!bqau1;x_oJkQ=(WH%u_e`LHhh4E!=J5izcq(QI4zI;`t~i_f z9~U#yUg%8>$zxcxuS~4Dc-*)w9YukGHRm8R@}N-2>O-A|F12_PTBPaLh4T-r z`K{tEom3E#siT@^&kRt^?z1~BM=t~zKk-kD#1DcHs<(*A_;w%eC*ZONdo=0;gjmeL zJG1mOC8JdWZ?hS@u%Dung`I~)g$skzS-TfwiM**8hU!S0Jox_EwlWgLuKGp94r^gsZOsp6WABd(w0dTR#c$5$ z#yFua{;OG?5%OZvagmot7{gV`vFM1qh^YhDDc(k3F$A~@J)YgTZ|_s%luF;5`v~E^ zv=}BZWNJN760w%ymY=>+SJuhP&&cG{6hbQ7c2^cgc*0dFPG`=!#nV-%Mn|vW|W4P z%so~dy6xAwAFezG(o=h6F*B;z%@yA@BCZCp<0-Hogx?%}hS<@!UqU-#+N11|LGSOn zCMgV@r)cZ6k!Irz7c}Nay{|aixBEx!evqB-9{L`}6dlVJef3lTtik!-gnGXstXYuC zLX**8#&?@`=&h@$mjP=B%;cNe$=c_FON=Mg@Xue>8y8s`Kxo3-Lk{olTP6K^!61fE z)hAxX)e-nHCLdk#DE!#%X-lFf-qqUu86k%`IDSM3@){|ZRsy*(e^dob5 zToErod_$;?hSD$EKymck`LU2*@e}ep;M@a5lmu+xez4uM z>8-H(0zg@*Wcc1l^}n++%QZ$H45i5WsDHfkHUk?P9OOp|LwGM{b50Gh zb6M|XhB6z=j8}k7Y7ZZA4rpNCVEEqN+bP`0l-)9!p9sz>u-FQFTZeguKem1;G0g&$ zSm@F39Y95<&47SRyfclLRB4VAP9oHqa9|3gfz{T5=>2~8iq{%1z8L4E!qpz90clsk z9z2dXDIC!jXs>VhHo*cuPGl%j%N?&;@vER88KGCF=f1A{0n70_U73+mhmOc!Zno1n zsdSC-25CNwbZ%I zOeuS89#R_R)p&4B@~EKD5*C^;Cv^R5{!_9Kt;1u+g`#-TLFqhCn?K|iRVb@D>*%6qiWu_?Ty+>JpB`sH5Iap6$GN<^ zuy_7)0%Vhu0K`tKQ6f4*)YZ{xPpWVGBFP4JU|UZ-e6~`SCI?LhAKz70-|{8}Ap$)S zhp+G0%&Op@Ap$MEBD{?zID_?w7Z5vl(Oau?T$0T>;~#JDs`wx>N3y@SffJYVyMwYI~FLHLLG$i|o4 zb6m7)-26N0cK&_+gVYV=u@1saUp;y}FD()T)Cm!R9aNKYmeAwrXhk6-4JO=KZs1yXA2crZ35TfZjj8LVR27ABIrX zY6p9RCLu9Wc5&^{u;Isv&hPJEtuv^RZpzdgt=(#aNVvac4hMN;p5^0BFM7}wtF(sl z#!O~4&J2LG)ANi9&sx`rbV?J>DR57j!QYvr+RVZIH~m&}cG#}n|k;kJlXG1<%!)i`_%txzOa&TT}YE3WH9Ajh_y zp8emyz35_)eWLbqu9onGY+k~eM}ifz-UT+Uf6OY@A)q8i{>0F#6=)3RIh0z<48-iI zj;|>XNO5?JzyB7axAOt0CZkxHyjsqV2#kzB>%+P+^Hshp0mq0N=H;=I-+Nm{*K*rv zbmD%w^4-MeDn5=!hAq*}FEbae_EPzKKA5R&Wr*17+tIK4wVKonCP=>{ap}BvTMn|{ z*mlz5I)hK9AX+)ARpj1yK27jtDwZ$QMg+mxkg)mD zS8PI~#FBiFsiawVv5kF!xNMR_1 zgj&lb3}`8_vDoc>)JkmS#sPr3YpdrLj)gA^z2pGXtOdJ!`lp5UX_sY04{k7O$-8>G zUmnR3esk>~J6AS6k9^r9scSSTc#SIobA-%9%J-FjU28kL=n>~)WCfn(V61wzd3YrS z9U%wU>v!R%-wXY984rcpLaLcQ(ShSVhWJkoMEsG{eqgl?5HF)*-@z#4qEKEVQ3ue01Olh5K_ z)G>Ko0u>hA7vMz%q<#la?Z26cjzRZj{Zb=Y9olTLOnD>pPwJVV$C71X+Ff0-W-bks zjGAwwh(LI~PUEdZOCl#F$SmicI32Iy$5X%M5h`bV6tUxVAuHsIqtMWoIm)6+QF687 zI44VCU}SFC?w?w2WZ$0Vq&k63+=Q<3h`u-9)of)5*fo zM&CxnLRJP;5rH=>%Do*FOc(ah7_5@-*|r4<5XIcm0rtu!e9_gkR*SGg?7c-kMa3!*PvSn5&VpGtRi>UVEC3 z(lnnv)bbP+5O>`Uxs(E#p$)vrMZedWm023xM?4Fpp&e5nad%W0i+`Jpeqr<1lT$;A zRYuDgZ}BxkL1-zQnMJ*xp)Rjb%}CS}h$fj@2DNH^IRTvQhb5DnC^cgDY7j#8Z$caV zerINyp{b27%|2*#p>3A-+Y$`BASS>#m2+5rvAE{ILOeMZGq}YOF4kn&q_Vx|OrqEs z$4UnK@4JXHbvbjEgFKQCma-W#D2^nxw2dCw;u6w1*}yKftGWY$s#tlkb>>4O;tkv^ zWhr>&V-i_8-_MQ~9^nHnRT0c5L*VH!d#`>MmmEvN8dnJB_+Hv^QZxOpk4HasUx?3K z4vc8WSD0`??+)0nmQm`Cm2@is`-)wixF%h}dewp+$LxE%i4W1v zQeiUiD9Fqw7mw$)Y?*^rk#2|!kDa+p^4@Z8)s{+wXXka6gL21z(XYBOtR*A>fAf`} z1#reRdO3{DTRNg$)+a zLcHqC^5^57;P^4`K`nW-^}V%(Z*S`)$sTa3beDuN2#@*lOdhu=6G*kAtL<;sh^p)w z(&yw=dPaQIJAFyn?W61un_I_t%OSfsQ@^7L($Xxwf>TbhQ*Bv<#R~y)UC2n{tHwZh z&!dy0Ff7>(^D%Jy=+A{Wg7cEGyuJ48;H%;6;c?neK7X=erb| zg|p%8bvlLZ5md)Q?}NT1avuxf0>~$y+Ty&$H|wH*9H(VmfLNNwVc;;mVl~pSGBcl}7h&q;~J%D=yIFjg#n|_erRa`{4DhHx#w`#wA+}srtq9 zjfPuzV8Z=9k77=-sj@TK@uxra(+Ux7#wJ>?Ybfkx;KoV-_M~GZ=p@}N>9?FW?fVHZ zv0bQ5FBRd7l(W<|pRwxp4HbwiUxqcS^kLP>TF-Z34%})+@RMMQ?i+G{4~z#E40B3#1q#e zoAcxVx6^sF=mK$p7_5M`V)Kf53%`){rxF7_r*tW2;ZTBy9im?kSG6xiDLJk+p4_G& z9$RQRT>gCVkU>kjqXhTlYWu`ZV`4pC0GFe5Zou=zmA9FbeBng{M%GZgOR^hJz0A5f z51wl4idY-%h=_^VyQbuOA}gm{_o3ou2~-zI@eVnaaLQb#sK9SwJE`awdm_uDI5803 z5t$Ct>8c>#zUQ(4x#kGt%Z{#Ik2(10g+Npxl1FKyAg!VR;N)qku;k<>b3uoOxjw_q zoY;-$fgNB7p)L!@Q>(e-z0~U!Tf+CXQE|wa_m$j|ps8t`Z4ZFwSrA|0bT{t@J7G_x zJ>3S=e-9em2e1i6M+2Riu=(gtk15E~@MG}CDQM)RC;G=8$M5k_JXM43>DvO5F`Z;& zD4$U%Hw!(D;Cfg#M~if?lUCpduCxiu;PXdX)uyUEXtee$e%p$Pn-7u7&PUu5%JLVK z6JE=3I9Ryf$w+#$7TQM;El9c%=2(n!XIL>{Yu)fz2{>IWy{muVGrD~FpQ%4#;qeU2 zy`&9pd-J1_vW(ZVa-fU^NTjX(`J&Nu^TelHoY96^(dJoHe{l}3QUUnFX8yIfhJT+B z9eR2cfQPETGnmjSqA?}f1T#XBqvSCbM{oh^#+DUxri6deqiffTM>$1h?ZsuMW0wOS zghoiW?xfZJg^tKt&nFy=zCAWS-OR#|`u`%GK8-3F876jgCYU}5KE|kiZXiyFK3GnB zJqog0HOr9_L!RQN{W-?+4g$CedqMQb1lmnVD-cT-a6$+@7^zqDG2f>+g$jVfsbrY! zxdenWeT z$mI&5U!)GAb{;lGPh5^z$39jGUbQw8%zRVy$B(*!YCGo*}y z1YPdGGe#1G=Fzfvm~9JPcwFh3^;B!3zm{|l3vY+O1JreM7gABMA+dA|C*(e-2m@;O zI3vQ;rs<&|cnT-AI}@;t7Ip8<{WEd>W{00oBq@WdmAE>5C;k$ky~|#sO)_e=ea?AN z*Mv)1Bfu9ku7Rem2p19GUzH-6_BMP`HFPUVo~>|dEl}DV?a@c24DQDoOkAq)zN4l7 zZMU~)(68_|8y_)86Np0M=&a&%@i`^_BFl-|i$p5$#W?aug^l+jje zfyVLc`^nNIRwRU>F#1TH;Of8S-B*jwjh<~+6TeGmp1a;IQ4Nv%lmwK&c+f2yW6RO% zj&K|$2Bo2YzZok1Fm@BCPI2(uR_1_9lTZ(PM!tKS@D9?mFlsWf{G1@2d@@>zODOmB zSuJVy&8|s8cKK$%>*-rdims6c^ObIcf{BEg%vvTno15Sbx&6Egt^!U*P5~sNsmCjk zumk16Qf3z{d&UkAHUH?KD zD8LM?nStX!&Tp_jI66&K6FL<@n$DC*q3-o zPE~x1R&wtD*cC5_Zc@e1B0oRuE{BxU)_xfZFj%6D(vOClYq!4^nmccBVt1rbb{u!7 zC3^#}Hd){|`*-7VZi6I`pQ4;xK27b-fORrX}rN!HJ z8MU1OoYil8uR{;Jr&39+a=*Fdc!4OH!M0Yct!Ku{uIZ6l*Auk&jx^ug^!sxzuf%`_?PvCE_=R zr)&@JNdZz`%n&O{VSo47v@QA3%5QyZA9}Ckhyks{N%{u2N46zLR-|0JX%##Q2(%&x zkovHI8w-l+?-yc8^VaW_{Onl{m9|3^NuEw!Ky-EP{>S&!o(p8*w|o0wT$t(KTa{Ed zW!`wx=HD7vo!|?uyAoDFO*>`s!*k01{hQW{Wbr$!BB-mrgC~#IxuK=mW$l<3Z2=R) zyYD_sFcS`sq~8l)(im)u?4ax_tvh2Qd>WqhFCX1PN8b3J{5QBET+vSSN0zJfGpa$P ztlvTwz5>MIs5TSq zqM$VMsB0ZZ8?;cNlA+R&wvGy^`p&L+t(a|2#ExM+*!*J=>Z18UC*BC5+C@N+Mp&MR zw(lpADQtsVziNf%uIQ_bu{H3Z?G`n{QMx9xj18nmbhPH_Y4uNGzH>jyqw8O-Y8w$( zhaOax(F~%TWw?#H36;3$o}Ph*btV5Ie8Fi zu~fPhgdVR}S`wntOa2o<<4Qmu6_<^&%5b!#G~XG@9!9lixhuocm3e~%B=|mI1zT)i zdh!b{yu9WJWTK#N(c@ZB?hriTdZ~xt!JOCmCAbYej)A5Ak|nD-T(<|?-4e~f1Vh?y z4Pl4xb57N{4cP3r+C6E!`})JK2g1?7?PA$$PO8iKrpAg`@7~&oG>7amh2y63F|*5( z76b2tf$-ul7q5=F!B@%X{T-Zi*NwEmkNU6D)bFtU*0_a6Q6idneOP^7V+4G*JXL+% zWEWzDKmBVFQRz_=Jkl`wVzOQVpFTdn$98->!2gMkHu>Vtu#?GDQQK1$Amq$;VJi`& zTS=U%yN?(|uA1wabCXy}`fzDP(OaVlCUwXd{;~C?w9JpsrDRu4NC?SJr8*~W4WBvkCE`112T zc<9j>LC0`JDro+G=R5r7mKEzDdDDs2uY}=P5Ef=cOu{h`@DH|pBHh{m+*5{^n@Ufy z!fli7)F#vWI={!qSjg12J?A0*xuX7u4=Idx!9a(_+ZW5P<`oj zWGNq&-k(q}D^;D}+3I`@j7W1FCR&%}e{Q2l7dH4S$O#6gU1AY0QxoKyTe@SnP>erH zQ-!;yVd}PW&UE&z9q>>AMIL}aPBW&9VPw>X_%f}Y(9j1xLQ0EPSSXl!PgOWysa!VF z^19M?A|9L-W7v|yz_nMiPW(*~gdOlig+?S`y1pdtHDZ3dp|Ns|RFNXCtI;7N-3xa7 zwogKMKLiEu4_a)QoS12C^t=xi?h(qQQi5w5@?LONst)IkO2T|DGnRPzH+k3Jj?5_M zXb@^eG)bc|+NW#hrm>Le4^Qwu_U>NL)?(yO?zxK-8u$h-?5^kNnG1wB7=(fvVT zfk%y+07I^{euEO*n1G1__lVtsI!P67%|C`a`>w_>;}@61=5NJky5BmhvStI%O256aduJiFgrnxp{ZvsfuAe<;`OOzR=+>I!RC9*kr@)-IA= z=75F0eChsia~X;p+)_?8nzz;}r0Q43nkq#c%ynpCAIN$%kw&Oyoe@~E^@NjTW}}Ee zCoiu~S%#na&mCBL>|rHy%MVZPAsQYm?kQUj+dWATqkWL9D8Mb(MA|*r$K~nk=zP57 znzX(ZMwyEKO{HhX;1>F>Lf9(!xj#pthP#tR)hQe35wB>)6YuW39fNE-cRp8XqIWmx z%41wOc)(hZ5Jijo71#RG=kFm@Ws|1qDPITE5_?5@CZSCai1A5Pm1!!GXtz`SFp;0p!Y0t zJ4qd~B3YeX9Gy`&{2Dhfo30)!Vae!%tW9<*z4cRH^YeI%y0TwnMttm zFLvhlkTyh%pa8d^cTdrqRaxyr@%huk0K9A@(ssIK;h?N6f5i<4g--;XM@^X#n{WNp zze?XZGBDSjdjCD^=LJ4x%YoYgfFGJ-dv4mIZ!h{qF)I)VF=ILPhX4xK^XIkVd)L{=NlOD3E`yP&yHMXu6|uVgOJaQ}j0FnIkc)uYTP!|v>kClwaX>H1qr4b^ zlpJBJA9M_-ggoH&B>N10J6ILPb|LqXY{wOVIgn{btLoAp(LqZ-$BX7 z3Z|A89tKlaOg!RbX2B;6H6nD2Nv8y#(9^#C<3a-O0k1!WOZ_|A)5pKi``0i3_Y=cs z{^8&J_ldXcdKYXJ(g5l>q^bC=X5ZSnxSjcb&(iy9OxJM_e3@8?KjopzkbFsQu&~n9 zQXOi_oyBc~KMU|@7%w{hB7ObKV1>#K%%A2y!K$wFUGUiNLdjg?ISK1aweFZpe^%i1 zoy&lZ*`!w+uey9xfqQsW&B@UTO1km_>J#W}?1dPB9vJ2R#lZ5vA?cX`hWn%Qq5`oA zOfb|@-qJ615)6B-SJ2a{v>{T}uu`CSTUd(GC8QK<> z7EntZg8~pOOf>w*P5;w-F8-n5k$pWe(IjXnlsj+Sn|@*vTm?Qtw&#pLD?7gB)NDK| zjrxo~b9vSH2#yH6$2)kA!beVzE>47of&@^lgtZLCz0TMLtEAyr130m9sWkj3svc!uxG?T|l-9PI5bb(|(0f9>K z%jVp69w1!p8`IF-lYw8pea3@*_yqju@^&hI)2HSh71`kJi8X?t`{pd41AgJU9j!6w z?#Hg!L*Csk{U;mv20Xj##+kBS5&U}E``xcrBxYg0wV%4UF$DpNJQ-X~C8+l5BYNm5 z(^cdV9b?T~Zqkxn455Nlgpmqhd_+XYjfFwh+Sa zif8~C*zi%#x6ab5EBw~%CLB9x+Za}k3 z>r9|K=|j*@O~zVG{ErYqqP*+{3Kh-uJq651Kyp$7pUS& z=HC695FB7uY&C9A``!`6nf#lpx)j}d=dA+P61JXKI=TczU02lZ5L1V&WHZJG-83N1xPO7SdY$@SSsH*M;-|mY2DhkD~1%{G{`mtCk@rW1a<Ey!))IF#H|mOEeDHO*8gKj?k*OXJmt8bLxl6ZNl~Rj=cD`#fG@ z%J8qyE0O5kp4;lGmrraakQzxdxsk*xf=0GKDad`WBaqY%n-I|8U0d?*zWt}*Mta=mmkAf8vOf{=gzY9J#( zZN$*`6e(>Q9Tds}b|(Xh6ItgYo#2U-MXqnX$Z;9|?j-s8#!EcWEUnofrCUAle@EZ% zJTRIC^(p&%eDG1ojqG_*gOvnYTRX;-1v4J2_sLq!Jk^yLED~$DaRxqwt8pSQ1e<;7 z@=M0TA37Ev07>bs2G_%3r@+kmNaH;E^u zbaL&lW{(FId~FaxMQntaz*CoEP!4rVYPal@9xto2+~tL;*b1Un&q5Ym0kR+M+<$XZ zm-?mxHcZKVl9eY{hj+t4iRMREW#{9v^#`!#7vwAbH-P^9Zx*HjF)jX)(4V2q7Ig7P zd^2-K@EOW2N$0DtXg9o`7u!pT)n^Tfb3fR+7QPU5Uhb|;k!u(i&*dZofQ?;`2sHX4 z;VUR;`BK-f$32JYP<%VY-U)42)Q{e^AKc&kJe2ft&?^u`<&kFi3>_T$Rrizv_va;} z{MVCC2oh6?>&4UHiaX6n-(L|JKQq0OZ@V-Ag9g{x`RK!2QAu01`RS;a2vsX$3GR_1 zJ3!$rdC=|u0d}iA+Lq54ssk!Ca?j5MAhZUcI4mH&^5`h!#dJ2!X!5bpkK`~TOlXKpC?wArtAss60w^j}-T zu!EM=#bsu@A{_^3#EDm8NOY`-E6C@3^@Ea79J*lQni2hJ$F7V0N11o^CGk2@Uv6BK z#82X*2x~Lfb0Qx)mHujrxWM0jEsmgNsxq%7D+s>B6(M+>H1 zS}9*i_-DUU|Mh257W>`sf}9g-Q08=h?&>0R-`n}APE=IM?%>1NnYN>fz32yE=rhz!&^+j*3X%5|b9X{rMu?Qt>e;9IbA0V2(5U{l0m3?( zX{t2+)b-RfP|M;HK?+SxElO*e&|i2W&B(7w1H%<6y$Jk z}ttGQN{CxHAiJhomE$eS?tasGYN^jzQ(LGTGh7d=#o# zB(#Orj%ZaQ#YF{>2jROeZ#NXMyTpTcVxH7mXJnW>P>zEk*Uc?~deKHx`Yw_r3sL~7 z3bA8Td2$^ug1--_MQ8jn?swM`{x$vyVke-(4iX7ERPGa-GIZgo*7m`or~iJw{SUL3 z>(71#AF^=shc2z0fDR?LL&Siqm^2Y9BebfiwK2mvOvmSb)AM%h zeb%5h8;*lpF%-=1yPLM_yG3FRhcyLA9$&VZ?8A)#IgOTw8cd*MYK>y&b_P3uuf5yT zMdP!Y5&KhE?n-NBvKqL(=$NI!_!4cyUCAv?(*3EX`{zhThniswW31}D4T0)(I60UX z)a_&J33#`(<{hXV(gYq(^ge|zYC(U*g$*wseE&x1tiUWGTlH!71I^s=Jp5;P#*d=6 zJrG?bEH_xxK5f6hKQW9=?cELYD7Ulx$aHewTqzy|%ggc1_~kN{c8}ST8kR?upSjNu zmj&o4+LEpS&o2=asDx!P-&RQ*>9M~wB3mIuq5jIAVo~jd@C-6&VhRR7qHRGSfXH8A zU#g#&Nr$YyYPUX!%yb;Xq>V#QL#EEVo|&RAzDQm^pskDl4+!Aw-5?U$B^2uGtPY6^o&jp?bhd2#cP46IeY3m5Bc0HQyE5wgyX z;Ce;u=VE$Tp7j%ziXsR3{sg9Hx^sxFhCBBv*-~6S##9>t{mvBf`fa*B&`Pak+Yql2`ug^T(Q0%PTPN)P)n}8?;XD~KDhC4#g<-e&K2I?v0w>IRm$B3eA+%qQ6v=emIYvK79+o-VOEb~6tI$M z^&dB}+P>Tc%sAVDTliaP$uO+Z7ok&cm~K(@UmCvR5Fy!ZMgJRxmE{9Yk+Ymfw62e4 zKhLz~I=!gm@?rVvQ_N0qctRFI5-r0yyy$P|L17!4A;f0$n!g#>5A8L64&) zpMG-#=wayq&jYBv+=sfV*Az$7f3cEp89uRo>nIKz_*oX3 zO3*qYcGnEGUl2TGaoQ5)$(We9#SVJ!j+hd_seBU03&Rsik9!y94RrW!V8x4*?G!EQ zLYqcfR}}*H2)o0uJ3;;7juC@fM!$@z2Ie=k3~Y>vFY&`=6?fkMCDHeHR&H3UpJAPi zo4ZL;Gmxb>PHS2U<_7H=+5n(&-Z&qnb8#cT z{b!~=FaX$vYr5iB5UPema4>J*MDQ0v{mpUlnbPaGK`d7qThJ!y$}|n{C47Mjb9p1X zmL#09t>n6MRv5u!K%?B{<#l2dvb9(FWSEdF58L1le%>>I>m}^Ko%=yFCHvlFMse-l z#AXOZMR3S}&-jSZNeq@JynJTLruZWRqW)g4yM$Mt*}RdHlpvw@V<+Nnc9`br;vxT} zx8&*;S@839kcC4mz9b&d2fzN{>jJG>;OmgNK)>=c9C>hXaD~tY4ur}XZ%5nSDMuzS zVYf;Nwl2}2NMvbU`A4;2P$%)=?6MU}lG(A-B0Sn9FDG@z^mAe-Arg9+m@9KmtY+>U zso!Sg>l6>l`{Q;vs)tsx=U_;1^S+x#?pGtjX`YVVSBZahg**{LYTuhcX?a~=aI8ubgqduAMIRZTJapYR56+}mjM&!gk_6I~;ZNO(uZ3Oj z9BOVnO|W@QD9={_&|g-nhtFQ5o~rU{&^-iC_{O`ZR*{r84p7ph6R%k%+yE}14aZ~T`b^bcJC{h!TO zfuS&^l>C9I&7}X)yS0}^>u4ra=Q*&vuy`On=ASW;{s^VsZ*(LGDFH|&>BOfd`Jb{M zPavAm&Af4`ZP)%gsOBx@XAn6UmVK#bs_!iPh8epDyVDDF#N_SOGd6N*S&+!2gaIJG z<&(GrfOGUc|EHfea+W#cpG{hyzHq38PLwLOTXB7g=Sy-uhy5zNe9fuP?l&5tTAB6o zH8Sn@Ku8(Tb!BSDx!r}cc)!c1cJJ+iyC$v3(|Ms=A?yKZ3Gz#$e<5~wyr#*Y4s-oP z<{U^Px9g!L5Z!m4&(Ye>z(u{|b@=S0rVqkzkZ2P%Y#y0Z1&lK3%4w(_7%(%u0I5${ zThQ3?W|D!1KSIEkeES10y91GAJ0VzDs9?&ppcNYhPbg1zZUV3Od8DC}vVSeN5T@-E zu?@%u(FztuY8uFZk%lbw*gwW6*S`r;3D?Z|NL)_ZQ}f;uK72mRUk93huph#AJ|lME z9g2Z8{g4re251(}dnVr<(ZKmjS3q6jH?=xnFfWd3j4MZW`4;KKa2w9%L#qIKRB&m^ z3$b(k$+kPwAw-?V>QYj{c~?50SLoKe3MJBllg{2wWdI?Z%<{shvC&374l6eEGsOy#%Jig}(X&(qOyMO1y8XrNW?vA@sA!)yn*z>kk6z;WON5SF+X zj&_{|TV04~nI+8hCvE0;dp}R!V)nV9&`0Pd^fqQ+HIF(xwiBQz7Ys6myp*3liGviN zj8Mn;HoO}?Sc~hkCg-BHV1xJy3juT;6SBCkY4*f#f_jYR2Q-mdPz_7l`)mD4kuDH| z+wtG$t5G3CNuH!xgsNqD;VPhvkKK zf=4_?4AttxjcM*%duHfIqxZFi?hn7Kn8;2MsXl)!^i61<@8HfB=_x61qX_a^v?5hN zW_c42rHH>16|U`4lQ_xpckjrjUATX}>UQ$S6DW`;kp1#PwWd!9!r0PDEgVLawwTD& zR0L8!`QnWR!^K{{Vy}jhUtuozIcL$%7`xj@4c77icc_j&-mbe%ydDvDr~~4m;&&)^ zZh&Ort0HLQ+c%RbH2ZXJ8y!T-ppUORTcmF0#`?W9t^t8k6*6mPG>5#XZpeDtDw=dsOyT!1|W@;RSOOoUYVW)P6zN&1jI4_POn<$w;g zTQTI)b{b=q^vqzIY~&g`B_PT*`sX(QcXo;{0;tI4{{d7;z2TB>yj64fbHFuVZ4v>_FAaEifr#mkn%&z_YkC<=+L?O z(a?w8)AVa*W>FmEM|u%zB>NYRnU%lwg85VR=_dlI93T^%wXJasG!>73x65ER1W@$@qBbJInc%QoGZ0Bc_%^kF^Eyvhq6vjt5gq=|q)B1@FAmS8hO?mnL?gg%W-y zmd3kY1N5tWu?fI!bknfk6{!AUTqKaUW36tOE-!4D@uUohhh`wm0m4|AY-E8d5r!J3 zp9q@+gPF!OhLa47guZw`Fh5`_J9@3;$9!`gJJ|gyKzl?pk3Je&t*$bUXJMF_Bzf`E zOck!&B_xhlIvEQ&(Tk{mtLoMMrurjz;zJM{Yl79 zw>>>?sE&I{V)kA7x-Pat1cboG@Ug>qu714%s9-F)aZ37|v7 zjd%+yjO5hpQO-b;N`Ekak|7WiO#P`sXQTed9YzfTCh=j^^K|*6pUVqN0a{t$!NG&? zWRpi{{ip-qL9j&kRr&}+&*C;1(S1@Q@l1XU85BXwQ5%q58+(Ut#hQg?LFYJoo{R1T z6bbQ=T-Kk`Sj4V@;FI%}+T8Ps3_uDoW9S?g^OGDjT!SwqtlC9m1fc8P6@PkKx-5KYj+(v(|s(ILGp8=VWaBv|PqNKiJKB;Jsn8v3nHS z=Z=Lf5%ST~VSYK6JpHXFNa(();YrO#BUJ`qB?;7f#MkYE)rN zZ+!HA%V+eeQ$C|1>0(~eIMZT!tus&;@~(@?Pk5usIqUDV z&dcm!yvTFTu>Xay(%JjdzTXrQnx$6(bj6@4j{p4MH(zIW0@o!9sORvD6Vou4F5G9l ze65A?5G&JT*z`1Ca(+m#z%^_a{LX-vSa}09KDdyqIvFZ@zS}625h#5%@muOgMi_Fy z-vVL(_!D6WmuH+ao~iQLS|0=y-4IQpsr>OOC<|?!5}XBG?Tam$MqL!OztsfOy@`mT zrRZJLvN}F`{j*7h%nc7SvbjTCNvluye~#HUEZKYcyn_MWY_Ly{#-IHu`5XLjuLZL^ z#3#BOk=8aPal;SSU76W5sr==}P0OIiAqNa_0RLzMN*rYboQM`(Zr8twf;W~uBE0p@ z!gPvyO1QQ@CbXy9e-??$49EOBs+dVngyT)%BeB?l+YgbgLJ2O=jj_D#E`_o0(nY;Y+hfbx>2GefG2zxg=V*@+=0U z{rRtkfE$pDWeJL$0!_>a)z_<=qHGQ=#-8-f3Ibj{N207v87KjMZ9@5s`~xL z=)>t1*)K&bhdD~NV6?iA?fZt0)Wt_eLB7dXKV2keLnj0Lv}c4}&hzM_O=9ws9;zvI zJiO6%QQ6OwUmvX(&DiF7dt`I^RewBw@PS~=%R0~AR5*9G$E{7CpFtgAQme6P+RSno z6JnpFdTGGt7mFK7JDCe>_HBIbwQsOydzIl#>aTYqY~J+wa$Sr&34i(X922w+a+tQJ zU*p)SGYaT@>9gzi;o=TOJpb@=XbU-X>EMD0l1XG9BmzNHBmft5H5ohZ64g$%C<0c_-WzLrMchoR@JbV_HLB-bMKu# zD?0gImfdza6#2jW#3m3%fK%k-(bcIFCB>worNm%k_Et4K5;nJFg!_RxyQ`#zQvwWr z>s;78W%>>KAKm~MM3YA6R7A6!ZsW6DLB!J(00^JfcRMGIH5%>wh=eFQIsb<~``il6 zVwmDE`6!khZEqA}R!rMZW>bLwXk!+w9;b)CQ5~A6P(gcff!18Z0_A@xTBoJ-+-~eEMTZ=Y@LMCU`nt$Vf?A!Er zy2lr}LLc#;C0J8OLJoMy$Pp(z+lLOqV+hH`cPu$WEXQ56_NZ0r^2HK(j;=#PMl= zo&p2fEd5ka&&wJ)8fY`9*}F;lzx$xR`Xdm>5cydSxrE6LX8>54K6IJtea)S@7UEu4 z#0#^EEz$;OCw2p@;)A!7qz26X!?Owca3u0>hU6UZyBXV@rZiy#z-Hvw;+ha$~FMIbw>u<%U1? zJvyj%!~Oc}(!CaxyDAqd1o3xC&v&0$;Aq+vxL}mk8&1{)K8rUri?iU++feiDx_U5z z$c$)drQ~-$yVWVpz99Ra#Pjp}Fv{{&JmXOi?J3JZkz$N}_z&1zu}+v1fjp~6zx*pi z72az(oukx!e|E8WEx+n7WwEp+IuaNAUKr9-RrQJj1l@@(V!&ARltVp=i%cV^F$_z# z7`A)%s7_K}K3gi(pLwJ|<)hI8^f4MRX15Ux4?_jeG!c&jp#2qq243tDjNk>wczZF| zfc*fpIbfh<3~o=3h<1uN<+W9Xbi38(M@F=Vw}uZz+?`5kXO<$CD(_$+Wz;@!I06Cw z_TlTt?!k_iq~Tti{8TwN2g%NAOro><%_v!jRKU?{fl{#0iJ^e(t4kkpzk)al^O>8m ztd#uhv3Y92@d%#7y}~L&aObJ@g2!G<^p@Bb6Jn?(aSy=t9RhddU;$kIyShujC;+l8 zOi)D)Bat4cE>O0hj<-fTvS(2@F^}j|J65B3mFWg*knO4P;XAUSoj3Lgzjy z26IjQV=GeYE?BDzha>uryF52ReTko z{B;pk2>R6`M$Q(`h_2oBox%PZ=B;r80pvn1!1*M*A0B5NaR_2abJ4B%n4rl?!S9k> z|R3(Y?qosDCtImw!`9uB^LqYQIj=~HaKoYaH?j_?;I1!o$x>Ky<>q4M;mlaHC zLK>)!b}S6u?BT^zx@hd&UkUc4iHQ-L`PUU+JA;7r0N$_BGBJR{fZHPKVKl#%&Y*S6*8?ojO-$PP!D7_-EZDeSA zsD~TXl-Q8Jk;c`vtvPM}0{Y=A-{QA)V1msYS~D?H3ji;#21EZRudhCKVDrfd0!XJbC7el?<}Lg%fz84Yytm3| zbU4$A!yzwZWdB>&-g($2s8oCnTRzmiu(%h}1ZqJKV3bM<^g)q$Y~7i%FU zbW_MHOtSayt|-7_7Z*zy12};@p{~(?FZM35AH`!74KWwIcVO|{87f?zE#n350U*L< zQky@a-*z&kU~^|Y&Lfoo9=IO5qh~m=*qA6rbPr&&YCW_5dnY7hRAoX8931?7E2XHf zpb@Yj0#Nzl(A1~{r)T!Q2!)hC=APK$Wwow<_Bi-)1!q>M>cl@8ML^c|`KUaK8EnRn zr{r6tO7V6pUhhOv-p5NMdRyD#tx*DLV(PfAqN1?z`2Bollj!%TF18g*f=``(F9XU| zaAHmwG4OpgHqjeZbP28j2|fwb8g(0Rruy4TdWFc}TM`+DEXiVm?xMB9v|mCQp!wC` z4e5_aYvI|u))Bb?bTd_kh4?~`*N?vz;bIEPCh7yNO*ENOLjhzGmpec*7GQxUJUO}L zZs>wvg9_UN?!k_}1R0Cx?IK?&0Yqh4{f%uui}wKK==iwO^VDW))B&QkQuCwP8OVm0~P$h+AYFpS;v16^2oQ*&od~9pu)_P^g#VPwB_y6*u@Ww z37LZHi^iDgc$olSOG>CYH2mWl6?)EJ6Z+odPeC z(8Hq7&hC@ZYsm?CNb?Nf?jBv&U2B4Qz}CXpZ&;M$X^BtAoo?kvhYgi~G`q^+~n zk!;T+TUx#}ie2?U3@Lz-yYsC?Z`vV7l8s0lV6|C5cS}JTjgn@xt!#hSp$`Dv>3zGkNb90vrAN<$z4_3|tM*s^a88*jfS@rd2?Mk{| zn`DLu7x=_L+A6)5Cv#H3c_pm9r~tcH26v7)R6}mj24`nYlM5JOoa%yCKM4oWR~1!b zlgu^+6ATf`C)-kcFXHzye={I#ps&<4vT_q{3M;qhQ@@mQcbXH3OJNVbdY|#19 zn=7t3FTpSM#};S)4Qi;uTV!g4(?F9hz=_^Jf0CtiRgsk5&{}j&=7$PmPtB-}uZM#o zy&+V3Tm_}#{mc(uyh&s`t$`X(Y0#vdUZEwSVa?B?B(>YJG`3G|NGqCFOTQE!h+EHE zEDP*B%UOuiotHAD)aRMrN>3AW3+N7%9w)_iwUx`7)4{fR5De)}!=|nB^}a8)NxCf$F#EQ?+C_KW;CfSBjpFRcCPOTj5_L`?Jb6d!%J zCuNd}f!GY37~u7inMvWjl`^5k8Mj{T-gWQe{b-9mZ2 zG&fYIh$~;;2`NyARJbasRctD#znaJ87_v*U=E0f(dlAbImOY{F_R`~b(d?)ur)O>-iY_5} zB-JFL>LiJq>_N3+@#p15RDM2r=Qav;r)Lfp^?>ce^{4O-P;f*?S7N@ThWyso9f+MWNMrur!^XBJA0~r!iL*hMp)z&&GJG@ z?Ie&=Qx3qlIh~Kmp-@j$;+2R+`WfAQ?yJ!KkDcV9Bqn?9T6jQsNCZDeXS1aC;mcLB z%Pw=XzC_8xn50fBN|N!UKcCcs`XJPf7nkik+s}5vj}j$I_rMBi-gfYg7g$Y%u{K!V z{6a3GBhxTteQLJF(y&BtnPN9@_-!$ZP#4Jeu>q1WmGdPXCeLd{?=ohhef&y{nQ{FJ+ zfZV%MCZoTgJ8IIyPAGYjj+yso6l*EP`>&T+x zh_%eK)8=49)|wqbY`22q#hK(FKwnQtJQkZNUg!6gNnYvC3ytTr~@-HxVNYi9G(eLgh(VxEHu5?t66ct10F!4Kt2 z1UMS_VYg=F!jIKktuz0e3W*+v7y`VJjZi>E7279w6~%kUpvg4=nFLLC$JEVoL{d-# zkWZLZF*1X8GO6RedD>E(MvZc>euj6(Cn*bki%q1#n=|$cQ5$#5o1V_0g`3Y7{4bIR z%ApIFUue^O`H9xA1-IUIW`+6k+tRWH>3AbXqk---ln5}k8c1)dQa|tvx7$kk>TzKr zJ*?j*s|63fePnC<1G*q?UY;`|WuZlgOl@*e+~Zn{Mh8PbJ_y+JHnKI>2eZQp25{gL z%<5v}hI=IPNEN=$0u>yWm{zKv=$Gt&G$dk!URf|QRMSDL9M_;a?Jsy%`cO@z7xYkl z&EqnSx#t$_yI;5Mr&C^^nJ|D;Ez#l&ac6Z4heu4iB>{M&PO33-iFK+j=tUt{zx+IK z$HreHy?9DCt79dUWr$%*9rIqgY8}K#75tXmx3ini9L${j=QXm$`38Qs8kzAZlT9Tv z(z4qX+s#+@t!Ms$MFrOWAfsp+^A3+~ET4aHITN`~u>+E3wAU5!-#*29nu(4rby(Llr`eM+L}pHUr2o9DmIH=TSAoZp}1D57YnKYN=A^PVNnQVKS5_(SJ{ zhfpR`=@d$cLP|A9_XVah*1ay$Mzt!o?KbY7;E_k(w&$Yg3oMU90&y453KE%d52~#m z_&()S6TWU(<`-Kug8K9!)p4i2Z_rKySdeGp{>(k9i#hmgBe{Zb?MOvn=ne7sAtMj6 zfXIh;3@BR4AR8|a8)|ooYAQOY)4#u>$gML9=&b$m71`1eTU@OWl22dLY%uYA$$aA( z?kKj}^;@+chp83`BT1dGjbNex^~)w|=merIf?SGENAL0<2{~=`Tffy<dD2_B@lZ z>zcLX?q+mN+mk5`rAo{%p{y@|29HxeF@rC?z`g4DRxQ8dx&R6Ye96G!$W`ZbNo&;b zeimVryIv*yF7GIPO`^QRWnygc4#C|*V%Pqpdg>sEyCSLEEv@*JM`g&FWRC4FFflc~ zKR?c(Z6e9;)=`e<{0tmi_W^mtT1$gd zx6=}#?e~69{IrQ9C?E$;l26|X8)uCq?(-!)YdYOQr4L0}KlLpey!?5Oh zzecq}LfjV7=4%yj>J5LWXdhf}6P{nsXb)dGIsa1EY3df!^=E7ML>j@L|CzUR=d3MI zNZBHK+Q{rMQLMa8ag=1iU@xNjo2<3@A|HD)PrcAt`9h_`qLpXtX`>i*LX#(-`>o9G z_nX35icOdjDW(q`Au(stT}=zElQ}uX)|{ScwBaG&Lmzxe)9!zfS@)46)_LKEv;a&+ zxiF$-*IgM0=10)Rl$g3mp5N(3%^uvqAy;N2iwy5)=9Iwp|_ zCbUSZL>HOKzxN~sR&HgXHfDp<%_+rb|#%8fzp|LbB{ya!0dhvW7{l)@whP(>w^D)Es46@Q%5e zL`HFVjV7hR2;(C}I@IlDpVlC9EZRVy`Lt1(a|xTekUg&+zR4~Y)-4vO!*A9~IUnAw z+RMIZ5)HzSt>=zqINGGC?u3e=iGp9$@}54tc`~V zW|6u0=dUh%?d+0eCW$F}%} zvMxR&*&&gM!?1AjS8`5}pA7ep-oGqm&eH|q(`C#zERVAv z^x-<9eZGQ})N@}9oaTKGs`O=&pVd6ZN8orD1y_H0#FsNI8!wD_x9{46AJ_uE_ZPQu zH?1T@YIUX$vM(;^4uH&lgW-RR-2zh#&y1bmRv#$L)j1XkWRf1u{4vw50&*Tt=n)ss zx+IPpc6dbSrbl3+Gk&A{P!Bc75w(S}zPk3aCjRUk?oz$K^W8{y+}M_glH{U+lhr}n5%x7#c)c*B6aJV3d)z= zZCn*4=lC^AiD?+D6^8^8CvdgME2z6_gH$1ER@Q%-}QE zYg9rhWJ8Zypes-bERcG22BSRfN0P`BU1wDK+mC~V=jhb-e!_EJu zS{UR=>{R1A4hELSwqJ8=(yb+;Bz|g!%G-Y}65Y-*!VZ2@J~ewMZBpft!_H}{MtN)) zy{@_Y;CT8WS6!cBPp1tG^8))3@LUw=Hvft^0cTFw-Iu=Ew_@4=lq|5myvPM|-I2;G zqn+Lh{+8K9DEiBY__w1jlO=gY>irgDg0Q*z^Xa^qL@{-Ehj<~Qy-|`_&N_uy8?8?i zN_!_rEW`=3vTZjX6~@Y=B(k)=()xXs5)yT|G^NRd@PflDa1MimQ=V7Bo_e$U*3%Vl zh$MM+H7;afd0VA+ntp0pF^&DgzhL>kZi@0dQ8Z(Al-^?xt_!hWV*tVCG_!O+95f6Q zzw49y4LMe6{S&q71IP(NvZwBR3bAE1R}y7t0Wu9h>L*vUpYb+DU;2b`gimRRSBY_U zvJ#omu`WDEj-SCAh&bggUk!|gTPBK2mKp7Ju8gUS)Z^?_iStVg*8k3OC12#K4%+~2 zVK*kd44jeSpPZ{O*8fEEGAYdR9~h3Fe$I!@MTa!FU4;^}DX${KI93ivEsHMshrJot zKmOxvD4JqvmtWoLpIaO~ezx{IvP6Td+2=MLJxs0+>1zFZ$Q`J4zx0;NNgb*C^W(!& zG<8VGBZL>^lHEQD)Dk&pX*os)-;zDcOx9SDCoqu(b~Kh7~kT6qg9x$kE@Xhb(UO93=h zJ+myCK@p#}Rt!X?_Xs%&lCm&jbd%W;Brp=#7aKpGK_-b?bVpB5Y66m5Z%kHE9d=XK zzF^s$9h|}wA3YC~{9MU&m5r)b)Exbcb`MATcaXE)yT+%bHSG^X2EQqv8-^dIEWRKp zKzC*Zun8cQlxCm_t|3Ih72p`~gi7z4X=8mxE;D%y&oQkYu-uHibmW;p6v zcwYp*O?KEqXE)h|YlWZSChYjQvYs;p1Q43*pu$aIMz}jrZIL*l=fJai@poSOj&nuL zA_jYU4oDun^xt-JT{+Jq{Y!Q9fV6=y`mFKX?AkgllxloDj8^K-Q$wHkMb_cwpo%mf zH^hk!JjblekF3_G>ew8t^?UB89cdSYvA;-(jZ002?nn!>_^b+me2QA&r#AD5E~YmP zB+8I2Iy%<(us-`6C>pRYccjJ^U_l40fa*663|~Eplc56XtMn>xxR}K61;fv}^p=w! zvAgyBgOqA*Z%W_VA4mHZZ;qiG0hA^by5n8EzKUK#pP=s{dYszZ#bMQ3OF6zg%YV26 zm_qRazbcDd0R0Po_R1V{3EV_FPAm|QO5 z7x7)9uxeMo!90{0X(pAuCa|zoSQu109LEm5z>06C=e{`* zVHs+{V&P+9M^Gf_FBB0aG*sA+8#C)%bzV4)#zSt@r>OMs=2D3GR0n%M23x~TsF6H9G_F)#EM5?yL(Ki;vkGqQGg8cJbc$i4f z7y)@|a_M$LrLl!|*MQFGju7)Qpi3ZV+uml^NdqU{K42v`Lj(t7UpDP*-L?!23eX1o7Gxm6tAthp^fgw&y~GZ8c-J zt*sH~cYKRpdM?wS(U}XCx2n3Fnjv0|lM)KY|AZ}0odBQ5w^#hD!TIFm8>5j;^7u#* z_VABr9sZ`=8;nyWGl$_LhjVQ%#$A2&O_NRc+|4T_o?}gv4tiU!88pdyH1C#>*; z#Y|Ph`)lZrE(gc9gj$-l5z+%UU3(V7X9;{BHzaA_38>Ofm+>I?l!{${@ne;~ z^U;Cb>ZIR~0fO0ov&8mqsbO1pp%W4_@ZL$}5sf=ffM~rY?hj`7gxH;=_EEqqskb?0 zglOB-avQ%1Kjrupx7alns$hPYFA~|uPAzsJ0}C~Evw?Te;1(}7m~+Mi7WdbnI|E4n zW_@M&S^QG+swuDby1b5AH$61{1-6?BI!hw3^2vj=@Fs`p{S`r!D`_<-loo34^z6W$ z-~|tchM5qDu1aeI57l4|utog`@*U)#sqbQye;!GJCM98Cn;mAi`rrMeG+no|C2&JMp8ME-Z^Qfb zU$+?8J$GcZf2<*6@-mF@2iekYChdvrFo*q}$NCt`th`s(y&CQ>aB_W+V6od>1VUu^k<3AjuD?-8RI#E^~6@P3__;8Tw zwon(5NB&k7D)3%G%*<_uhRHL^?M37VDm-xhsWBdX&{~S+U0`)U>#u!mYB3HSzRCrS z|EZB!bK|~z;(GQ;vb;k;IY#(I-07SA${Z))j9z0j87EQ_g86!ZzvC3Umdj+IIC?MZ z4Ie%(sVi-r#wA&%ZFeg&{Fc*{unqtW=|S)vqH9+-`eTs~pT_^4)*#A)MK4Rrm4e zi#T*AI=;EG5zsFgPp24?9_1Gmw%=z*#OOh!zGU;O+gmhVfU6-L--bMvxD@cKTO|zr zW`f)^4c)b?D4hlLpA@5BX0M8d8lfRz%bSS%?!rd7D zaAHpC+mO#am4m3vbwB0o5idq2#v3fq%M|a2(u2z`o*9@xw4A*W`kLlu@J*WTKVpN-Q1zErKw=Nl-^Ci?btNW zeQ}U0`~=H&{Y^f%gdOY8}Xc-X6QRAoP0W3vr#K9vAV(Xk9o@v8~Hs8be;6zM%~N3mnL zdqUktAJik@`0$%e)Zs5+O-g?G2_?_Fav!mb2qeD8@2KQ2xp)2yCBT{-46f8)H3JlG zh~zlq^gquK_LA6dIr3ilYSX%${N`o8Al%(RIRYCZT7AlR)N&!Yk-xt*B){UjG&1-` z_cix~B6g0DMw<`asX6kL7IcViz&c=j*e}9G%`Bn3FnpeK=x4Q6NRCv%-e~}0Z5&I% z-51}C-4H5{NBudLz;BX_KLEG3Nje;4t-N6mPku>v$^C6kMuVC&x_1nVgLAPv@({+n z@Ar$w<)~eB8Pu6ZXqjvw{TYENeyAy)c0jZ@Oqs=tK2IKoi-`yB?{Pb75d;>WS0~@^ zl-y*y+!9)g?dpfGeEA!2(Ga|eGi?SGvVE8pp$gF_53dE6D|hZ20ksyF;I#XT=^G`^ z)`ktfIbjF2W^(Ko2k3eiY!~tR_*_mG-1nE;i{;l-r(g0ay z;ezIgXXE}XyXbz6nV}^0wi$s6O~!xFXos6HT!;%KVS8?6^Z1wkhoQ9jF9YBC;#dHs%$4w-K4tS8^nINxnP(PO zl|A3TsX{iE@ntPIKvazJp3)4XnJU-o;3}O9cAL)_J#AlZqGlD_D5TD^+=Aw3XVaXo zqUeBU1AGFGt`-WrD(;4!jE78UZT5miDLaHZBq%uyDN!;eOX^?M01-*_Ftd5wtf)Fl zxeyly2#qK1_dp*kYH_$RA0XANq!HPuu~B}_19;}6Q2XHo2@J`A?l1oSqNqa%u$&w~ zg=#D%NtNnd*kpBD8U?&!EIr>$WC6-k_fL~01>F)QwJtgHDKGh^&VCu>Eir4&e28a* z=pTZkReR-JR)aGJnQHH6DiHX0OODn7sgRzp?QT@9X$_K^y}H|??uinsQx&17=95S z7Ih}qa3{0RUfxrsL>xAoNV3ikqW`SF7s82ULL!QAg>H7eza__3bFi9+M;TvRodaLen?)O9 z@{5~3#Zofa-^0lkOs~VLR>|3d3N`lm#O$j6YNsR9Ja1r39fk|LAje|6;~Cyj;CgWB z!(%{fCjrY_**X9Q>K{Oz1+d^tD>}spZ26R`_I8c6$=R9mU}o$d3^dsgwJWIf{xa1A z-rqIuJesk=L6Z8L{lpeMylZA`eY;r{_iTOtiNSdMq{i&v6p+Zh)5fcSkE#1rbn5T6 z*3z`c@>g>XDBCo7uA1WS_E*pP-!Ap>Ug*DpA>ap@;j01kzaypo|IYvKwMNVP^Jx(9 zeAeg?Ymn@;W`#K9w-E0fT!dhH^MrEmeuxL-njC^X0*cOtdZb~G+5T8VA45G(8(f_fChtgB1o!1gv9^VCgdLQb#xZ| z)hlOac2@lSHw0^yFz~-xfYc5{W1y`l4WwaaX${gqNk}2U_|imAOmEDV+(mRlNFb72)Djq{)v2+!f(1 z(U7aMi%-RXO6+l1Q{SL-;FWsOPU0QB7mxmiN-LmqH@JR8R;vm(xn{!YNGC$rZr1x0 zbvwg=!2k+yGN@Y|UbZ);GjwT2v<{TVsL3{g)422RT;6OEd-*H`;RVhH@1?xZhWGve zxT{Tqr>%@FZGqOh)n7=&qLV;B8KH;!?*IX6+ZK3+m>Dd<)xJ2(1f6(+9lqgA$@diZ zMIE}6E^%shZ}MJ7+&U^up_a6ko{j^^HMy=9xZqLw-)=AXw7+u>bPG5a+)jw_O{Rsm zr#4Bt!VgpabRzxVcg*J7)q)9s0&5jV>lXaHED3iBUDdl>PqN;eg_4(5230Bbh)pqh z4UGb6l!SEBHW{;4Y4gmp){ywKP`Qbcbt3dS8V`gHff$7aR3u~1gxibj36SaD5he!2 z{mjW?Z_lKITa}Mrha2)%*pZs8SzX4AozIQ~3bS4spJgH->IwmZnYzdFKsVR@KdCW) zI`^B~qOAQ7uRTkn(r+?18fP-9XohVcs^zZkvJgUaan%ar2gmlF*B_r7OhDM3e6(v) zk8ir)5~^;xC~XY9bp3b(kDg>H4JR=yLVZh(w;-*Z}JLOJg z3|g(z+4{b!&$Rscxv8_)W=*4vw#eld-bR<%s_*!J)_4A0-D@GwX#Voij(hLX_ko(3 zn1dO^)%9=6nLTQmVUUSNshM-M3jgX5d2l3S*0UNfu3C$#vznK-u51wU!tjr&`)*)k zKSI4xA!Ff86D?L)%k9~64$Wfs*pP=&$mQC=?M9iN0iUpXy-g-UIyxA*jKjGvG3jN7 zKyk~JsE&42!^r<-!0kyLOhcG7psHgM*>|2XmL5Yi1HtZl8c?ux8!!vZ5_O+DPps+SeUo{ zF|;r=kh-90*%RDYJ^|8Coqvd*_UK=T97zwW(I&ZF&A-MXuy$GVkf)|;?E8(a(1WPJp=tg`9(GSX)y^=(ABqw0*xJ$JZq3JFD`Pd}bHtT-&!Yvmpef1$$Fk z?;V>?s#qckj_w$$#goCD{{~Gxe51xdO4D=K&^I2v$|2OPS?fZ@^9pI5%Bt8 z>OS>=M`=b;Ktx4^54N0kK)RD+Q2PLG*rxnL2|+5&Z{+QgSP}-@LVwYdre^FwqfS|58d zJ4(*?A|#FxRWBuE46nQg-Aiu+8LMR#8Hd7SV~~p&Gu{~AGjgN$8wOl3JtPvS^^;BlxiHe$quoRO+eIDdu??C znAg5Ber|As^WB2-Cdxh2;#=cpz}|-DvqMy45)~uz{EIs_O;)QOT?|K@8RF%+hT4H* z>E%1SQ~ZojbwIz{jsm|ej>tdQJ_1yS@qa5}3Cg_?KuV7f;dJ>{;$VF~!9F)UC7%b| zy9tVYVBIhK3n-M{nfka{D$V}swEe>=U#$oHtmxq{P8jnFTGMx)?9U2NNmAuPjd2Q~ z(uMjv{sNK-%~+4G$(=N;Ze8>ed^OAPUe3m*TxH7C%)E{#l&KLpnO2Bh%n@1D4b|Rk zn~BpJP*~DMe&-%#dU@N)_9+B>Z5?R}4pLgb7xX5};tf}tgILpoZn8n)*Nk48LY260 zAD&SM#qg_GyM6fR$9MnLU4{n*tTcBf4*RD1q#1_?a|if)I{i9F>hoLc56$eibnQ0R z8}+}5qBg%+`-f*w9#+Fn!TW}J4JkWlU%(z(^5FhM<%Q=aWz9M-r-VE6tn9)B?qglQ zL^=^Ur(F(a;^L01PF+mcc5YSYP`eA)e$2ET7v;AkuO5dMew66JnsuCH0SHO`) zBAV@BxAjYpVc(t6b}%= zX=a!gep>zwqJ8b`x%%=wfa*FkIC!~!MI0j-4?33@$?Y7GLCW|{>JMr-J6~lB*C>8T zksf*Q{Nk|lyP7UO<`t$fxHz+wG-ar;3ZwCZ)(JhSDNNrCH!M^;&DxMk*8{9GQoK3u z-pm@qo&f&v?pA@aj4hN2L&8*D?;gI_s0NesRcP!;(mZjiU#H%h+a!xg!4P83a9$yM zea6gwTNo$1@pNzb+Jr_klK3#NK7r0kueY0Yyq8~m{8QCSJ=eGhj{P3%1oBVwiO!C8 zhiTrqv0+;FeW`A45+1+DX$w0_g~sv%#=PclAL9A{U1SjhhYOG6U4#NUQq(nRYQ(MBBdf=BWk1oTK#l?xh>&?cs_7`%B-hH_ z>B{by|1*bU<<^5uR;D=X9cPGYhM{==B}?%Hcf`=pI-h9J^=3-{inki<1~9{Gcckg{ zSv9`g<$Q|ERA~BLlJW|6L7{&?m?(Cz(Ivdk(SSU`=OfwX9Eh23=V07lBKfq}!D8`_ z(ju`aKz>#`(p{A>(1}U);t^VMH^FW5joEq|JKkmeOz`iN!=JT(zr&Nzph6CJh-TA^8n)~U+E+#Wi8t@GD=Yqs)JUj4k3 zXoL`x!qe&zPjr9~A!LHu=0krOc6*?^?0mIRnJnFuH%kxfch-dHX5X9L?Wc)m$iju7 z^!lu2Y-A_p#xiuv3^anlCa!kT9JFcKJK>4-B~7Q>U%ov@(1K)(tu=_X#Otlc&RpNm zN$fn_tm)!&%|&@+XAXH+-^HX~5L<-(G+F|mF~(!9D4$G`C17fy%)fW&9$sJeMUQNs zy5Fd_F}NYQV=gQ?D2{%eInrIEw8O_bz5V4>qHNav0ghK?-jShuaLCg50gg)I^E7?l z=P}P$ecDoBs=7cTA#%U;{+8Ej2Z@?H7U_hM{jD*no27eeq`|SglV-j7g)H;cz-4CQ zUZCv0+`W03o{fKR=c`sn9{A7O)PV~Bwr?>RiX3vW=CVEYg%4kuUlx1Bq+A@?JLcUn zhaj>c3dOh8t*gA{1%J_*pu3Z`MC)l$gX{i}CTw+22Q=0s8Yn~Kd^JC+B{n>$u3dT4 zx70#preV^@K{mcRIQf~klR~I2B$Liw5$!nuxF2)BEFVHXW zwseIT%Wz>G+NhUh?K?=ZojXeFehqE#&}Yf-#yPFphV32}yZos8)z2!;ew#(wYpshO zmw#TKccc9l>W89pR~eUgzK02zK$G0E|FP!*nddCxFS4`oCjO5)<~}NXN=1e2FuAFJL?<^A$lZ>WaO~H{GbUu{?C-~@$Uk1NZ z{gqWo+M!c?X+wBtGMlP?VuRD@G9Hy^dwsZ;#WfsuqgF|ADNUwknoK%V(Fc20DKxIt zvRjZgIOMOA9Hnr1b?%>u?=)C!`Q(IcY^HA_yrQyBsq41ur=*QUZDLmdpO3l^_s;xzAVpA)j zsx>QW#Ow%aYwaphiqdFWiiRMly`uKa@6q@7bN#L>f4DBmbDrcm=e%C8`*olDjyU`n zda9bAa_#azSuaPr%VugcvUdg;BGSTh(1OH;{eN1E3Ntq|PkmTL+~_1foTN0D0S8*T zLM|ADM8_^dxj7D}2&_+R;9g0&#nG{@r@qucKm~LDXSwl%MQohG%VJ>b6T0%(#OC5V zz0tF&o_GI9li(0(BiVmynz}s~{<8^v6?VkP?yu@vwFc3mj=HPUiPSOO7%7aov(r9d z`iF9Q3k(`652sGI!T-I=&A7$?X`P-9dph#1|6&72AO~Q`Y<2|7^M-Mbxf21Ki;d(2 zKfrM!JPB|}S{5ngS^K!<;#ow;255rE!%KlrFU08O@I0tvjopHWK5q{0yfYf>=M@5@d>&$eW?X-G@~*O!X7(SyXRY{ z5SMQ7+kr`k`W^&q=QTzLY(p*G(33652$h~@?leK%L9SunqwJ~IXy#}y&fKOKmEH3N zw~f7$Tw!JLV(qF#YcsOE40QcgGNgP3aGP#r23qvD-p+H<2tL1is+<{USEW!psr2}) z?r}Cj2NA90NE8)o1s%1v_7tGg>a^M}kB{I^Y}^hnz!~9>Md!HdctPJ(ogGX0$)1jd zx08g;XIV%-&}fC)jpPDDj=*!*|2>UBD=}H>3T7OWBa8<+%8{C0!{B2?eudUHpW!Vp zagw2rVEIHvbcP9MY~_Q0Gh}O8v_6D&rmLT@BjS*nh$%p9+}Ov@an-Bf`JMiiE{X3Z z0OdvsdlvhEgjQ-dHQP|?Sw*-Y_YosVe$V8=ICY*qi+cnrN7x9|x;ztZ{Xh;B;REv> z(tL*z!VX|VA>q}==n40Vkp7j<$2}gBs5!c;KE^~UXe3e+rc!jfa4l4S2#P+`&+l1# z>d<168_++7t^`49r?vw#e+e(JfAWA84oO0oYgkUS40r7;Zozb437@oDgdFignZc3B z5s&dfTgokDb^ppZEa@j_w9rKL?!GaeFpbT^F7K}s6>%#Mq4K1JS2N4`HXMOr|2!-2 zy$O6DLvhr2|8vtV5D>l=yxBwzMhOm-az7+wq1Gt z6iD&SASAwfE0FJI>}xYtm|ldigqC9{=8nz>hl{r-8_};xYvL5oHzv`J&rt^iM0>Gk zq~%8wC!|B#v?tfWKP zdA*r5L0Teu5G@;f7aC{!z8#kdgebs0K6Zyl0QkYCIiJ&oqwr;IqRWDh<5~Cx1y3l& zo4no*LDVB$2Ln_!1f7=8&(}tNtY$Cv6QT{KeoDeq#<2RdXyhU~OmcjrXW&Y3)s^oR zi+K_yC%YxCyO&9I5>@+GvAN!1l(#n*y^5wdp8-+!<>gfehPWHSf++?%pEeVhQ^{1G z^Hc9He>l)ArLkFKBj$SYGuyHe!4U=JlMMNF7i-<`z-0H0mB{K}xZFT0h+zaR&RbG|HIt;FU?$`q5&*Urzr7K1U&dGT0~O zlou~A!!9lHK+`0o*4B4q+?DaLc7EwG=6$z{-}p#(JAIbtx~r)E%M|PLSCN38BoOmY z;_d=!mtB44+=IJR0f6Ph+YcNRXxW&~rCQh56tBIGAMmb@6>+D6?DWEz_3yC0Byo@9 z?4mad?~33}65C<1bqn9>Wp%qg%&gfSlg)Q!6ry$KTAHRWo@2}K^k)%oIS%@o&yaWb z?XimEA?jcZ8L7WCPb=}l%pP-KwK0AqUY=CTq)?1$D`JXI-H0OxwoF<$-|qtZTs4hY z(?6J?I%>Lo7=#TL-=5E~+pPN7Y?nc8ayW-o0>!iLPi!vJiyNkZ!+ac7z zxuHUS%e%q8-C8^#`*#B>EFt4dRs%cuX#oVSAM>-t3K{t9n&?Yuf!kD`mgpNM#2A-t zRD*O`MCN1g9TmTcfTF|A!JR0TH#ZmGi9h;J>Nw6JhtRXn1ZlZ3FpCx1V;grdzc|tk z+A>*`+SE#sXPN`u@64ca`4n%XtA*lU&&VvGsquS!9EVQsQ_Utw1AbmyEXxKtT$5)6 zb3O`q!Dg5gL-ao~q~;0NB?*%A2_6DqffVWF1VbK!FE~KqP}nkX7LrwwQZg@Krt%D} zoj;vgrxHTa2YEilEn{!PcEak}dhbNjl5`f)vNK-96h^%VhPd4`(;d7ZjiE zlyHhH`_g))wiNZ9HtIq4;5FjemBsV;PTHmxJZIcNIwIH9FhLV8zT`T$A@9)*Q>aZ9 zZ@n{nl~(r$&FlZt4yK`)WS4E`q7j$DnVO))r>$z zzRx*Ab5z@)FjR<7^1w&jxxj4un>nOV%1OaT@-7$PovsY@^5seFO|ydrO)Wj#p({^@ zANd_^nkSFwFJ*SL+c&tPk=Lxw7Tv*TV-g5}nYMRJ-x&E>WZQtI3f5_BU zOW;xUi*C6I(_6+j+}Pw@yZ%E!0^BNI+WjUXN^8O1fG?FEwj%S1?o%TD)Cw-KRc86? zfH`H2H#NF#>>Ny=u z6Vg4Xfz`Im)moCA2qf{AAqQ?N&~uCCH1%?nm{x;EXVsNrEW}q6=&fv+CcQr?Zns=& zl}lSPQLN?Mq-DPtCcjz%*CQ6#cbcom%leO^DyRRF6 zZn@t}=7;5gem|d{X*9IurNFu9vT-eY3dJjtG@@sg@FUdJ*P?sXQ8`OpNVuNpNrwM&O1FJBzh2D z|MA^{WYp~`rUb8zdKBg-O2u=nYt>L}_VXHc166GEN-sy)L9f!~q3X z{7}z~q(Pg3zm->vdz;MQw_Ics-_#<#2R0Q2QNf)$okNfKe|@_YO3azw82K`Izkvy+ z!q|vux;LNz)gFK}(g~%`R9oB(p|3*KJ+OLxJz3$Q7o1u}UTy-|8zM_89*yj@Bi`LRT+oxrZ;1j7RscGxa2-2BR30p)%BWNa1qElOKrb6K$ZPV!;|@(zxolKBK<<5##?yjO(I&q%j9tH$~J6J|X!^ja^^ z-wb!ZAfdq?j_haa3(=P>LtX)j#66!hlqEyQyRh`3!Kl;?L^A(WDWi#O8bP34HH4H< zNg$ugOq_9-zcYIP!xLb80S6|4xo7=$2baAjxRJWuShF4NHL3LdZzx^-`=$@>E)lxn zP3Yqig~qj}xUM^)9-oU4tnl&z$xx2A^!^ox(r8t!TQk?294IU1wy9M1%C(c3wz1%i zYfl1ZB(2u*$|fC!)xF~Wc`|;^nb_(cNz`zf_qt<0;I;QC$P6%dW`by5s%K;u zYT_6AN>+ubyiphy+S~oyzH#_VnBr9d7I20&o|_*zuQz&Lb=#rB+A9$g*vt?j8_LiQ z`!+Q)1<{J_D=+oc(c=0ux7Tc+*AfjTUwht(XhCe^R#JX7aGcm}@cMv;*h6}9IMo=U zCwz?+cg4_PGF2+`Oy+(1Y=UK}_Z^bLktVqs7OXp!m)TjkZ84y(g`9l5hP?t)$+w8d z82QnXwfE*{_>a)z0{Dmx;6O$8vd%d*R?7D%K6dgUYt>gfKo|zFL0-wZv_FQhnQ4({ zSi1CY$FqBJSIzvZv;fYq`M^4ACv2aKZ1=us1Dlvm?)pK|KPs5-l9lL_VF6GoF7Q5d z2`SHZ_$#|x6Sm`@vCrBSLZF|rQZBesTPAVXdoDcE@fOkKNSwqSaWAinCKQj$VHzHG z(Mw!QCNFErM^Sw7(kc4Rb6BHBOX{mVMvK0Zn}R003S*T~-*MgZqfbdz+--D1GT|`s z$^>~7{WSCXJffw{MO`l;X?!DYOz+4_C_flQKefoA^L5)n$RR1~-a*Bm6pQW>*nOPL zi>y_oYl0PDs^(uMG6rqLW2+!}`9l)xTfjWhcI-nF*9#DPmWzd6;dUHRl~K`9@l!8{vN>(!*_oyiU^ zdG_MD?Ld63z6kKX z0xfvM=#@f+EJ4*>lztI6p|pRNp$i}MtcCa7*Nz?hc9GGu)5<`JS*IEezMrOaIUso< z;VT%r#^yCh3fuZp`6YDDR&1EgQ{RwhSfv)h-1s zvUxXI>UgeeGX!jVx5(swpo9?z@D&#XR%j~hioChBe*CN!^Y(=8HjGu21+cD_5DL;7 z<+}pLDQx#|zkfWgV?x^2w=2Ch9-S%1YTQYtL8w^mrXG=dF%rCB3a1<$7ErI!KB zFO?q^K)|RqS{14&C)g$BPz_em|4S8t?`CvrkZ}o%yZRDKsE+*~7a%a8YRMWBE_xi< zhG9j7BlnTp2>!o+QFPSw)Ug|Z=G9bqs*)T+({v~*m_~&b@Fc25reR-WyRpus#}G}` zOQIwON6Bl=ovXeuPS5;8%r-pa85g{(SC1$@?FBh3!$hl+T$?<8Je(NRs;XEkZjT!>YK{I_oEDB2Z^cgu|Cz`LzEni`xdiu~eO zu$CxbhLsMb0G_%3o?d?1DNHirc*mAN)!ambdJ2j*^1VadNR0=3Qccp+QDx~p?!^_* z0>Gr$DvMXl`pRxsB_q`Y^>^?AC>+e}?1Uz4qIS9}FCS2~3|(2DDZR|^L+1m!3hmjW zt-9G{KLD5nNp5TMTT%h-1JT{$16Dugr}h@z8jK<8Xr2emyrw-za=7H6Gh~ja{C2Px z9WKsBE6^nJf3xEo$LZ`y7jXxX#Q zMU|aQBjmUt1R29P;|zsJI#?mtj-N;F5U%Z=Qx&th-`In5q2Q4AJ&W(A7d;)$vumlV z!jsT~vhLy}!Y>zkNK3Iutu!3^*hcwjEG8Y%iIDF1nwN@v3R0a~JD;0N$dB9cCVMI} zW`D!ANopFuea8Dgn_Zhz`0s~0p$OwcQ{!z+s1f0E(G4NdvENZ(MRzzFLB~C0a!izK z9p8hb=QMqo+J5ulf*{2O>Pp>Sf_m*Vv~}QrkMP1-E+bH@#*KNNuEzRtYwbJ0 z;P;u0+b5M;N&f%+B-%g;&k<;>$Q+MHzQ@+Wf-PU;kDA+a{e>5pWuCMczTDVLGW@`J zP&BgZxfLeg`aR2j-ng36<7>pt<;wzeMI%35lIP}}+CAg^{&&e6r<>G5o&frr>AmE% z(3+kC?7q3F$Njg%9qqEA8m(0_?k~`-IlhL33k^oTh5FT^1Iv!k#B72MfI(pWE0x-D z;?73U4^k)YlJL1ew9@}BTsv6#lM~qeW-(p6Slgk$sHb}8?;3UEib+@Ku(3K-OrN}A zz?u==`yQ#PMfU~!ONZb09`pv>g%*oYn&5yMlzmaXVd?;lO(!xn7jvZ(9igoO+a_TA zl&+9(M%}$&TPX9?dtMd5OHj^0F_pS{hEK`oW!=*}AJp(?un8={{+eka@u*Fk{#A5Y zfo6!k!8v2e0Kx;XiFQu=$*G-|74{}{jH~*zBXRl)48G{*%n?W}cAVSi6P-4QTOmpL z2h1=ZMwH?F_F^JaY{%oV9Wa$!15sEjBg4hVki+0KS$BJ)pwqC#pEfLDYh>X64U{@1 zCkW)U=?K~YMF2dU@ptm3CZ_ZheNB<F@yez?V*f4T zIz^Vs_|51Nku#S*4At6uSoNl%n<^7ul_or!-n`=GMDj2P(Nnj7sg%N2k6n}MdzwXl`FdW}Z{@#GA+iwo z1ek#|RAM1di^#N_BTW{akjfX{Z!BIY(N{%ebMm^we1|VUfG!%!S)%?Y@gpdaG~y#< z`x7#Zvv*J2u#WxDrwf=QqdEV*X$sJhC1p1lvUISI5}q|=K%pu_B37lp`}6{OfTgA zb|!l8b`L9X+tz2G?02Eav2#6WLXc62C%+hLhnax9`M?oBG`4@`IL6SyQLX0jb6gf` z7*gBAyd8<`ADmQegpcad-x|1$6~}^LJL|B7-8X+(w11RwGRM5SaGH|j;JsnNfp0I} zV@D0s?ao6W_|GjN}S!~Nmhs2`typKW|bIO|r`@lY|t zb)k1S&m6F(x^Vi-G!xi=OYWXL;nVugWF9P#GtEIfQo(l`eO;kDYP!1{mOm5pRH4Md=NE&bfo<3rjr%Ya z2+{l?1cHRK|1N1xXVwtZX08r$f(dit?{Wl6olckh+_#7lq|TJ4DWvasacF7b5aK9J ztP8&6h}LjUp||foPEe&$aqA`)n-W;V-tqZEw0pnoA4_O{r$RkFO998cQf?Z;tz{|k zSHXFGVLTu`jb||(x&uJg?>}rFq<$h8E>fk%t zz@69ze`fkH2G+rHjYT3F5#gqr39tqyvz~YKPy=duQl?}gSeObwT_q7t-43+-nH3HG zICFPZ@6KRLVJfeGghV?sRMF_rKH&L%p|_PK&jr?Kjvo7jl}8xCzadnSJ@9grFtv_C ztqJPZtCxR@~=eZC^uf&SDNx--P z99o09DoE$!&&_OPSagy!vx?iiA0@q8yNplq&L#9Rf8@AE02KfCd2+#>mzM?Wsn#*; zWkUmpk%NaN{*gk4K{~xdtr<%n{P}$HUE=(0jbZbsoe1?(=O`Vw>)i-;CPnbliz|*( zF~mas1LF9X?Oe{L%9Qs9^&PDm9E~~U%HOM*72{|0^FOrBAlBy(9yQ^G%f{WL>v)Elz1csDJytZQ8Jnvc9GA;6`c@QlTV_apDlu9{2phDsk-F=ZSM&Y=I)BV znM7l(QfrxzFMdozK@N&qMt($Vj|-(&#$irEiSe}lL&DjF*A@r!Az?zF$F)+vglu$! z8^I;ngg<8q>WE3I8yorW-|w;gVK%qbo%kG5GvTw*3qXTs)R-y8y!YPX)tkgWlWoU^ zLJjkT!N(ipFtXrobaDHx)98A`nWn@cwFuW8R-$brZw)Iv87-YXK)Do3r7L7LZau;w$)U%BOz|z}?n&FaLA0PF>XS{iPP(X$CS<4q zwk#_oycY)$cG=06M1L2KsPQV}p5J^An1NGN&9F>%j$@l6jcB&Kr_zP6o=U3*lgJMz z>{HB}`K1qDISViy(Xwg)tX z^3OZDOwKN=&P063-#E?<6J#r9WTq!R(bn9x(;tm5ZYaeH$bOQ?mpF}6<4oO_Q>0T^ zf6H4IS_#77HtkrrBlfC;(Ze1= zHFQkejjC1QKQaPL$G&~v>DdK5uqlhj#f!)B>dgjb<2N0fQSaIMkL6d=$|*MW`$J*) zoj1UpkSL~wP^`ZAYRi1p{!;US18g4p!MLy9u3%Kow->?#Yy$2Esz47~AD6J6B$#j* zClr&*{+;f~ZlF<9Vop;5Q2P`3-W8$>lc?$ndHTJpk~3%8QR=fa&u9F6=n=g=H~7)z zC_5^>s|3sGVUa>o-wR<-d(YeVu6|d#r#2Mj@_wcRRq;1}#+p)e@;52mN@4^(G*ayN z%%qDj(ngAiJlqd48UHxw_dvGws*^Zh5^m*8#3hOXBvj9gTVi(I#&ULSfk;g>8tVa`!_EH#74X;k}Hoa5A z#Fja!#EuaXAKbDDPN=n>=+IAW5vs=wG);N~e?xuD=gh^sAl9f@(CVjfX!^ zlUL`g-OoEh!l@b&-xjg}D83T>?d!`M0eVY-d-3-Ln*NpB(B&BrXZ-YT?Ioc=e>)Nn zN=2#G(ls#Y{;QCTgEjMWcG_iI?$RF)&82JngA3=%e*!>=iUh!ic;)G*Z%?3_T*Ks} zCaMNb%sd!U|GZ@7vpVQKS~Hs)3-<>8ySAKkNDa7=*NKnayQi|kjPr)^=MqL?4Qf7X z6F);$TdSl>m#){}x5@vMb=Z=l49JJ4VYYT?wbpL4JARY8I95E8CVNi>^FEcXO{q2? z=S8K!Q*%Y=*(N`+V=asJJ?`-vuS-!UnYa?BNfW;p2?Fue;%yy}sJcs5GNqMbMvX`* zcaT&jD{HDI7mXeIU+w(JG~+<#$L~+8%4uxZGpD_;XYBY{EX9f}T-3{fG{2QamBG@L zZVpUa9LQgGXUMRdi8G^IKxIPRPyRZW#_4>?D0DDbl61cTRW-8;l(V-Dg`R)W-52~i z3zw{1(|0syqKLj_6BYF>6UPosbdH3VGb0EA{s!~hTckB+Ge3j)XsC4+{L{X8K9!-N zu8b?g=C6zpAccjFN<=54-b~$T+U|^m!kFL|Oa4gAxFR3aiZt74cG%VI)PJYTKNNYi zub;!uZq3;I>$PP5!aZyxiY-&A9Brgzq8uikLAe5ED#@e`o3I_wKz%Io489^X2?n$r z#s`o4&Mnqv$$X!e`cx=RtN(ov++;a0i(<3QYIl%!)%d%=g}$oc8P70 zmt8n$Ds_P+3A9hsm;`m){uEh^;`LW|N7TBPaW{2EnH8-L8b@*#LLM!JxIo9$OIO#M zKZ=@-Ctj<*PVq61Y196-5UvHr-=s-SI$IP+>%i-x%m1x!Bkn|};-{)KJGb-jYZMAD z)rKfjnRAoU1*KLvRn#&$v6kPML&g|dQB8jEH`85fjN1X4_yLsa5B|=~>pz*AOtm#9 zEJH;|A!wDMnRmqcTeBTw>8{O$v*LY88K;$%aZ;YHd2nOY z#YGd;oc?O9fd&($gq2*K=fw{FHXkgY=uN?zH-5ml%Um3g`(~<+_Of(A66Lxso_kUE zz*xe7R@K!Q*4wehi#nKh{*a$sppKqp*0jAXPY#%Dpn*7|B zhcd&QW`2Gfkts0Xps`DM-ozNiCwwK6h-f6!x+q`m8Dxt}L~An~ots=;dun4E*wdbS z5-XpBP&fv6>F88Xb0vGe*ihtR z#=8d0A90aGH$KIPWM4T~SS0dJIILim7Pqnh0MSBX`6(qsQcf2K527d7BIFP0r5CLn zi!`1RI&eNFidm$vmfUH$0qC=spkd^GRW-1srQ^D><3V4Olxn&D=#ES{T2Bt+kwCH8 zhKWZHx9>(l#zc~XXx0pJuP{y?_ifgoBTG{DaY`81Fa-Lz_-6nRGu_4dnJSXxUry5A zMiVY210rtFzCUqrmmAR$@dw@zehV{rcXtjrWaa^jgLw*lLH+gnFJf>NO#@+PY>`Uc zS}nIc@s+Ak(T6ikIjXrI>3UfmIZoPFZ$S><-KX$n4;F=z|Csf!j6g(mGz+?et`fx; zlp*0Q%JbR$o);W%9Yr5*&aEi{%wL*Q9^L}+(=$rwx1+NZYA=wC34mbm6$t(P6QdGM zv!-eLEp;QPc%1O~%p{>SqkkGq^iqjVVt*%iOQYR}=A?5rsCd`29}=hk>RmN?lRz`c zrxY=Nd2Bn3i8rQ%wLdtHN}-lhEE#0n)BnH~-}~uWBop9?9>bl+-Ic2HSg6wIpnWq~ z%3FK*W<*v?(>Di$xx~3X90P0zn3w(AeG##MN_)=g+cYtQH2N`seg!v37h!-YcAS>- zx_4^fmqk}4SigkanxMYj*yUs7`W8NJ-(_?+6`P^l+3z(&XIxGA2c_uVz5RiEk6$S9 zp-+Sv$y=-Hvx$e*69D8m$H)u-bB{3N7)$hUpNLz`bdZ70*B!lnyhS?mDagjweOHSE z251qoe6IB{PqYDivlRsY(F5FK{_KV`)~FSXdZ}n3xSN=})md|_Pcg1K6;-O-Deg!s zEDT&=psNNLa4$*x>oXYD3e*@ukH@eRu$@Pmcpx?d5Y3Z6urSxR^8hF_|6jY_$Pt#R zKT2h(#lU`Doi4NkCoyskU={uYxaIW!wT+jsxHbAU-z}}v3ot{+sBLlyLPo)C#kvC; zSl@oH2b!rZptYUdPw3X6z8;!vM;I$U6Gg4=%dka#JDbpwoPUplX6O{?JiXsDs9?Ha z1K$vp+SF0EypzzpGYIDXm9hrjOK`Dm+J>&dm8*|n@)nDY;2->Wx?gX$5U7vfO*A3g zDA-4vj*7o-CN%%~{)W=8)u)(dfQt}Bmf@s$bvf;H4_?Q!SBwqJG*EAnHm!NoKuw?C z4+3l8{C^A!4P`*%N{)v6?O7_>88b&EvM=P&m!G9wtS0QB#gbn@$7z_Twp%}?hJ?^7 zu%yoEoZs_#AB#~&=zt=Tod|A80mOQdADi10K*S%!(ypr6yFOxpKMrSeg9(EVp&C{7 zsf;4-jHKydjbHzU++}xe$9g3PL>3uu!w$(c#b5Cd z2){y1I=KM}m$qvk8hqe{ywmSx%NhnMr;0`TU^lVpL+6rEbF|z>zCGE5lF}&Mqr>Sa z$YIS*pEIudXMzX=T~X%J2`1DY>AmOtQvNK)uh((CT}Oi`o(3I1F~5OfS2O(R5Q|ApsN#YPSO3>}GDZli;rt5RE_ z&wHRo)GJ%Z2q(#>4AVT-&tb4lIGkyx$rD|?1KVjQ5+4^e_otW9&EUg~23%(g_ogXO z$EyGrikTz`1waX~kzh&R`xa1pXLZ*NR8_|~X6=iewF>>o#YJ<-VfgbSe*k0_|M7M& zlEAhg?=BAsuX6sajWF6+ZfwEa8Q7O))uPLjTznPr=_TgZo~*)U3vUtkl%tM@zWqw( zJbJBoo6E9D9d>r=cvxRb3~m=uOLpv*rwXU0yi_uy*?YMaK!THm9M4t#*1j>|fT-=q z={u`*>ZqoIMG-}0{ID!`9jE{`o=))WfuoE7sk#tj##(N;7+KJEgB*NbHN1_Rei(cJ zwxTN=M^utsU6@I*BZw+>2wL;{z%lkAzyiz9y2S92`~Tppw%E`>U>%9;8yGy!H1XQf z8r{#sQ?oei@rgkcp|xNWOYj`lD$dfxK#&!{h&p@N^FT0m%NUc zn{DXksW@id*2hoDT6B+bJG|@vYP;Ua^FyABe|~A6{(k9&OQd{_ly;6b2Aj8-)t<%X z-(_4?`@wtH*L@t3*T=M)s2`nYm_BiSScvNi zEY3@C?!IyhRgZpOw6QVuiL<(NN*>N>FJ%{MloTdbjCSb&XjrdHxInxLCYfmIizw}sUBzA%()=epK3F_l0CO63sV76?8j7_75=ERcW_O;_5>`{3ugd1B5SaABG_6+dO-z_nr=>$L&WZ+xW}mo4%&gy(?lXbsC(jx~);) zq0zfN{l%^(>m|>nd|{%#FL_;n2@JVywl}cN~&CH|(a_ zH}b!`bGu5OJ-mQbs$z2g8RWIJ8Q^$q9h*&PMHOKCzZBA|G$!z4|OBf79xd;4FLH`_30_BM|nl%24_ofPBcXCTB!ueif=2{#~0 zW9KovECEWdo799G6PhJ@ajDBC1v1MwpR4r#j|<=>^dm6T6JO8A(&ZHU@}mWsa1%=D zvg4_udQqpQ@Dq32JnagUhh$~FCbRfKitI`}{*!QDEnZP#I#bkT^YHcn>)!k;<(GGL zf0X{0kNg;Vppl{FKownAT+65h_Hr(eV%rOjFE6Snu%}qNTnSPvIQYACpI3TDDEI1b zQesob!|%4^Qj6Oi(?r|~;pMnf&;2~hJF_fD2gg0QiOPWzVV{F`GtQpJWy+T_-8d=! z&;TljIW8Y>T%fKTb3{^0y)({0yjxFM=;g1oCBGig z{WuCZ|I9{jXxfEX`7TM;FRpV6^>k$KsQyVWFrA6d1%%D+RHTb6B>bbkh%9(~O3Ix$ zzjl!$+k z(ii8o8;myBd=Xr6sJAph2U~He5O>d1yyf6;#h1`y`!$qXZKw}?LnY^mVGCK#$x3VC zQTMl?uQG8xp>8y9H%Kk@57-gU5PCXcnPMz-0E{3f>xYWvDUJ@ zt~!za1#8T!YyUA?KAum_AzXzuRHFr5VDlrTUTy{-1ek=^* z`p%#H z`NEg1<>K<=3xI^%RD83%M*tsM33T!N9f;=I0fwMeai5VrQA=Bi|Hr!QhzTsGG? zy-um%)-=1g4|@xE-eB~d$RK;u*$_~!9ND#Zk3!1^UVEWbqu*E}dHL%=QzK~T<;Uu& zD^Y+`SuQ3++#B_1FE7Sb0WFsg=b({{v*e)JzThsb6*>Gf82}*ao@~jm|2y;}=WS%l zt97dxb=NU45K9x)#rg>>BtUBoV@26DDXjCMx?>@fJ2e)T1eh9X`3SU+NsA z;1~u8ms~|TKnH;yF@ImJ(WlyhL+IUmTubff)hD-TYk7w3;vfhnPKba5<0KfPpj3?Fz6wLWaRW&U#=Xt_h zE%Z(Yabkn4lSP84@&fi~Zpeu% ztgC`i&sqqdIl!qpzaG1OVEo05JaISJxc`C`wbvsUpx7oUbQ8`+OrA9cdZ`JvA+~4U z%SW|N_Kat9e+l`+4&>m?C(Kmrq+KWDqkwD&!vYW3rL3d?5c$~FVq>&5Z;oQ2>tm&* z=4_&si2H=~*n{}{qs#S873~vRG+L8f^A}lHlBC6Udcd7N4+N_1QP+1sQtuf*&Wwl( zbNuy(Xku;#^R``!2D8^`GPKXpixpiXgx(-8E6_$iouj|sJp;)@r@nr1&X=8<8LXpP z`z^q`XBrvW-pv)XoJ-(o-l2sze(Y^x{+hEFL}?GiS&@~>oXjXuSUAK+GP0_nJ2ovU zMtbnOa;<#1{a2BN>3^+rU7%w6ex1imO(sEpF7ax|)Ac}aM_6Iz*M4M4P6dqVgw;pC z&F|ss5T*jc>+B9iyk@9JdrN?Sz_h#gE@71 z%F`L0s+OrlJOz}SLokO@I$}R;e;KqpX|8!T!AzObFV3;KZtP?Rb=aWjz zBQ>g<$}SADM@zJTfs)OSN{5RC;dN1t^wfnl5@X+J*r(@x64GikUx z=eLS6?bEskRVuY#@1LpTleJKz`JD9c<9z=y9T{L3QT1QGmEz1Xb;!x@vR&!F#g%nz z9AKIYAIJIeAljNrm72J^y8w{kM=vx}*%DL^t~Icx58)Xd#a(?T zDL^B8Z6FP6j76aeKHF`uDb*gH*Q^y;Q2Q?+F~mH6z{PvtREO?&t8(Oatcb~n3bfo^ z7l04~3)X^1oK~%Z{}6Wo61dB*I%5UO#L5b^eX)}+c*d~$kG%8p%>Qvka>P~@Co}X< z!2=rI^#2V2z#P{G8nD(_HHc#Po?53zcJsSJ zZOEwS3U+b$l#{|i!#Z;fD%^N)-{7rD73AWg4yx_NFCRV`F@I?KG;9U2hXDBIfl%Cv04eV_UerxR zdqenZ>!=?t4p|hi9$q&CjNV992x_Nc!w=!XUbIKn1Awm4>oe)hqjV7d3+^&C4*UX9 zj~*XmSX0Jja9$Y9J?wi~tzUN7s-Tp8UNSEN)4KGMFIHOWMcn}l&No_wdZIx%3lvf} zj{vyxO*aQpR@Hs(o>E1C2l2xXFcGE6Cfb8@2;(Sj@b{kB+jIzYa;6e9*$L88`6D6@ z<3JtVW41)Ao1RS=L0v6*6S_doCh#}ae=&TH9v@s#4;06(+#-&OEL{J;r^=+a#Jr_a zySuzHDfoaPbOhH{@cBwO^?3ivrbi@h<%CZwSpJnTIR$N%uNVMs#jR)qf)tCLe0(kC zk6^C)fF^>>zUy^UVaE`n=^sHzi^fnj%@M~QQxg$qFE3;^KxK=3f0j&37Fb)S+H5H_avw?B`_#D<(#&f-~uTsIGQZAA2mWd*W7h&d=Z&R~Rd( zBUo7I6Z(pVVP_%8xEp7nnpR51#j0uko}DDu}_t+M_28&~`t8@6TG zQ^4B9$@{$JPX(Cei`}Ct)!IuQ0!~&(;jXui2;nOIt{?HHOYPb|+jQ_0bY$9hKN|pZ znS||J(=DbyXlj5m^XgA&G0)tDJsNVj@tkB1JJ z0(NJi6#PVFPp?Hrv;ZZ7(}O{5>)JxA0fXJS7msu~T0|brx>{a4clRa-?KzGY#Ucq? zWlD<{phcQR?LdZBy0aa-yFCFC;Q z#e2}V;`tXA0X10BHkzyQ?n#&#aoOL~yu#^$5WkJTG~@=1rg-G*Tl=lz;|` z%&fqB?wjJ!dCM^~wL1f%8IRwJSC01Xv%cKA0o~;ky;wkPpB0G4C|m#HlUl?B7S8fu z;G~M(!20w3+0w+mTb8x!R<&mXMtLs$eaG^0OOxbA;+wv>FyQ~*UV9=H$^1MqMigoG ztgx3gfhO7_;Oyq)t_;sSMXWelz}Pl4nyTqunqlR{dIoYv|M(knlCv=1Ogyr+cOM63 zF>rW$)Z)?LUlmT(`S@aW97g;3g#)K&yDKb@N{4O4Wq)A$M5ckBD|YeA{DI>m-jg$^ zsNpyH_0Ok{nW-;P&7VcaGcSx!#*Mgh!e7+A#5h1lOVWGf#bt?{f~YzczxS;iWFAt4 zBbd}K&Y)t&HC?N4Cv0Ve^LRnGcJOB-JE(90bCh=zFQ^3l8DGSAz05Ak>r}zs#!e#TId}-|^g`4rmSP@W!|kolC!b%z zKAa^!<&5UP`U~^v*<^`2LK~@zYPe9`&mv0w3h+{Nu3WmNag}`&Y!p53;oUE>D33k4 zDKdMFBth~-$;p6f9_Q<#zDu0FX@BRam7hNE=SswYlC!4p{?|J^!q^Z*ow$|pCCkHNhqbhI;(U`( z_S;@RZqc)IviHWW%Ad(=0%ey#TPwfK`wq($EL&rpWOr5>@_rUi%np)V{itgU5+i#u zcOQQq-QoJSKAcy-y7_W)C@4q3Gx={zkcoVL&nnh+bfu7ePh}$#GwyWBk!-%>B65bR z`OJg5-V?#Tc~8enJz+jfqbKY&wyMhlj1djFL2a}vBXR!poQyZ(M;hemJ~87g$J#i~ z{K{Do=)=3Rkkf?@a7Rz|lJ>?W4q>7(w;W2nDd*u=6b#Zeq|Xitak~x9X=Cqwc~`hE zHRcMM^D*<)B}Ln*l^j2pbO-0G9A3GiQQCIQKngKxz(Z+zZ-YIzzl0a0u4(%}xuGvv zghs7idajunEuBk8Z33R8$rP1iXg7@L&0|+dwrP2Y=<<4kPPm#n7SIt~|}K8XhXu1YKk5led?DU-J4&HBjgsJN)o z>Y3)f<@-wlCd8muu$Y*~9XN$x>nLju~L;Y;TmJ!~45;$5|cZ<#6rDb~!{e_d}#+da9$F8tntK z0V$)M)vjNcgjN#1JxgrAALViww?sDvY-iNpe)XO3s`c*3ZpZ#U5&n^DaKAxt%JI-; zEV|*i#8{|@5LHWm08#bRy-F~RNqM+|KN~#=qie4}4h*`nf82uVwd~nryE03fI?xq> z=7t~mE>$+9x(^JiO;hR|hJsg_R~979sJ^Xt-NTw_xCi)<-c}hdemE|$G3;58f_6SX zBd$3X)efbw(a%+|q{ z?0G(aHTFgQr}(=D!8GYdR*$T|#(ue+pxe*(C_DypjBG6x>y;UddiwjsB=>4yBF5!; zhOUYTH48qgt^d|xVpDI7a@UyQ)&ED=m&Zfde*e!x$QF`)$&!7SeXDF^EqnHe8jL;r zGU~~aEo1EADT8ER8aq>zveqEWgh?SZ$j(^4xB5JP{Qmg8US5pbICI_SKG#{^=UnIP zc<@Y9!KBNRC1gzE3=&6fTA~&&XYY9*+!4J3VpF8GcrZ#a|FA8~IPn?#J#!EB(MM}h z@n0Ye_k*f<$%UddzOQ?BM^RS{Ilai%uw*97_89!4U;j~XOk7tG&He4B*T`5xOtWcT zEK{pH=lXX;0d4(JZX>|g*}mBrU9snZyIhL4IKTJhAw8~FM!W<`pVZ#cJX&w^qfB=; zKH~NJ_{T!@wR5bM`>CYP$bZ+ZQ%k=&zog_sCgp9UUxqNOqKCY*mzKQV#OoIt2+vSN zVG_t{f-QX^el@zkwd5!lPt5wAChiiRp{%&1Ze3N+Vl_9a`{pJfH|v+EI_1aA#%m05 zL5!Y!#El47J055rQfjKiy5JL%K3}9_=_xPf*Qb`!OVQ7-PRQppBKM|twvH_~J2)w_ z(z3AGJJF~(nwwI=4$|kSps<+M~lKW!vL` zZ<8ccP3qA3O7YXI(D6k}`rW^GZ)}ub5@S#&)aKC95f#4aWtx{JvkdU^YCcfArjwe% zOk&Qsx_h->Y0YA2`mq|Evq94azlDqS2@Pf6brlf(;sCw`5`jx;D2S8^%uuN&sA$sgoC54Y)_JoPH9c3`!UywhP{d0Y|d!H|HqFcKSt za+ljNvw&bh%m)`Pi9DoM&OX>krOl8??DYw13jeB@I`SQhpEwXQ4Kc8p@4cXv=NsRR~h~}-jyrQJwG+w@{ep3E3 z>Op*FT!u$!vZir&B%T>vq4&CrzU^%3i&1D@M-IzKg6d|aa6NbY!bJSDlfmdNItQ_aPs z_wOd(^s@D*L)i?~lI^bhfdW2bA#zT4AQ=YVI+;86FJ>eLWCT8AH1_%t8^+dx)vCUM zkMAucyw@{y7v0(QNL0o3U{5J;F8<_&34K(np`ab+Z^*4RbEK}ZoF+R}Y&BsXKwCa8`v!Kgj)fi=1 zpXrz6=ICrpYT^-pyOLJI!n_-g+sfN&oYw1CGR(%;o%xq-1nLyE1D1At1#ZNSkK=Bm zh6MvdbbfFih-Ikk6yoPZ5Z%_5gxs1SP=>$nza)w2PBu%T7jXgwn1cn>+vuVU z57;JBA17;n)XsC98!CP>rnpKYNBThu&irRMOvFShsDRbVTJk2i0RN6IIDF=;H!VEF`4)*sFT&dhb%{9amD& z$rI9&_XEyuHu^_M4{^3n?g(w?N(J@b>XWh;*`495$Nh7^^m&+ym*3Bg-oZg$pJ>_- znexLw2Wll7b>ZbCj0d$=d1QF3UocCsfPu$g~w{#PEWd+M#G)`@WKEIBdMa zfKi$2d5IxJH8fn6Pd1Wt%xh_^p?VFUh7%Tkxx+^IZd|vh(94|XQX8|@KUY-CqKb;S zR^=x5{HU88G=EwvkpwtshFi;bsU5#WFA1dj>|xlb;;9tQ?&Te~fHVHXhQ9b8UpbzRpd)&_m_78(coUry#v z?PK_q1k1i@IIn2Sz?E@WM)+vDbjCK9+sEne!t{bC(o*I^>Rnh)ez?V6GS0U*Fzb6F zMQ$iWWU{NxCL~&b$+Pn4GI=}*l+~>%GRoBAx2R8u98=DA9e&fh!ty=QI?z{8D>O4T zSo{H?KxI~$V^^Ia!}Z5hFe+y7b^+qF$~4x3aR7P4Xa<g}M+-8z~Eq9Y^QYeRCo&y_ob|%U=k^)7) znpZ8KgjBEDCZ?QJGKU5h3o(BrjsG%>ItbjioR`M;_vUOy#+4*}`^9&q%TC*FdNs*o z&q6s%J+)CiJJQ?Q(+zj$Jb_&|rRC=rr;vBz%p%h#M06A3of5_1)oQI+VWe`5eTvFc zMcc;Dg2#H{GPB;<68sMeF(t@$ZsWUVr1~r8xXb3*p>el0cPjI@&%++IZ;_o(^anq$ z`Q6PAktaU2P(HJ!9OjxtS=PS1gckPL%5KxEqAp$Mc9v-K#v03%fIpCU8IMY|<^@VG z_6KK`$E70D#R&}MWC>gDyX1@_Y8A{_fqcRruiWXo@vI~&9^z;6aYo%h{$or9-O#XW zkI4(V@4OH?(PFr55ZkuD12pmS?gdLteUNs>v~*t{-lXIQXI@tBlRAz%1vz>ztJ2A> z;Mm&}0Sl8s7YK*jwTX{^lJ@~%aG5jsOZhY7}F`a@T>T|{4SENy0ky3W)r9Ci#l z$8E6^PxqIm_J(Z6RL6>@D}kH22(Uk;iVp&f^!`R0IYyU$dE_d(ABewO$G?w66+ zkn9-;jo@Y0U+7}%MjwiuU9BMexp?-P5v3@S!R>E2wcSJ#JghTT5Qa&BTtt|a)^a3v zFp8wVZk&-Am>4!?gwB%Q**&1~ga6)b z?oQcH+ZQHoy%t~ul|zj|K)>c)oyJP23(2#vJWe4_0cngh?e88DJ#KHMI=uSMryD^Q zB8Xft=A9kOIbOaoN108ke2>tk_D^Ln@p4?u4*J3JcaR~UeL`3FWO3fyDf-aS ztO5ul0D2Du6DwF;a+**iGMBp05dEMp`JE^0KvOa!vml$ZP??j7Qs*_izHZ$eE+Pj^ zed39u2RYX1r}qG@5e(WNJ_tRc-@jaU-v0$@K)9MRiav}*1=-Q%cKd!sWl#t z6G^1KpP2mfyHEP+^;3ONXt2#o6R9a+GJQXeo#;kj1E$PKEp1=i&?c(mzT)_Nv_+Rk zapj5p!}UXlqHqt_0eo{DPLw!;o%68>`EIcWAzHf{xC(8EZkQ87$NDR`{j}V~KYF6H zti(2Xf1h;E&PTHwMqT-G&W550Jlq_u*SMJ#QB0vEWW(F`e2l}iqj;&QvcO_RQ}ykc zS8pU{Dg>x-GaCbR2!mlmxEfk5I#)F0cmO5km{Zq%HopX}&#kE{`2c3tw)@ zW^Q3ktT`f-DpEE8d?-P56sYrO@PC1EiaSme}&(&-24rb-iA zy81li>-V@EAHV?2Y0>-mlF5jENy=wbQ8hofU#=BUwp>z~2C8{UWTp}-Ed+T;9tB|L zY=piXc>>g%F#FnKoe$@MJsrdF%}LlR+=%$E7NC#t0_(IeX$y9#NPGBn8|wNG$xlgj zHfNw~#=bDJ`sfwVcSQ+XTtzB)M9ul@)^Su8a%3F0hOMm{znwDbm^ z%mZ!l%04~NU~+d9hv>a<9Pb&La!SGN$f$&NPk}NBtv#JcmFz1P5a!>ssC2ziFYT`* zIC+zfs>_x}K$7a~?aF3e|5TF8mq+yPn0obM)mi2SE*`32Kvx$*6KGYfD@1*4k}1ZP{yC!b>4Rmr zhC$|=`&@Mh%}LPBn63l}Uj<3+qRLBNAJGTA=XmdmYq^M(R^+I1|4~O4;}N}j{yi?p zb@08c3BknYtGHhEkI54TGGiLBAGcnvZtB5-e3DU%bk;I6PE7z?$-ij1v&h5QL%ff*=cL z(v|01?Z@p(AC>EQGi;ydJ&1GpqHL7UYS+8_y|8JbPRpONu$!?&ymq2&Yh;F+I3~Rd z$OP?wR)*6C!u!g3a5}~upM+CRqWyB1_|IeH?`j6SK$WP{vD$*WO}!WGat!ZIz9N0Y zZFzWIztNby#blyZ>L%p||3n$LRKOhyCK}+rAP%we8#>7uVERT-3Bcqs6Q@Qv=PC=0 z5ZAE-XBd8$>@lk2`o$MJS|?o>n;c!K0GBOqBvlVy@30jKE*XBIGf<(3(*N|1^}2;o zr$~?r=8u7hx3T0-6Dec!`S63Sy##5Fj-tpE?Kx$H4r8Ou5pz+vf6O*znyk_~1s0@Z zXorUHMT>)}fHsh|SM%KD&a3>JV)YW+4yo7uA%oEV3?me#cD2y#J9036vf@D8(q1#o zI)=uT`e@q#eZp_P+cicCS5KsdaJI5usynvIj2^gfEp=~&oQNFiG{rUtvTT$dM$W)a z*PvJ*8w31>d43&11RSSd?wKEeb`E4!IY zi5zEN{tbM#M{A#-6#%hDT?nfP%(afY*FREwX9gd#(-8_DeyS)JelNV+_GvJ6z12@* zf7fN4$UtPH9m$c3sYIREa;x;(I!H?b{>G@VRR|%ooWlI!WfvnGv#Q+575H;6pe|Ip z@<|jhq*r?(TEz^nQF`Qq(qed~|KoCa(@CqLn5jdnq$eNX$J7ruA-cc3FmWHAQMcrJ zF2%I*4v}7z|2?>*vvj#-wXU30kykP$BbxKh1n`@n;&V(V42sf{eJpI_dsh8E^hcom zFprlp?sJYfEnH3#ZQmYH0DSDz{Oq0#+}Ksd6;3prhn!vmdMb8YA#tCpnws}_Px9b$ zk@Pq<`Ef^f!+~I%ojj$GG|n0#KB^ZYB)u}?9c7!bEnDY#J2240Eh`b+i$*yh%#|MN z;$;%~4JFn=E&#w%J^Ksii#z>XKi>l)$`jME23dhy%xw5Z=(+s^50PqnTa`PiHE&eB4Yb9M5A3Yvy(RNh>TG@R)S<@Ly6ZaxY)V_t$dB zIY`h{Os6tI3^C-<-Z-wY^57gxaYAn@kZhz6-_GZBg@Pn@Q<#b6Rj&W82{c9|{T0txC@ z)w3~_-dhX~w@Bfyg1?lJlo7?wyrgVZuLvJlau#)zd)l{WmZNij^hb!1)1}Ah`^GJ(a;rU$WQ2k5jM!%4hz_KpPJ@Fq{tx<##o^Le{1S1e4Zvv-&hqS7<#A?X7?lW4x8!}kgAP3O_5v3aC`2<+pa7G#_Y{>P^O1|P3xy!A#YfYkZ#VGey0J9 zpbS4s4&g>n{ZBPcR;!IB`voRYSORLA&W1wj>Va7hS5%zwK=l-Cu0bS zw`jb)RE=|(r~IRXKk7y4 zl6-Wl#5BSC|BxKS$!j^mOtSg8A)6+)hlPK=2crtV1?q&8@9Y8&8yYyrUVD73uZ1sZ zQ$xhR02BI(k?0b9Sqoma|WrICfS|51S-J{9CXq#*#) zQ{!_lf&EdW9Jkxwe7S6k*p@SfO;^U9RTd!&2U#|hK68`fk6?$K8?P%?EsT>Pp*#3` z%d@x&ki>Xk7oRN1x@x+u;OcQMuH;zwj&3kCQBAi@a?;jk1afb+alVe_0?lWEgmR zPzwlPH#jpYpRdRg+R^fTOxKXF*nDJ!;Iu?O98qb=gde4D=D2Vje=n|)FcyB%ZerKC z^r#y5!OU#Q0Gs}@>Gus{df`}{p`+^WNso#Vl??^rAjOftPX)bByhIC)pvUa`pxU9wgKSR9;2iIA5`?o7 zMR1pT@25_IpfVsfXzYra^xudVc=FLA1}cLfp&|X0kgyU-*i@EUBM8v8)wb%&K;{J$ z52>9h`gHpME)1X*f0WqSzNJp!(1C652P&_27?`a7%<<_qS04FqOIe)C{adSkhI;>r z0Rcz`KqmhCW$gQZ-rm_I-v2`HGs-(H0C@4gk1(p}`wvn$Bcdt&KbPi=G42)=H^4O<2-t)&v{ad*YQ7wU4IMVGh2D9}cP$m$jV=E+GAmo;+LC z|G<`t?r>1c#mNh~T)C|Km6#nwH`TLij{ite4$^2)9aZ|ZsSWXfpWQssEMp^ZKqW0s zj`d}*3Nx9CTy+d}u9-9E^a))-=NY24>Z})b{E2yDDrUf4jotgh^$M5zV}T7hSRZIk ztiS-$4yKycVpp-lzr*z*wFxJd$113E&`OZR1vanHshH;?07#(aTC};zAwaX0j|=Qo zOlbG%o_?`*)AfG%K{km^h#t_;(&DwRk3BOZdVc2WLzse>71{aWX08jHVN_uhEiC(> zjVb_qUs$?QW@Yk~qGNnJ@Q?96taL0D0GJlL7ZW~ffHx{+%~njnbSAZL zD}zPAwvZ8HH5+^fg7MSAZ$_{)gjXVcCg~Q(sTAUvi(POT1bgK5VDFrB%@~kHI|T3# z1~;z{$*3;t%21{apVC=(oU+>T5(qXYwc`slJ2xy%(zw>4+&8uoMH5#2Wmpd}t&%`3 zsW`k4C6F5+@2|R%h%vufp`46yx$q{xGL9wAJC2;lB$fyX0d}%7Ko(WTfdpS!iPN zDrHjpqB^($#yRChah;8>s`KPQc+8`g7Cy5Stx2ez0X{qHB#r9Ir{AaRS8z(b^dOcu zi3f2vgbVMIXDob;4kIq|23$a+uoX4yf%as8AVQz&BXo^XT1EzS-(+2#2?JSO z)y64-$h6ObsUgFaO^reKNRzV+DqRiB&ApLbcU6movn%R;1iGyqS*)q8s!C{cBR6FRuBk= zujVOtRSWl1GzFJA+^pWGNB-(P(OC?y7yf*T3}&*-?#C}Q4&8KE*j)fsdR-DH>Crp|Q0 z4GfUD@=&!A5(zE0rZuvEoLkg*eQGQ>O(Bl@N|Iq){+Z;y`lGhCGsnB_h_`RV##EYv3P?@S9I|x#&Wu>q z+b@jW*tJa#qf34(w6(#yXQ`xD-)QY{p}LF=`0w+%jeXq2vI*(G)u5Er`B8U&(Ucg3 zWvlB{lheCj+_0{I(sB@M=a5yGGrgxkNS<~s8&fSuhu0aHNdkJp>u*F{MvHa1yoS7C zQZU(qJ=IiOyVTrf+#$Yw+1+7e6z^ zSK;^u2JZQ2&)%4scEU@0sKSKkv$iGNT6OPhPQOr|aW3kXf){yG3RTO%n>V{mjGNI* zJi@6jci>PruWPY;k#50nKKPE^Zq!gd)<9QCr}9rGUeT+82YjvU-}1_GcRKgWAVrn~ z;$!?nSvA#;ERfW=@($-J(?AaSgo(J^>wF!>^j;-`qS;d>Uv5*%c|XQ!Hbx5~+|Q-GRvV z@5YQjGM+v+nU z9|NGS%s|XfJ5<235#Jp61Wr!DH)}4lf2#*-`F!~7c1c##;C&3{bXu#}aPlBppc_ZO zaBo|*-mAwpse|Hp=K4bG6mwTWONe(+52RTR~aw z^!yJ6Vukb})#s}8QheC;t5O0XJQB%g2EFrov_igo>2fA!9n|BqJ~|N%gX>>ejx{NA z2LeEc>|+-pdBFTT92cUSC^u~LT_!uP(V3nGF>crY1_p#6Wkv%6X6`S6h)?w`dQm?c zX11Tie;XHO7IvbFs-xYBakhDLe~hfLp*%hJvFp-)3#lH}r8i--9`K*6K9(@sa#q%bF={-zBm><7=S-f2u^lQ5Sq~$;32Zjl5z`-K>@>eT z$jw>N1{aF6%8@$=ZKo+^kV?K?s)uBKV4od~mU&$cSB-l=UcrvC;b)FzxXfI-=724x zt%CTtN22&21Jwh|Jnl)Xdb5JS23isRi|EHTX>J6lNoUW2hP{6^MeDb+J0PcT1`z;X zHOcN$Ai~Y6#}ZeFlk=dAFADA?l^Dg^V5+EZGV|MzckXjk%RXGRg?BI~#?ZXreONhr1Q!jOK;i(oS1(tXUciN=bav?< zIOM=Bp8=oy7q<6)=2X%@2}`zzyV@{s$SGA7qa=-nF5LkiEQKPEcP$Y(gR4KU(4r{k z=6XR6_2ZZQRCJOCeKL3tb|_Il-TM&7_A@u|cMr4AzM`CX`5goi3@g7NXrC>j{|}u& z_CVF*-N`!cJu{dHbMY}vuP92tCZSwm5^!uQnt{OWZvqd^DK5yZKgm0e^#qZx-iPVBz5?cyDyQCJLY){>xN|xSL3|!Cf(cVa zE%r|^>OZMn6}QwjXb2O{Vwq?9;r0J&M;>8R^R}=1Tf%yf1VlRgmee@<*(GnL&?CJR z&ndcnJ2CmGhP>m)q-bFtos{|g3mJmnOo_3$3~WIS(v5rxRtcRG(7#-zn_|<#rSfIB zBx`<-UmvTef4~h(`F9e{{&-VXZfIVGur6D{gS?s;CBP@VsWEsxWxJJOzktLB18jE0 z?KFSG3a&HszCxa=YwZ$Y)ok^|1n5_+FEsA6$cI&Ue&@`4E*$t{;w|h z%Hn3JFy`f7!^#+>__G^SVFIo=89V=pf?}kBm|~3nzhqGrJTb)tPG{bEP>vL6QW_-=S5SJ$bIP#am1hK!qdzJ62qV|efy1AFi)MW43E00gC8`M z$=GiAOJ6kX*zopra0m`aDy^PiPW12wahI_pzgD|2=tHx!I0L@;;1x;pu#Kg{Aq`|E zddjOH#o46EJNd8Bkfx`7)6^U+pB|_?tr0<(=YBbc>)~yXU437tbPI zD8f$7H&bR`u_=Oy4PpK`3-GclaCTWLB*lEnA4J)BtQf90Zuag8S>~lyt>J^O_GaBz)b5->B zzDF9ot3`X%aug{=+}&L7L`ocD46dS-&S}y8ns7i&YC%-4N~Z(@vR#d&5;XbVVxinn^wc?E8MmOiXSvjTu%C{Q}{-I+3KV)xN& zQY4fN5-%9e0_wR{TwgbkoM75@$2mrw$;kAHwr&Ogu!5ih+I`_V+Dx;@x%|t9#2oD0 z%kkSO?C&-{PJb(U@%=?a**&r%Q4s_inR4z+jlMnliAHHFa z*h%%Kp@&ZaxY7>4f{G6<89qXNdD;}_o_Lg^gMWwuTA3RxmAb!5#AFFc$1MK4#2_{H@wm#R3_ofRYApCG|w-Ys-OWcAFqez)i6h&Be?nF#J=C%Zh=Vm|m zZ6$i)6B#WwyK{E`uwVV%m}- zgl(k5X#k`17*TtT!fMmZgHe>tYc6Y>+Mmmx-@g%a3#E^(Ximf=Cygw9>s?x)fwdb= zm)L~3!$z1(JiEk}Tc^MOa8`eFKlbeCK&SmQ;W;+8u;G{+CSlmdp~9-d6XuLpCU8e4 zV!p@qCGLIez<;qVKxzMx7gwzpt7I6 zZ7!SM1%|iqFE62dh4lwCZpDP~$t;CtV=DX6FC&ftJqrCw@r%(>V%Zl~pe#ntl3(zU zAx@ZTF_N2qewRd@%!H-)W3QnYM9T7KE-xR+qh8tj-GMP?MT{7?)g`Zge2W_9hm<5R zTV^_EG__-nBPDPd7WwCVmIV0@e$GSp8S2bk_JG&9(*_5Ue3KK$KN%T`(B7XAC?EXR z;){mUe>V@uHX{`4nCfCk^+5}{-pZ=8aUOFjt`okyd-)c7eTaSy=l-bLcPF*k$9@oK zKi0M%%i}P1U^^9S;Bq>6I0_A`;qtgAT=ss@iEsjQ)BX5MYQ>5Eh$gqX0U;akCOjrt zt(fXWrFsr3AR1!XK6170T>bUo$GDCD-SN03B;(Zz1xA0F3{0So2U%7C6l9pG)(qXk zF8BEpV@Yq`6&moJ&m|)7EhU+JN{|$;M8FfHp|>ol(p~SNA6n?0vdRG-|DYCZtQkaiT^`xE@x)d!zvU8e5d6Fb+eQzyI7cGAq9M}ybu(fBa_v}9I)w>&g-mFIe=YmMD z91CaJ7YO(~D`R*F3b=PT;k55KaxQ9WcQlnAyDm^BGai(vb7Xe>ZufG{4~zO^?u_0b zL0G|;m9K^rAg*I_8m88Ex7J{l=|;}9))>}Uef>h*+jo)v;(dIG(?;%7Gvd|x7v$20 z(ypzTr~%l4(A65jPuOExC)9K*9)GtWieOKD$W8n|(_E`-xDMTNzG2>N8!eni_@uRF8a zi%x1yRBtIMK*JrMvWFFY_$^!10@Z$IH}s-rVrgJRQmFd+*Nmd{53GUOa&PbMUb5b$ zG(!h-2V9Hwqj63-mj+Y|hMM7RddD7(5W3oFt=H<0?K4Rw$ZD@XUQiS|msgR4z#?>f zX|f0g^;8CNLD+ICOe3M|hH9Vd9e=hpF{pg~geeRao-v?UDnf(?I$3GI4|Ds+pobhR zl>3BzeMx~3Yasp5_L<0 znAp-rSOi_Oh1Pl}STZ5?;_;ZuI2mXpNFsY=IiUALLUZJnW1Fki3zv0hGIPn*2s{eM zlKT0aR0<^K79j+h!9wX8bBS3>W}EfO#A+Vmk;cahR%Ckm}X?F1exJ=^zDPaI9y2YeWbCF6U=e2gHDoO(pU(= zePy`>TPq#yYUp$&D?{^5sDX;fjNdnx8ujm<0=!|OG1s+G7b4#1?mLl5_nvf1Iv$H35-oK z689puAli@<2#YO)D&u|yX>0YdaN+2htH%ni7x|AH>m!#72fA2NtFL*6a^lF4tefH+ zb3}9gu!8V=5N)W1C;RV6*$k3_u+*)G3dJHQg)}Oxt)x>sT?WQKB0y$ChTyWinzsFK z&c_7@%lzkmk=d0EJvY7hg`a?lrT+ydWArZ*WMGGq{()lTip`hpIfg`Pro_n-Y;1;o z=ZK-&?5;ugU`)T*{j}_J#Ip(ms+jY-#8-ZkmH#Ep9)&_(v7S*m55t8z4pHj?)zpf(g zqg>``V0=*NEJfpjAkrm*9}GBW7J>Eiy5*xzY`a2O7nmgvA=g>rI&UOtNbUv8d%fsx zW!+E4+z|$iJ01JxecV)aw!@>ae16t7QAM)_+0n*gJ1Kv0>Bl6XxNN}cNh#8x5=Qd! z@Iy6;%ZvQq@o5`*U)KDOuJ`JG0Rvy4)bnvAi) zXX88;1b*{r2LD`#iWnY-eRPt`z>t#Q=eQIrLZKVRL~U$e_NFa_{OPr@Cz+n$mtCqh zlHHU_%Bds|7^d+c`+QE@ZH{T?{oXu(O-ilMGSAY*M1RZCO+z78iUVM@EXM!N(jK+R zsO4DXtNq2}z4~Y7fkK|uh4|%Q%VU4<2}x;?_jkO;etAyh zCV7&RHY0Apx*IzE!ud|b;ATX9*r||o@6x}5-96p?2jhjbzXX%Rg%sAjoid7|q z?=o`0C`(f7nH6tn!^O%W~nt`JXOQ^ySM@^EYz(l`HpG>?v|C{;ONjM zTN=OFdZ5GhqzeuN_ol7nZl-{Y7_JRJf=te5d0OVse7+iV@oO}Xk>gEibH@!$6UPtT zvt`8s(oGW*eJ$xNI-MmVWz2^5Pg_K}?~&uDyLNu&UI}a_u?_4#Mq2mpHdhX)i)Tl3 z-*!%`Nxv)VaS5cQpMndgZ{caF8b3hqtv~90ytdzf0Bu|*Ug-Jt#Tk z9;KE+RLI-f^@--pO^ryPQJcv1llWF z-(~hB-y;7!AgmQ!fFAd}$l*_oS`$GDd;vTLv(Yx~KarsV>X}6b5Zig~nhQ)hJ}@QQ zCJJx;2}$u*KuQw`IPqp@@mkH>g zvkZ~g>$A*~C%?zcuM#AWcYBP)G9T`ADh58i=ktmi9K{_5ZfQv;qyEOvD=z@8rTFNn zBAY);-Fa7{5>eXK+Lec>fQvwVX*zZ*e$Yg|Bs0=e{_omUu%w?#C#C)0f6T8r)9)O8 zTDo=L=v@s6OG+VzhZO$j13t&F*WK`#bL*XZ$QBnGm!UiRs1I4-(>?L(8tf4=D=sUM ziF$7`rmft}5dQ+_v#@Z4%6|1LrqPM0i+_l7%rboxXPb{McpT>B>E)CwP!MXUMyg{!{Mu+5ZRql-ea zU;f;p-p%aahPmyXpw6Mfom<`!(-2sNK;8mEn->bp8=fNMIjZ-~lY0?UbFAZgu0s}k zPOf^c%5D+;nI5M#C?(KSA)*EmmxGS zs4z>o@XB(&8SfHonj5!!1$TslF34(wFI8y7PtTT2!KP>F+3ouaM^}bKB|;^grheFVj_k)??cW;Kl{ZGZKzk4}w}+1va0Hwzv9@~J{11=d`q@CGWWJo@CU-E_ z=LcaS+|YyY&t^wC={+-Qvda#isf&N_5vM*-LmJr9#NEcd5T1bOrv1~C7{$zoZbFkj z_r6TffBHhf&w^QVI_9>8`^BEsnUBe?WO^b!Y(3Z_`1-K-ryw;kdDYJOg=f$c z;Nus61eAh8JYisq6NRmd4(~)_=;L+<2Ffa>f)4L}J`Cjo^2Lm;0y!DU5(Ti*gu*appji;pm@iL^0bw&}HB zX;JFROsYJKlrdi98c!fM-=z6`s>|2w2PU@}#$^nmo$7i`HN9L@fJ7uMi_JA38#Z3o z+y@H6&gd&T{aI$3Q(QXO7@`bQV`)jZLwwuneZpL8k==(@%ry*YajZC%g831&UjI6! z&)B{|R7df?j#keozT+SO@~_57Bqgsa$9W`1F?vI92uFX~ov{peW`tBiwVx`gZ&)o) zB*aO@#i48(j&+vz-UIzgh!(MT!)I;qn+tO?Py-0#X>8;kkBqIgl>bpPGyk@LA=NSu zjWErQ*uKt1z>b4Umo0f;QoleRf_B7KEQ>_lWDFz4pFDYq491+dpjhr?qolabf45V6 zmv`->Y>?me#lXzLb@IX2k-NiFbz9?)pH<&DZudiaX-`V&x1$$-zV3Y z0(kt{0k>Yo|MvHwTr2GaYh(cKD^vY7&soh!#gBwwC`IF>H!Rb}UQZIEgoZ?IRSX+520N-3y(UvpwRcQ` z^ZfEe%;2-J`!T+VW=@aFcR{$k9W_S%-VpRSxFpU%j8)A3`!g1%N+{XFMxp9FP&}X~ z))_A6ukWcfK-WJgA>gKI#L@o%Nh$i~DOX(agGsXhc7I9_Jp<)*OQ7^he&?P)+3+5rf`z(Q$nX?dTiUhwP8-nrlBXva0sPBHbGD=pr&DM} z^?hzxK7RpO0>#-{Ft_u7#Wg-E5CbPr8AJ%2E%Q8-0p%H<*ZR$H0BN}B*Jzqe@0b0|sE!D`2qOVz^E!mm)f{z(aG8fT4snUGHP9@Gxv0!@Ir z2L}5D-DrsPxQ^GPMx0*Wfvau`c$QkydPLaY-P8GYA1A#JjpQi%>f=ETIHy*^V`TG0 zq&=!Zem;;`c6bmarcms?3N#r;kQAu=q5i&}vl?Szz^W{?<%#?|Nr@`#`vS7}a;^+O zsG4>TL^r}+-(azp0`n@wPlHiq0Y^hXDaG_G6*pDYv`UD9ZVF`#)a1T*>}6r<0Dga~ z1hSo~!um`1!p|XxaMw@B8@j*fq8`QR=!+~(6x+CdXTinFU0{cYUlvF^m-b;0_9#^X zcq}_yPzDRW#6%k0=~#0pIOmoPOBIceMY_z`7a-lWx--|fZ>2>1@^V!aR%rc82BR82 zpUjEAiO2l#QEAa|>Arh@gS`%zLF3`At4wuoal+nj!V^R+|W9kr^rIPpZyf;aJM>8k?s-ncKg(wz+(v}?~@Kpn9lihEeltjx4yF^wj; zGim4aJ;#mFdYad)3+NPU8ZOH~`IK61J`iteqVl~(7w??$Rd3LZXz0n|l^3DA$C#ot0%1%s_8_oSuzoTjb>C(L|j2|jgkXHp{6CUD@ySBpE2QAR;?L)46 zEb5^Vmvnp2I#{SN$+^sI`ZJaKEIva=(W>8F>|LZ3bOXCe3WvMP9HlfoaiupZO}hGX zX5}1Gf_?I2qcCyb&{J#1=K_=Wtxfr(D+;kXA9rLQTYL%Pacv%q>Z%@ji`Fq!xcB%> zx$T*7itWTDwiLu2Q8rt?f@zqCzvOdBcgf1*x_TJ(u%QiXwDHZJxQ@f4#aU{h`rz`r zi@vW%a*&wGdDLchY{}!ceUMNPfH zLSxvuu!}0n3biEpS!T3QcIB-=XARSj1b-%f%VD=u`Kv^JK+k{k1jPIB=^sl(-+W6@ z6a2>{DU#*R{tP_7h~b7@ln{Mp;w0VVi!M#9rCA1VY9oK0OLSz6r?V3GcR+Ma8G#$Z zOxf!W(nRIPF_)Q|TlPOI%F1J8=-KdtXgQolMe+ElNDk_JoG=HU**bip0zf{02Eo9s zsasGkIIoZwZj#;?L)X%J5+0adJF|V={aIf^05`BgxM$XR*vgG#tI`+Q-kQ{tEOyYA zLW?h~MimoMVjgbbkdK3wE;}$Iz%ht-{0301_E_A)+&kz42pb1;L$AaWV^$tjbPnx+`{2)W;K&1GZ`H3rp*+)cTa zaTjtM-`)AF^;zq?*7r~N?jQCKd+)XP+VA&%zh2MR7rZ+hWlD+mESw!t?3!(pwRq>H3$GToaX>u(xu3h4RM&;F(=__B?g4G%he z=`(aA&Tcr!{ly`ffc!FpG@b=1XzxjGO1z&E`0hQEqjJ!Ej&nJ-vw$u_3Q5!dFr7zm z?FaXjP8k?{(xqQjA}WHkdBq`pkk}(*#^@3v3e|F4r8wl`(={nYMW5C47`lfr|Lvnd zS%N>_V9*qYU+U*mpd*>Q^~vahus_7#buwqnKaEyo>!@|YYB&JAVz9ExG7vurbIhMc zrH_3J*VMAbp>FW_YcMi5i~Kv%gcgRzL~Z4N#-WGbtlk*it0?p@J_$n}jt;Lf)>!w` zN~_WV`elURv+Z|Nz)O5?mcwB;O&04#)Y|$8uUQ7A>J6>%4zc(UL`dSPC40bILw|j= zv9+fAzTw&{cTVk0TopLHR}gv<5%|3$v%aKqGN8Zx5$%E*;|u*99c?ICI#X1RHIenU zL+wcoL7huJ1g!5wUPcVr?C$Bp!iuN|R0VW;60nl&+KERZ9@QAKp=>$kk=v8g?+Y@x zV?x?LY?Q+pidok9*-)WrH%aTPrMSO$cD7VwDget5%n%*e7f%A_bW8F(`jzjDuX8NQ z`@?WKRqWJMbEf>kxjw)p_Fe|;Df;7EKLX*W7s~%rtOjiMA-NOw^#|E`H)1rqx@asv}L|;TTmiT-*q!Q2P{3*fBh`3(Uf6F)p*L`2?1XK z_1l$L?j0S{M&vBM4~%7YZkEsu=xJu|@e-tV8&SA*z>nQPHm?x1H&b5Hs@QQg+Hmu? zl@3-*)#mj=H@|n0Txllr_PvcEe!9oTSRsw*i}JrxtXdsmh;+)DyDwChmUj3=4lU6P zux2&s83R!?=jFg%R;DQQEa>8AqLDzf9wH@)oIBm%L&#{9YZP>TLUGEQY1mKG)PPMK zF1xfHqyN0=d=a-bVB9A*2JbeIpzf5BJ$4FO4JvqpX}FNC+RqAb!QXX7sqjbB*81Ol zq=(R&_APq7Uwk@Lt?V_sc-l|`JulYaU_rr$TY_b@C;A0c91dF4s}A!T|5f-I#6fHT z#iwGPN53{QD)?&BbA~P|TcLAx|GArIDEo3etHlUb501SwoTpaL)wDYFkjb*-><&mO zp@%h_)w}OVT0Ck3Rlya9-lzAA(_5&Zzwttc(BeGWB_TcJ2#Fw}6*mkUKR&XAW`|)e zy;%MPVOYfz;&tqT6EeqrYQ(Gf0dsSvd9EWZTix9pP8)%*`syen9-Z|Ps)DTS?;zpe z7f?Z6uUSv}E*Rz!hyC(6z31>~&f3;JqdXn&h8AC6P4>|qtnHxcJsxmfyfQ@R!^-Rci|hzWI`Zs zK*mEqBk+w+Gh-fJa7y+1H11A=*2W4{M< z+)aPtsiOGX52XT8=DzGm>s`@r!~Bhv{Bh|4y)5s-n-xxB7p0)eU_r0hI%u^34#$w! zSSL02`p7Vo(rR5s&F7ybKP5?MtluR|UfWI1FU$RDiRTieX9v?KHTmpe9#H1F7h0r5 z(tVB*XK`_`;Lzj}FE^`ZWbOwLepD|Oa87V0zl>1Zi#||Kx^Lgj8@oh8tnsF}ZV&%!K7?00Q8-jpf_jaFGfZ8)i|!m~JX{ zQMWzwE<6PSc!``lBu~=O(_eMydGuDQSK%^WoAEIJD?MCP-{U$CB;GaN7FvB4*VFB~ z(I&x*lz@8Mtv$Qt`nwEN(AIGh`Jvp3pu?|wV(1|yaT>{7{t7%W0v}uXA?Gk9)%~EO0lO(HV!%yZ6UA70a^GETUDh`nDy-oWHl9k>iA78NVZ`!x3AHYTb}{NW39p9Y_Y` z$HQsc--To9i=x})=fjpMn#wgQg@DHqkK<_r?a9v=FBZ|gE3X?X87J3wcU zeV!eW?eA$AisorlM2GbX;uZ#64UN{U3+i}}Bt&2tFxjX|`7 z6rDe6k|+|>t-V;@OTC7{N;uOuShpP&$f^~F@9D8oC(BRQK<0Li$25~66XP!#>ghEf z|5)x4Puzx&%7Mt_^YBrviO*c(0CJc2LQet|+G8i$5-=*H?J#Ex6x-6s_8r40qH9He zS>P9^2Bs-FQs0TJzq)wMwI7$*iQ{B3a2sOGzT9TPy+z8 zq@T7*Px(C%<`ICMz`G@l6dH;!BG{f<5*BB-K`vNiOvhD0s6(NzH z0hT-|b?53FjztjZUi*VXTILLm&=xC*)Le>eu&z)&#?wGChv6_Uo}s0r2UGB^_xPpS zKPR5{s06UjDvkJ7&GmJlDPs;k*@x5^LT<^WG%3xDPRzjH!Q!%LlSHkq>|s+zFg?FF z5eS_%V6h?#X2oM4xY*bHxVQssSe`Sv(*adrVV(SM2$fD|n=nNUL~+ z0b(ot;mzNHU-iES2bkzXTphsy{VaGT)&QMeEEV+V1=2xYGR7x!!~bv#t;S$~U@eeZ zwtHTuJ;}3wp_y>=Cm3**f^OWZDCH`r20=5%1if8jJ+ppl9qc*cXnCb_b3k%Y|0yf; zQd8=nfjDUPE$gfdp^Nt_c)7@-#-}ezNHHSV8ZYb^7W%uR`whsRc?j~K+=a$Nq_3SzXsnvHPYWIo;Fl)>$~nXw6r{Rh# zo8d%c8A?VMN43v4Mk5gycxL%FzXx$s=+e+t?raA+4FN-15`8AyJDtHQzdi zs^u^~NIVD?rFDm_n_UCDt3MchskHYP9Z%biAnXca#O~drQqtlckQJ3ijrxkH)M#~> zFqm7~xBhlyOCRfp6`cw9vSgrMy6afA3in=&Bq><87t)>>qWD^6*ljupSnIQ~iMs zr6Gx07vp&RMOaqFW~-Zn;s6@FUBibv4m5A^KO;q$3DT8u&U1?* zMaG4Ijr_--DD)-)T$8fnh+4S1%^!Yz#%1V<(k@FU&meO@nG(N*Q~deofud5>{AY+G z&r+en?(QV3rPO4%0d&zu4g<1&Zjc!*G*sU=)j&<$d6yh3|6C<8aOts8%QNta1#TD5 zE{`9t;Dx^-GdLA@cNcGiZzdyYpvoiq4i3UOfvY#35LDer* z39JasjQ;4r+z@`fbN&G6+gd(06_os16ldBYNYA1XIcmwZhjD^kFuDmfDoZn>(#c@Z zj;fFq4sH;B+<%#Z7x)!*#}p&0#wK)sh+s})Cxm-1@Pzp+<)o0uNrAR)G{Tn3$~~4r z1%dS%{!YThy6}+lpg$+h0UukkdHwh|2amu6)}CMDRC)Eo!f2TZ_~S{Sb~lZ9J%8VT zeu30*t7%#R19}sm;3f(U1a0=P!c%_SP(UN(e=VRLJNyI`3IW(}2EaYt4q1a#U&c*P zYZjseF)(^K4~h}NtXa?_T~zAfROV6nqXluqJwQE#xU97Uz0K{JJLipyy+De&E4T?ypcHW_?rn1(G zgh0>8Dqvdg;}i)K!2 z&BQNz&qDk>S`s;-MuTo)YOwmkLnnEhbPtosb#S7RM*MhJr^_1U*tH zZY&21tURL4nKA09j16jwh)uoJp8LCP?Wd0X`&uxMFUFh0IfoYpu88WQ#7MsC z&qV!!{wOW`URIe~rWm)oR?;!I?M7B)WOp)Up-wjE1T1Wrn6~7#sDCt0D_#T~?x|nY zNh~W^=M`%>!ZnRzD2G|PlUk1Tsg)k@`QkcW4(kwdE^FiF(;{lD{A5f^rzc?>8~0Qd z_j9KkAAw%BpA|{@VF1jj&c8RZG-q&W+vR}a1ClTWasVNTR)EPJ0;n8SH>qQMXt!9n zq^{Rvm@Jt}a|-jRwU23SZgX>SPfa0*BGz~CQ)Fi$CmlcneAoQ*+ZiApPxd_hwT%4p z7V3mFw8$QVyDXskiZZod))pYrA`qc~xKMgco@-vh$oXSX zPfovdox@iLm`66*xnZE<=ARpp{;g!^s!%n=az1tBFN$Pv;$Z0MuUgQXLMv^N+@Cv< z%Df$k?xu`qP<8#Jjd+9-S_>?GpKSe951?L!9v>~xcJ6)($8re3}=K@}vb9Zu%}!fhnbmx)Pq zY`;f654A*8&2cZ%wsVrJ-Q1UbmmJ4F)v08Hy8w{c`ZRq6NC+imke-qf;L4JH zc?vesF8F}aD|>0QCrSmH?U&#{elYy9@{pQTqiBRZ9%+{{4jDtd{n-!x{7NfEVePUV@ckE$8x z0xa^j@-G8XUj63F8&-6u@;4(LMVo4kM^wmaJAM{nnDtHhICLHCXX|)zPqs1S5#v!g zQg9F%0>qVt9ehB#aQt5?A@mZb68O>xktO}}8&x;B3V9WrdbIxmkd|o=x&y$C3#JSo zp!QY?3acB>3xcVbhweK=(?_H@rFf1>IAN+zU zlS$;8^qnhNsO + + + + + + + + + image/svg+xml + + + + + + + Orders + Metric + Periodicbilling + One-timeservice + Pricingperiod + No pricingperiod + Periodicbilling + One-timeservice + Pricingperiod + No pricingperiod + Pricingperiod + No pricingperiod + Pricingperiod + No pricingperiod + Mail accountsConcurrent (changes)Compensate on prepay + DomainsRegister or renew eventsCompensate on prepay + PlansAlways one order + CMS installationRegister or renew events + Traffic consumptionMetric period lookupPrepay and != billing_period NotImplemented + Mailbox sizeConcurrent (changes) + JobsLast known metric + NotImplement + + + + + + + + + + + + + + diff --git a/orchestra/apps/services/templates/admin/services/service/help.html b/orchestra/apps/services/templates/admin/services/service/help.html new file mode 100644 index 00000000..069252b9 --- /dev/null +++ b/orchestra/apps/services/templates/admin/services/service/help.html @@ -0,0 +1,14 @@ +{% extends "admin/orchestra/generic_confirmation.html" %} +{% load i18n l10n %} +{% load url from future %} +{% load admin_urls static utils %} + + +{% block content %} +
+
+ Enjoy my friend. + +
+
+{% endblock %} diff --git a/orchestra/apps/services/tests/functional_tests/test_domain.py b/orchestra/apps/services/tests/functional_tests/test_domain.py index 27fcaa08..5d2e3bc1 100644 --- a/orchestra/apps/services/tests/functional_tests/test_domain.py +++ b/orchestra/apps/services/tests/functional_tests/test_domain.py @@ -41,7 +41,7 @@ class DomainBillingTest(BaseBillingTest): return account.miscellaneous.create(service=domain_service, description=domain_name) def test_domain(self): - service = self.create_domain_service() + self.create_domain_service() account = self.create_account() self.create_domain(account=account) bills = account.orders.bill() @@ -69,7 +69,7 @@ class DomainBillingTest(BaseBillingTest): self.assertEqual(56, bills[0].get_total()) def test_domain_proforma(self): - service = self.create_domain_service() + self.create_domain_service() account = self.create_account() self.create_domain(account=account) bills = account.orders.bill(proforma=True, new_open=True) @@ -97,7 +97,7 @@ class DomainBillingTest(BaseBillingTest): self.assertEqual(56, bills[0].get_total()) def test_domain_cumulative(self): - service = self.create_domain_service() + self.create_domain_service() account = self.create_account() self.create_domain(account=account) bills = account.orders.bill(proforma=True) @@ -110,7 +110,7 @@ class DomainBillingTest(BaseBillingTest): self.assertEqual(30, bills[0].get_total()) def test_domain_new_open(self): - service = self.create_domain_service() + self.create_domain_service() account = self.create_account() self.create_domain(account=account) bills = account.orders.bill(new_open=True) diff --git a/orchestra/apps/services/tests/functional_tests/test_ftp.py b/orchestra/apps/services/tests/functional_tests/test_ftp.py index 3567fb36..398d3e14 100644 --- a/orchestra/apps/services/tests/functional_tests/test_ftp.py +++ b/orchestra/apps/services/tests/functional_tests/test_ftp.py @@ -43,14 +43,14 @@ class FTPBillingTest(BaseBillingTest): def test_ftp_account_1_year_fiexed(self): service = self.create_ftp_service() - user = self.create_ftp() + self.create_ftp() bp = timezone.now().date() + relativedelta(years=1) bills = service.orders.bill(billing_point=bp, fixed_point=True) self.assertEqual(10, bills[0].get_total()) def test_ftp_account_2_year_fiexed(self): service = self.create_ftp_service() - user = self.create_ftp() + self.create_ftp() bp = timezone.now().date() + relativedelta(years=2) bills = service.orders.bill(billing_point=bp, fixed_point=True) self.assertEqual(20, bills[0].get_total()) @@ -79,7 +79,7 @@ class FTPBillingTest(BaseBillingTest): def test_ftp_account_with_compensation(self): account = self.create_account() - service = self.create_ftp_service() + self.create_ftp_service() user = self.create_ftp(account=account) first_bp = timezone.now().date() + relativedelta(years=2) bills = account.orders.bill(billing_point=first_bp, fixed_point=True) diff --git a/orchestra/apps/services/tests/functional_tests/test_job.py b/orchestra/apps/services/tests/functional_tests/test_job.py index 6d29b5e1..b5b9f57e 100644 --- a/orchestra/apps/services/tests/functional_tests/test_job.py +++ b/orchestra/apps/services/tests/functional_tests/test_job.py @@ -39,13 +39,13 @@ class JobBillingTest(BaseBillingTest): return account.miscellaneous.create(service=service, description=description, amount=amount) def test_job(self): - service = self.create_job_service() + self.create_job_service() account = self.create_account() - job = self.create_job(5, account=account) + self.create_job(5, account=account) bill = account.orders.bill()[0] self.assertEqual(5*20, bill.get_total()) - job = self.create_job(100, account=account) + self.create_job(100, account=account) bill = account.orders.bill(new_open=True)[0] self.assertEqual(100*15, bill.get_total()) diff --git a/orchestra/apps/services/tests/functional_tests/test_mailbox.py b/orchestra/apps/services/tests/functional_tests/test_mailbox.py index 141fb3a7..8ee1cb82 100644 --- a/orchestra/apps/services/tests/functional_tests/test_mailbox.py +++ b/orchestra/apps/services/tests/functional_tests/test_mailbox.py @@ -4,7 +4,7 @@ from django.utils import timezone from freezegun import freeze_time from orchestra.apps.mails.models import Mailbox -from orchestra.apps.resources.models import Resource, ResourceData, MonitorData +from orchestra.apps.resources.models import Resource, ResourceData from orchestra.utils.tests import random_ascii from ...models import Service, Plan @@ -62,7 +62,7 @@ class MailboxBillingTest(BaseBillingTest): verbose_name='Mailbox disk', unit='GB', scale=10**9, - ondemand=False, + on_demand=False, monitors='MaildirDisk', ) return self.resource diff --git a/orchestra/apps/services/tests/functional_tests/test_plan.py b/orchestra/apps/services/tests/functional_tests/test_plan.py index c0576030..bc7ab566 100644 --- a/orchestra/apps/services/tests/functional_tests/test_plan.py +++ b/orchestra/apps/services/tests/functional_tests/test_plan.py @@ -42,7 +42,7 @@ class PlanBillingTest(BaseBillingTest): def test_plan(self): account = self.create_account() - service = self.create_plan_service() + self.create_plan_service() self.create_plan(account=account) bill = account.orders.bill().pop() self.assertEqual(bill.FEE, bill.type) diff --git a/orchestra/apps/services/tests/functional_tests/test_traffic.py b/orchestra/apps/services/tests/functional_tests/test_traffic.py index d19f7dee..0a2df271 100644 --- a/orchestra/apps/services/tests/functional_tests/test_traffic.py +++ b/orchestra/apps/services/tests/functional_tests/test_traffic.py @@ -1,5 +1,3 @@ -import datetime - from dateutil.relativedelta import relativedelta from django.contrib.contenttypes.models import ContentType from django.utils import timezone @@ -46,26 +44,23 @@ class BaseTrafficBillingTest(BaseBillingTest): verbose_name='Account Traffic', unit='GB', scale=10**9, - ondemand=True, + on_demand=True, monitors='FTPTraffic', ) return self.resource def report_traffic(self, account, value): - ct = ContentType.objects.get_for_model(Account) - object_id = account.pk - MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, - value=value, date=timezone.now()) + MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value) data = ResourceData.get_or_create(account, self.resource) data.update() class TrafficBillingTest(BaseTrafficBillingTest): def test_traffic(self): - service = self.create_traffic_service() - resource = self.create_traffic_resource() + self.create_traffic_service() + self.create_traffic_resource() account = self.create_account() - now = timezone.now().date() + now = timezone.now() self.report_traffic(account, 10**9) bill = account.orders.bill(commit=False)[0] @@ -82,8 +77,8 @@ class TrafficBillingTest(BaseTrafficBillingTest): self.assertEqual((90-10)*10, bill.get_total()) def test_multiple_traffics(self): - service = self.create_traffic_service() - resource = self.create_traffic_resource() + self.create_traffic_service() + self.create_traffic_resource() account1 = self.create_account() account2 = self.create_account() self.report_traffic(account1, 10**10) @@ -129,13 +124,13 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest): return account.miscellaneous.create(service=service, description=name, amount=amount) def test_traffic_prepay(self): - service = self.create_traffic_service() - prepay_service = self.create_prepay_service() + self.create_traffic_service() + self.create_prepay_service() account = self.create_account() self.create_traffic_resource() now = timezone.now() - prepay = self.create_prepay(10, account=account) + self.create_prepay(10, account=account) bill = account.orders.bill(proforma=True)[0] self.assertEqual(10*50, bill.get_total()) diff --git a/orchestra/apps/services/tests/test_handler.py b/orchestra/apps/services/tests/test_handler.py index ea657f74..8628e1b8 100644 --- a/orchestra/apps/services/tests/test_handler.py +++ b/orchestra/apps/services/tests/test_handler.py @@ -1,17 +1,15 @@ import datetime import decimal -import sys -from dateutil import relativedelta from django.contrib.contenttypes.models import ContentType from django.utils import timezone from orchestra.apps.accounts.models import Account from orchestra.apps.users.models import User -from orchestra.utils.tests import BaseTestCase, random_ascii +from orchestra.utils.tests import BaseTestCase -from .. import settings, helpers -from ..models import Service, Plan, Rate +from .. import helpers +from ..models import Service, Plan class Order(object): diff --git a/orchestra/apps/users/admin.py b/orchestra/apps/users/admin.py index e0737eab..6451d0af 100644 --- a/orchestra/apps/users/admin.py +++ b/orchestra/apps/users/admin.py @@ -55,7 +55,6 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin): def get_urls(self): """ Returns the additional urls for the change view links """ urls = super(UserAdmin, self).get_urls() - admin_site = self.admin_site opts = self.model._meta new_urls = patterns("") for role in self.roles: diff --git a/orchestra/apps/users/api.py b/orchestra/apps/users/api.py index 80cc841f..dda373e5 100644 --- a/orchestra/apps/users/api.py +++ b/orchestra/apps/users/api.py @@ -1,8 +1,5 @@ from django.contrib.auth import get_user_model from rest_framework import viewsets -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response from orchestra.api import router, SetPasswordApiMixin from orchestra.apps.accounts.api import AccountApiMixin diff --git a/orchestra/apps/users/models.py b/orchestra/apps/users/models.py index ea301ae8..b91b72aa 100644 --- a/orchestra/apps/users/models.py +++ b/orchestra/apps/users/models.py @@ -1,5 +1,6 @@ from django.contrib.auth import models as auth from django.core import validators +from django.core.mail import send_mail from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/users/roles/__init__.py b/orchestra/apps/users/roles/__init__.py index 0afb6556..62b9ee6f 100644 --- a/orchestra/apps/users/roles/__init__.py +++ b/orchestra/apps/users/roles/__init__.py @@ -1,5 +1,3 @@ -from django.db import models - from ..models import User diff --git a/orchestra/apps/users/roles/admin.py b/orchestra/apps/users/roles/admin.py index 4d3711c5..0432a682 100644 --- a/orchestra/apps/users/roles/admin.py +++ b/orchestra/apps/users/roles/admin.py @@ -1,7 +1,6 @@ from django.contrib import messages from django.contrib.admin.util import unquote, get_deleted_objects from django.contrib.admin.templatetags.admin_urls import add_preserved_filters -from django.core.urlresolvers import reverse from django.db import router from django.http import Http404, HttpResponseRedirect from django.template.response import TemplateResponse diff --git a/orchestra/apps/users/roles/jabber/admin.py b/orchestra/apps/users/roles/jabber/admin.py index 8d5590f5..ba126c0f 100644 --- a/orchestra/apps/users/roles/jabber/admin.py +++ b/orchestra/apps/users/roles/jabber/admin.py @@ -1,6 +1,4 @@ -from django.contrib import admin from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import insertattr from orchestra.apps.users.roles.admin import RoleAdmin diff --git a/orchestra/apps/users/roles/mail/admin.py b/orchestra/apps/users/roles/mail/admin.py index d6ad91cb..58d3fa56 100644 --- a/orchestra/apps/users/roles/mail/admin.py +++ b/orchestra/apps/users/roles/mail/admin.py @@ -1,15 +1,12 @@ from django import forms from django.contrib import admin from django.contrib.auth import get_user_model -from django.contrib.auth.admin import UserAdmin from django.core.urlresolvers import reverse -from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import insertattr, admin_link from orchestra.apps.accounts.admin import SelectAccountAdminMixin -from orchestra.apps.domains.forms import DomainIterator from orchestra.apps.users.roles.admin import RoleAdmin from .forms import MailRoleAdminForm diff --git a/orchestra/apps/users/roles/mail/backends.py b/orchestra/apps/users/roles/mail/backends.py index c1283d22..b29d03ae 100644 --- a/orchestra/apps/users/roles/mail/backends.py +++ b/orchestra/apps/users/roles/mail/backends.py @@ -7,6 +7,7 @@ from orchestra.apps.orchestration import ServiceController from orchestra.apps.resources import ServiceMonitor from . import settings +from .models import Address class MailSystemUserBackend(ServiceController): @@ -152,7 +153,7 @@ class MaildirDisk(ServiceMonitor): ) def get_context(self, mailbox): - context = MailSystemUserBackend().get_context(site) + context = MailSystemUserBackend().get_context(mailbox) context['home'] = settings.EMAILS_HOME % context context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize') context['object_id'] = mailbox.pk diff --git a/orchestra/apps/users/roles/mail/models.py b/orchestra/apps/users/roles/mail/models.py index bd03a807..35d28355 100644 --- a/orchestra/apps/users/roles/mail/models.py +++ b/orchestra/apps/users/roles/mail/models.py @@ -1,7 +1,3 @@ -import re - -from django.contrib.auth.hashers import check_password, make_password -from django.core.validators import RegexValidator from django.db import models from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/users/roles/mail/validators.py b/orchestra/apps/users/roles/mail/validators.py index 55d241a4..eab400fa 100644 --- a/orchestra/apps/users/roles/mail/validators.py +++ b/orchestra/apps/users/roles/mail/validators.py @@ -44,7 +44,6 @@ def validate_forward(value): def validate_sieve(value): - from .models import Mailbox sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest() path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name) with open(path, 'wb') as f: diff --git a/orchestra/apps/users/roles/posix/admin.py b/orchestra/apps/users/roles/posix/admin.py index 45bbae6c..0ef5af77 100644 --- a/orchestra/apps/users/roles/posix/admin.py +++ b/orchestra/apps/users/roles/posix/admin.py @@ -1,6 +1,4 @@ -from django.contrib import admin from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import insertattr from orchestra.apps.users.roles.admin import RoleAdmin diff --git a/orchestra/apps/vps/admin.py b/orchestra/apps/vps/admin.py index 236b82c8..dee36778 100644 --- a/orchestra/apps/vps/admin.py +++ b/orchestra/apps/vps/admin.py @@ -1,7 +1,6 @@ from django.conf.urls import patterns from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin diff --git a/orchestra/apps/vps/backends.py b/orchestra/apps/vps/backends.py index 76f72611..dd70da2b 100644 --- a/orchestra/apps/vps/backends.py +++ b/orchestra/apps/vps/backends.py @@ -1,5 +1,3 @@ -from django.utils.translation import ugettext_lazy as _ - from orchestra.apps.resources import ServiceMonitor diff --git a/orchestra/apps/vps/forms.py b/orchestra/apps/vps/forms.py index 22ed7574..a6f12aab 100644 --- a/orchestra/apps/vps/forms.py +++ b/orchestra/apps/vps/forms.py @@ -1,5 +1,5 @@ from django import forms -from django.contrib.auth.forms import UserCreationForm, ReadOnlyPasswordHashField +from django.contrib.auth.forms import ReadOnlyPasswordHashField from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index ae37b49c..d08ab230 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -1,6 +1,5 @@ from django import forms from django.contrib import admin -from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin diff --git a/orchestra/apps/webapps/api.py b/orchestra/apps/webapps/api.py index cf938e00..97b2aa06 100644 --- a/orchestra/apps/webapps/api.py +++ b/orchestra/apps/webapps/api.py @@ -1,5 +1,4 @@ from rest_framework import viewsets -from rest_framework.response import Response from orchestra.api import router from orchestra.apps.accounts.api import AccountApiMixin diff --git a/orchestra/apps/webapps/backends/dokuwikimu.py b/orchestra/apps/webapps/backends/dokuwikimu.py index 98050221..54abdb10 100644 --- a/orchestra/apps/webapps/backends/dokuwikimu.py +++ b/orchestra/apps/webapps/backends/dokuwikimu.py @@ -1,3 +1,5 @@ +import os + from django.utils.translation import ugettext_lazy as _ from orchestra.apps.orchestration import ServiceController @@ -21,7 +23,7 @@ class DokuWikiMuBackend(WebAppServiceMixin, ServiceController): self.append("rm -fr %(app_path)s" % context) def get_context(self, webapp): - context = super(DokuwikiMuBackend, self).get_context(webapp) + context = super(DokuWikiMuBackend, self).get_context(webapp) context.update({ 'template': settings.WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH, 'app_path': os.path.join(settings.WEBAPPS_DOKUWIKIMU_FARM_PATH, webapp.name) diff --git a/orchestra/apps/webapps/backends/wordpressmu.py b/orchestra/apps/webapps/backends/wordpressmu.py index 88eadc72..83189d3b 100644 --- a/orchestra/apps/webapps/backends/wordpressmu.py +++ b/orchestra/apps/webapps/backends/wordpressmu.py @@ -1,5 +1,4 @@ import re -import sys import requests from django.utils.translation import ugettext_lazy as _ @@ -55,6 +54,7 @@ class WordpressMuBackend(WebAppServiceMixin, ServiceController): 'blog[email]': email, '_wpnonce_add-blog': wpnonce, } + # TODO validate response response = session.post(url, data=data) def delete_blog(self, webapp, server): diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 7bd474a1..350da590 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -1,13 +1,10 @@ from django import forms from django.contrib import admin -from django.core.urlresolvers import reverse -from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin -from orchestra.apps.accounts.widgets import account_related_field_widget_factory from .models import Content, Website, WebsiteOption diff --git a/orchestra/apps/websites/api.py b/orchestra/apps/websites/api.py index 090076c7..f419e38f 100644 --- a/orchestra/apps/websites/api.py +++ b/orchestra/apps/websites/api.py @@ -1,5 +1,4 @@ from rest_framework import viewsets -from rest_framework.response import Response from orchestra.api import router from orchestra.apps.accounts.api import AccountApiMixin diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 9fd7303b..eb252d5f 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -1,5 +1,6 @@ import textwrap import os +import re from django.template import Template, Context from django.utils import timezone @@ -123,6 +124,7 @@ class Apache2Backend(ServiceController): def get_protections(self, site): protections = "" __, regex = settings.WEBSITES_OPTIONS['directory_protection'] + context = self.get_context(site) for protection in site.options.filter(name='directory_protection'): path, name, passwd = re.match(regex, protection.value).groups() path = os.path.join(context['root'], path) diff --git a/orchestra/apps/websites/backends/webalizer.py b/orchestra/apps/websites/backends/webalizer.py index 75fcc9cb..51e0ba66 100644 --- a/orchestra/apps/websites/backends/webalizer.py +++ b/orchestra/apps/websites/backends/webalizer.py @@ -1,5 +1,4 @@ import os -from functools import partial from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 3531af96..ec753252 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -45,7 +45,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'orchestra.core.cache.RequestCacheMiddleware', + 'orchestra.core.caches.RequestCacheMiddleware', 'orchestra.apps.orchestration.middlewares.OperationsMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', diff --git a/orchestra/management/commands/postupgradeorchestra.py b/orchestra/management/commands/postupgradeorchestra.py index 2d6b44de..ce0bc5a3 100644 --- a/orchestra/management/commands/postupgradeorchestra.py +++ b/orchestra/management/commands/postupgradeorchestra.py @@ -4,7 +4,6 @@ from optparse import make_option from django.core.management.base import BaseCommand -from orchestra.utils.apps import is_installed from orchestra.utils.paths import get_site_root from orchestra.utils.system import run, check_root diff --git a/orchestra/management/commands/setuppostfix.py b/orchestra/management/commands/setuppostfix.py index 78c15812..bf159bba 100644 --- a/orchestra/management/commands/setuppostfix.py +++ b/orchestra/management/commands/setuppostfix.py @@ -1,5 +1,4 @@ import os -import sys from optparse import make_option diff --git a/orchestra/management/commands/staticcheck.py b/orchestra/management/commands/staticcheck.py new file mode 100644 index 00000000..b2f982f8 --- /dev/null +++ b/orchestra/management/commands/staticcheck.py @@ -0,0 +1,102 @@ +# Adapted from http://djangosnippets.org/snippets/1762/ + +import ast +import os +import sys + +from django.core.management.base import BaseCommand +from pyflakes import checker, messages + +from orchestra.utils.paths import get_orchestra_root + + +# BlackHole, PySyntaxError and checking based on +# https://github.com/patrys/gedit-pyflakes-plugin.git +class BlackHole(object): + write = flush = lambda *args, **kwargs: None + + def __enter__(self): + self.stderr, sys.stderr = sys.stderr, self + + def __exit__(self, *args, **kwargs): + sys.stderr = self.stderr + + +class PySyntaxError(messages.Message): + message = 'syntax error in line %d: %s' + + def __init__(self, filename, e): + super(PySyntaxError, self).__init__(filename, e) + self.message_args = (e.offset, e.text) + + +def check(codeString, filename): + """ + Check the Python source given by C{codeString} for flakes. + + @param codeString: The Python source to check. + @type codeString: C{str} + + @param filename: The name of the file the source came from, used to report errors. + @type filename: C{str} + + @return: The number of warnings emitted. + @rtype: C{int} + """ + try: + with BlackHole(): + tree = ast.parse(codeString, filename) + except SyntaxError, e: + return [PySyntaxError(filename, e)] + else: + # Okay, it's syntactically valid. Now parse it into an ast and check it + w = checker.Checker(tree, filename) + + lines = codeString.split('\n') + # honour pyflakes: ignore comments + messages = [message for message in w.messages + if lines[message.lineno-1].find('pyflakes:ignore') < 0] + messages.sort(lambda a, b: cmp(a.lineno, b.lineno)) + return messages + + +def checkPath(filename): + """ + Check the given path, printing out any warnings detected. + @return: the number of warnings printed + """ + try: + return check(file(filename, 'U').read() + '\n', filename) + except IOError, msg: + return ["%s: %s" % (filename, msg.args[1])] + except TypeError: + pass + + +def checkPaths(filenames): + warnings = [] + for arg in filenames: + if os.path.isdir(arg): + for dirpath, dirnames, filenames in os.walk(arg): + for filename in filenames: + if filename.endswith('.py'): + warnings.extend(checkPath(os.path.join(dirpath, filename))) + else: + warnings.extend(checkPath(arg)) + return warnings +#### pyflakes.scripts.pyflakes ends. + + +class Command(BaseCommand): + help = "Run pyflakes syntax checks." + args = '[filename [filename [...]]]' + + def handle(self, *filenames, **options): + if not filenames: + filenames = [get_orchestra_root(), '.'] + warnings = checkPaths(filenames) + for warning in warnings: + print warning + if warnings: + print 'Total warnings: %d' % len(warnings) + raise SystemExit(1) diff --git a/orchestra/models/fields.py b/orchestra/models/fields.py index 9668ab6b..dbda75e1 100644 --- a/orchestra/models/fields.py +++ b/orchestra/models/fields.py @@ -1,3 +1,4 @@ +from django.core import exceptions from django.db import models from django.utils.text import capfirst diff --git a/orchestra/permissions/api.py b/orchestra/permissions/api.py index 5bd892cd..0d91cd2d 100644 --- a/orchestra/permissions/api.py +++ b/orchestra/permissions/api.py @@ -1,5 +1,4 @@ from django.core.urlresolvers import resolve -from rest_framework import exceptions from rest_framework.permissions import DjangoModelPermissions diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py index b7a5c012..855b12b5 100644 --- a/orchestra/utils/humanize.py +++ b/orchestra/utils/humanize.py @@ -111,11 +111,16 @@ def naturaldate(date): return _('today') elif days == 1: return _('yesterday') + ago = ' ago' + if days < 0: + ago = '' + days = abs(days) + delta_midnight = today - date count = 0 for chunk, pluralizefun in OLDER_CHUNKS: if days < 7.0: - count = days + float(hours)/24 + count = days fmt = pluralize_day(count) return fmt.format(num=count, ago=ago) if days >= chunk: diff --git a/orchestra/utils/options.py b/orchestra/utils/options.py index d21db6af..9924e166 100644 --- a/orchestra/utils/options.py +++ b/orchestra/utils/options.py @@ -40,4 +40,4 @@ def send_email_template(template, context, to, email_from=None, html=None, attac def running_syncdb(): - return 'migrate' in sys.argv or 'syncdb' in sys.argv + return 'migrate' in sys.argv or 'syncdb' in sys.argv or 'makemigrations' in sys.argv diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index 54fdcd30..bf1a68fa 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -66,7 +66,7 @@ class OrderedSet(collections.MutableSet): return set(self) == set(other) -class AttributeDict(dict): +class AttrDict(dict): def __init__(self, *args, **kwargs): - super(AttributeDict, self).__init__(*args, **kwargs) + super(AttrDict, self).__init__(*args, **kwargs) self.__dict__ = self diff --git a/orchestra/utils/tests.py b/orchestra/utils/tests.py index ed62399f..b79db716 100644 --- a/orchestra/utils/tests.py +++ b/orchestra/utils/tests.py @@ -6,7 +6,6 @@ from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model from django.contrib.sessions.backends.db import SessionStore from django.test import LiveServerTestCase, TestCase from orm.api import Api -from selenium.webdriver.common.keys import Keys from selenium.webdriver.firefox.webdriver import WebDriver from xvfbwrapper import Xvfb