PK! ߗabilian/__init__.py# coding=utf-8 from __future__ import absolute_import from pkgutil import extend_path # noinspection PyUnboundLocalVariable __path__ = extend_path(__path__, __name__) PK!eabilian/sbe/__init__.py# coding=utf-8 PK!}abilian/sbe/app.py# coding=utf-8 """Static configuration for the application. TODO: add more (runtime) flexibility in plugin discovery, selection and activation. """ from __future__ import absolute_import, division, print_function, \ unicode_literals import logging import os import subprocess import sys from pathlib import Path import jinja2 from abilian.app import Application as BaseApplication from abilian.core.celery import FlaskCelery as BaseCelery from abilian.core.celery import FlaskLoader as CeleryBaseLoader from abilian.core.commands import setup_abilian_commands from abilian.services import converter from flask import current_app from flask_script import Command, Manager from werkzeug.serving import BaseWSGIServer from .apps.documents.repository import repository from .extension import sbe # Used for side effects, do not remove __all__ = ["create_app", "Application"] logger = logging.getLogger(__name__) def create_app(config=None): return Application(config=config) command_manager = Manager(create_app) setup_abilian_commands(command_manager) # loader to be used by celery workers class CeleryLoader(CeleryBaseLoader): flask_app_factory = "abilian.sbe.app.create_app" celery = BaseCelery(loader=CeleryLoader) class Application(BaseApplication): APP_PLUGINS = BaseApplication.APP_PLUGINS + ( "abilian.sbe.apps.main", "abilian.sbe.apps.notifications", "abilian.sbe.apps.preferences", "abilian.sbe.apps.wiki", "abilian.sbe.apps.wall", "abilian.sbe.apps.documents", "abilian.sbe.apps.forum", # "abilian.sbe.apps.calendar", "abilian.sbe.apps.communities", "abilian.sbe.apps.social", "abilian.sbe.apps.preferences", ) script_manager = command_manager def __init__(self, name="abilian_sbe", config=None, **kwargs): BaseApplication.__init__(self, name, config=config, **kwargs) loader = jinja2.PackageLoader("abilian.sbe", "templates") self.register_jinja_loaders(loader) def init_extensions(self): BaseApplication.init_extensions(self) sbe.init_app(self) repository.init_app(self) converter.init_app(self) # SBE demo app bootstrap stuff _SBE_DEMO_SCRIPT = """\ #!{BIN_DIR}/python from __future__ import absolute_import import sys from abilian.sbe.app import command_entry_point sys.exit(command_entry_point()) """ _BASE_SERVER_ACTIVATE = BaseWSGIServer.server_activate def _on_http_server_activate(self, *args, **kwargs): """This function is used as to monkey patch BaseWSGIServer.server_activate during `setup_sbe_demo`.""" _BASE_SERVER_ACTIVATE(self) # now we are listening to socket host, port = self.server_address if host == "0.0.0.0": # chrome is not ok with 0.0.0.0 host = "localhost" url = "http://{host}:{port}/setup".format(host=host, port=port) if sys.platform == "win32": os.startfile(url) else: opener = "open" if sys.platform == "darwin" else "xdg-open" subprocess.call([opener, url]) # run with python -m abilian.sbe.app setup_sbe_app def setup_sbe_app(): """Basic set up SBE application. Must be run inside a virtualenv. Will create `abilian_sbe` script, run a local server and open browser on app's setup wizard. """ logger = logging.getLogger("sbe_demo") logger.setLevel(logging.INFO) if "VIRTUAL_ENV" not in os.environ: logger.error("Not in a virtualenv! Aborting.") return 1 bin_dir = Path(sys.prefix) / "bin" if not bin_dir.exists() or not bin_dir.is_dir(): logger.error("%s doesn't exists or is not a directory. Aborting", bin_dir) return 1 script_file = bin_dir / "abilian_sbe" if script_file.exists(): logger.info("%s already exists. Skipping creation.", script_file) else: with script_file.open("w") as out: logger.info('Create script: "%s".', script_file) content = _SBE_DEMO_SCRIPT.format(BIN_DIR=bin_dir) out.write(content) # 0755: -rwxr-xr-x script_file.chmod(0o755) current_app.config["PRODUCTION"] = True current_app.config["DEBUG"] = False current_app.config["ASSETS_DEBUG"] = False current_app.config["SITE_NAME"] = "Abilian SBE" current_app.config["MAIL_SENDER"] = "abilian-sbe-app@example.com" logger.info("Prepare CSS & JS files") command_manager.handle("abilian_sbe", ["assets", "build"]) # disabled init config: only if not running setupwizard # command_manager.handle('abilian_sbe', ['config', 'init']) # patch server used to launch browser here immediately after socket opened BaseWSGIServer.server_activate = _on_http_server_activate return command_manager.handle("abilian_sbe", ["run", "--hide-config"]) def command_entry_point(): command_manager.run(commands={"setup_sbe_app": Command(setup_sbe_app)}) if __name__ == "__main__": command_entry_point() PK!eabilian/sbe/apps/__init__.py# coding=utf-8 PK!{EE%abilian/sbe/apps/calendar/__init__.py# coding=utf-8 """Calendar module.""" from __future__ import absolute_import from abilian.sbe.extension import sbe def register_plugin(app): sbe.init_app(app) from .views import blueprint from .actions import register_actions blueprint.record_once(register_actions) app.register_blueprint(blueprint) PK! ,$abilian/sbe/apps/calendar/actions.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.i18n import _l from abilian.services import get_service from abilian.services.security import Admin from abilian.web.action import Action, FAIcon, actions from flask import g, url_for from flask_login import current_user class CalendarAction(Action): def url(self, context=None): return url_for("." + self.name, community_id=g.community.slug) class EventAction(CalendarAction): def pre_condition(self, context): event = context.get("object") return not not event def url(self, context=None): event = context.get("object") return url_for( "." + self.name, community_id=g.community.slug, event_id=event.id ) def is_admin(context): security = get_service("security") return security.has_role(current_user, Admin, object=context.get("object")) _actions = [ CalendarAction( "calendar:global", "new_event", _l("Create a new event"), icon="plus" ), CalendarAction("calendar:global", "index", _l("Upcoming events"), icon="list"), EventAction("calendar:event", "event", _l("View event"), icon=FAIcon("eye")), EventAction( "calendar:event", "event_edit", _l("Edit event"), icon=FAIcon("pencil") ), ] def register_actions(state): if not actions.installed(state.app): return with state.app.app_context(): actions.register(*_actions) PK!(Ъ"abilian/sbe/apps/calendar/forms.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import bleach from abilian.i18n import _l from abilian.web.forms import Form from abilian.web.forms.fields import DateTimeField from abilian.web.forms.filters import strip from abilian.web.forms.validators import required from abilian.web.forms.widgets import RichTextWidget from wtforms import StringField, TextAreaField, ValidationError from wtforms.fields.html5 import URLField ALLOWED_TAGS = [ "a", "abbr", "acronym", "b", "blockquote", "br", "code", "em", "i", "li", "ol", "strong", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "p", "u", "img", ] ALLOWED_ATTRIBUTES = { "*": ["title"], "p": ["style"], "a": ["href", "title"], "abbr": ["title"], "acronym": ["title"], "img": ["src", "alt", "title"], } ALLOWED_STYLES = ["text-align"] WIDGET_ALLOWED = {} for attr in ALLOWED_TAGS: allowed = ALLOWED_ATTRIBUTES.get(attr, True) if not isinstance(allowed, bool): allowed = {tag: True for tag in allowed} WIDGET_ALLOWED[attr] = allowed class EventForm(Form): title = StringField(label=_l("Title"), filters=(strip,), validators=[required()]) start = DateTimeField(_l("Start"), validators=[required()]) end = DateTimeField(_l("End"), validators=[required()]) location = TextAreaField(label=_l("Location"), filters=(strip,)) url = URLField(label=_l("URL"), filters=(strip,)) description = TextAreaField( label=_l("Description"), widget=RichTextWidget(allowed_tags=WIDGET_ALLOWED), filters=(strip,), validators=[required()], ) def validate_description(self, field): field.data = bleach.clean( field.data, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, styles=ALLOWED_STYLES, strip=True, ) def validate_end(self, field): if self.start.data > self.end.data: raise ValidationError(_l("End date/time must be after start")) EventForm.start.kwargs["raw_data"] = [" | 09:00"] EventForm.end.kwargs["raw_data"] = [" | 18:00"] PK!.Х  #abilian/sbe/apps/calendar/models.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from abilian.core.entities import SEARCHABLE, Entity from sqlalchemy import Column, DateTime, Unicode from sqlalchemy.event import listens_for from sqlalchemy.orm import backref, relationship from abilian.sbe.apps.communities.models import Community, CommunityIdColumn, \ community_content @community_content class Event(Entity): __tablename__ = "sbe_event" community_id = CommunityIdColumn() #: The community this event belongs to community = relationship( Community, primaryjoin=(community_id == Community.id), backref=backref("events", cascade="all, delete-orphan"), ) title = Column(Unicode, nullable=False, default="", info=SEARCHABLE) description = Column(Unicode, nullable=False, default="", info=SEARCHABLE) location = Column(Unicode, nullable=False, default="", info=SEARCHABLE) start = Column(DateTime, nullable=False) end = Column(DateTime) url = Column(Unicode, nullable=False, default="") @listens_for(Event.title, "set", active_history=True) def _event_sync_name_title(entity, new_value, old_value, initiator): if entity.name != new_value: entity.name = new_value return new_value PK!$ 7abilian/sbe/apps/calendar/templates/calendar/_base.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_menu %} {%- block sidebar %} {%- call m_box_menu() %} {%- endcall %} {%- endblock %} PK!_XxBB:abilian/sbe/apps/calendar/templates/calendar/archives.html{% extends "calendar/_base.html" %} {% from "macros/box.html" import m_box_menu, m_box_content %} {% block content %} {% call m_box_content(_("Past events")) %} {% for group in groups %}

{{ group[0]|dateformat("MMMM YYY")|capitalize }}

{% else %}

{{ _("No event has been posted to this calendar yet.") }}

{% endfor %} {% endcall %} {% endblock %} PK!o7abilian/sbe/apps/calendar/templates/calendar/event.html{% extends "calendar/_base.html" %} {%- from "macros/box.html" import m_box_content, m_box_menu -%} {%- from "macros/form.html" import m_field -%} {%- block content %} {% call m_box_content() %} {# TODO #}

{{ _("Back to calendar") }}

{{ event.title }}

{{ _("Start") }}: {{ event.start | datetimeformat }}

{{ _("End") }}: {{ event.end | datetimeformat }}

{% if event.location %}

{{ event.location }}

{% endif %}
{{ event.description|safe }}
{% if event.url %} {{ _('More information') }}: {{ event.url }} {% endif %}
{% endcall %} {%- endblock %} PK!~7abilian/sbe/apps/calendar/templates/calendar/index.html{% extends "calendar/_base.html" %} {% from "macros/box.html" import m_box_menu, m_box_content %} {% block content %} {% call m_box_content(_("Upcoming events")) %} {% for group in groups %}

{{ group[0]|dateformat("MMMM YYY")|capitalize }}

{% else %}

{{ _("There are no upcoming events on this calendar yet.") }}

{% endfor %}

[Archives]

{% endcall %} {% endblock %} PK!e+abilian/sbe/apps/calendar/tests/__init__.py# coding=utf-8 PK!Ɗ (abilian/sbe/apps/calendar/tests/tests.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime from flask import url_for from pytest import mark from ..models import Event def test_create_event(): start = datetime.now() event = Event(name="Test thread", start=start) assert event # TODO @mark.skip def test(community1, client, req_ctx): response = client.get(url_for("calendar.index", community_id=community1.slug)) assert response.status_code == 200 # @mark.skip # def test_event_indexed(community1, community2, db, client, req_ctx): # start = datetime.now() # event1 = Event(name="Test event", community=community1, start=start) # event2 = Event(name="Test other event", community=community2, start=start) # db.session.add(event1) # db.session.add(event2) # db.session.commit() # # svc = self.svc # obj_types = (Event.entity_type,) # with self.login(self.user_no_community): # res = svc.search("event", object_types=obj_types) # assert len(res) == 0 # # with self.login(self.user): # res = svc.search("event", object_types=obj_types) # assert len(res) == 1 # hit = res[0] # assert hit["object_key"] == event1.object_key # # with self.login(self.user_c2): # res = svc.search("event", object_types=obj_types) # assert len(res) == 1 # hit = res[0] # assert hit["object_key"] == event2.object_key PK!I I "abilian/sbe/apps/calendar/views.py# coding=utf-8 """Forum views.""" from __future__ import absolute_import, print_function, unicode_literals from datetime import date, datetime from abilian.i18n import _l from abilian.web import url_for, views from abilian.web.action import ButtonAction from flask import g, render_template from toolz import groupby from ..communities.blueprint import Blueprint from ..communities.views import default_view_kw from .forms import EventForm from .models import Event blueprint = Blueprint( "calendar", __name__, url_prefix="/calendar", template_folder="templates" ) route = blueprint.route @route("/") def index(): events = Event.query.filter(Event.end > datetime.now()).order_by(Event.start).all() def get_month(event): year = event.start.year month = event.start.month return date(year, month, 1) groups = sorted(groupby(get_month, events).items()) ctx = {"groups": groups} return render_template("calendar/index.html", **ctx) @route("/archives/") def archives(): events = ( Event.query.filter(Event.end <= datetime.now()) .order_by(Event.start.desc()) .all() ) def get_month(event): year = event.start.year month = event.start.month return date(year, month, 1) groups = sorted(groupby(get_month, events).items(), reverse=True) ctx = {"groups": groups} return render_template("calendar/archives.html", **ctx) class BaseEventView(object): Model = Event Form = EventForm pk = "event_id" base_template = "community/_base.html" def index_url(self): return url_for(".index", community_id=g.community.slug) def view_url(self): return url_for(self.obj) class EventView(BaseEventView, views.ObjectView): methods = ["GET", "HEAD"] Form = EventForm template = "calendar/event.html" @property def template_kwargs(self): kw = super(EventView, self).template_kwargs kw["event"] = self.obj return kw event_view = EventView.as_view("event") views.default_view(blueprint, Event, "event_id", kw_func=default_view_kw)(event_view) route("//")(event_view) class EventCreateView(BaseEventView, views.ObjectCreate): POST_BUTTON = ButtonAction( "form", "create", btn_class="primary", title=_l("Post this event") ) title = _l("New event") def after_populate_obj(self): if self.obj.community is None: self.obj.community = g.community._model def get_form_buttons(self, *args, **kwargs): return [self.POST_BUTTON, views.object.CANCEL_BUTTON] @property def activity_target(self): return self.obj.community event_create_view = EventCreateView.as_view("new_event", view_endpoint=".event") route("/new_event/")(event_create_view) class EventEditView(BaseEventView, views.ObjectEdit): POST_BUTTON = ButtonAction( "form", "create", btn_class="primary", title=_l("Post this event") ) title = _l("Edit event") event_edit_view = EventEditView.as_view("event_edit", view_endpoint=".event") route("//edit")(event_edit_view) PK! O1gg(abilian/sbe/apps/communities/__init__.py# coding=utf-8 """Communities module.""" from __future__ import absolute_import from abilian.sbe.extension import sbe def register_plugin(app): sbe.init_app(app) # Used for side-effect from . import events # noqa from .views import communities app.register_blueprint(communities) from . import search search.init_app(app) PK!5OA 'abilian/sbe/apps/communities/actions.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.web.action import Action, Endpoint, actions from abilian.web.nav import NavItem from flask import g from flask import url_for as url_for_orig from flask_babel import lazy_gettext as _l from flask_login import current_user __all__ = ["register_actions"] def url_for(endpoint, **kw): return url_for_orig(endpoint, community_id=g.community.slug, **kw) class CommunityEndpoint(Endpoint): def get_kwargs(self): kwargs = super(CommunityEndpoint, self).get_kwargs() kwargs["community_id"] = g.community.slug return kwargs class CommunityTabAction(Action): Endpoint = CommunityEndpoint def url(self, context=None): if self._url: return Action.url(self) endpoint = self.endpoint if endpoint: return endpoint else: return url_for("%s.index" % self.name) def is_current(self): return g.current_tab == self.name _actions = ( # Navigation NavItem( "section", "communities", title=_l("Communities"), url=lambda context: url_for_orig("communities.index"), condition=lambda ctx: current_user.is_authenticated, ), # Tabs CommunityTabAction("communities:tabs", "wall", _l("Activities")), CommunityTabAction( "communities:tabs", "documents", _l("Documents"), condition=lambda ctx: g.community.has_documents, ), CommunityTabAction( "communities:tabs", "wiki", _l("Wiki"), condition=lambda ctx: g.community.has_wiki, ), CommunityTabAction( "communities:tabs", "forum", _l("Conversations"), condition=lambda ctx: g.community.has_forum, ), CommunityTabAction( "communities:tabs", "calendar", _l("Calendar"), condition=lambda ctx: g.community.has_calendar, ), CommunityTabAction( "communities:tabs", "members", _l("Members"), endpoint="communities.members" ), CommunityTabAction( "communities:tabs", "settings", _l("Settings"), icon="cog", condition=lambda ctx: current_user.has_role("admin"), endpoint="communities.settings", ), ) def register_actions(state): if not actions.installed(state.app): return with state.app.app_context(): actions.register(*_actions) PK!- )abilian/sbe/apps/communities/blueprint.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from abilian.i18n import _l from abilian.web.action import Endpoint from abilian.web.nav import BreadcrumbItem from flask import Blueprint as BaseBlueprint from flask import g from werkzeug.exceptions import NotFound from . import security from .models import Community from .presenters import CommunityPresenter class Blueprint(BaseBlueprint): """Blueprint for community based views. It sets g.community and perform access verification for the traversed community. """ _BASE_URL_PREFIX = "/communities" _ROUTE_PARAM = "" def __init__(self, *args, **kwargs): url_prefix = kwargs.get("url_prefix", "") if kwargs.pop("set_community_id_prefix", True): if (not url_prefix) or url_prefix[0] != "/": url_prefix = "/" + url_prefix url_prefix = self._ROUTE_PARAM + url_prefix if not url_prefix.startswith(self._BASE_URL_PREFIX): if (not url_prefix) or url_prefix[0] != "/": url_prefix = "/" + url_prefix url_prefix = self._BASE_URL_PREFIX + url_prefix if url_prefix[-1] == "/": url_prefix = url_prefix[:-1] kwargs["url_prefix"] = url_prefix BaseBlueprint.__init__(self, *args, **kwargs) self.url_value_preprocessor(pull_community) self.url_value_preprocessor(init_current_tab) self.before_request(check_access) def check_access(): if hasattr(g, "community"): # communities.index is not inside a community, for example security.check_access(g.community) def init_current_tab(endpoint, values): """Ensure g.current_tab exists.""" g.current_tab = None def pull_community(endpoint, values): """url_value_preprocessor function.""" g.nav["active"] = "section:communities" g.breadcrumb.append( BreadcrumbItem(label=_l("Communities"), url=Endpoint("communities.index")) ) try: slug = values.pop("community_id") community = Community.query.filter(Community.slug == slug).first() if community: g.community = CommunityPresenter(community) wall_url = Endpoint("wall.index", community_id=community.slug) breadcrumb_item = BreadcrumbItem(label=community.name, url=wall_url) g.breadcrumb.append(breadcrumb_item) else: raise NotFound() except KeyError: pass PK!\B||&abilian/sbe/apps/communities/common.py# coding=utf-8 """Forum views.""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime from abilian.i18n import _l from abilian.services.viewtracker import viewtracker from flask import g from flask_babel import format_date from abilian.sbe.apps.communities.security import is_manager def object_viewers(entity): if is_manager(): views = viewtracker.get_views(entity=entity) community_members_id = [ user.id for user in g.community.members if user.id != entity.creator.id ] viewers = [] for view in views: if view.user_id in set(community_members_id): viewers.append( {"user": view.user, "viewed_at": view.hits[-1].viewed_at} ) return viewers def activity_time_format(time, now=None): if not time: return "" if not now: now = datetime.utcnow() time_delta = now - time month_abbreviation = format_date(time, "MMM") days, hours, minutes, seconds = ( time_delta.days, time_delta.seconds // 3600, time_delta.seconds // 60, time_delta.seconds, ) if days == 0 and hours == 0 and minutes == 0: return "{}{}".format(seconds, _l("s")) if days == 0 and hours == 0: return "{}{}".format(minutes, _l("m")) if days == 0: return "{}{}".format(hours, _l("h")) if days < 30: return "{}{}".format(days, _l("d")) if time.year == now.year: return "{} {}".format(month_abbreviation, time.day) return "{} {}".format(month_abbreviation, str(time.year)) PK!*  &abilian/sbe/apps/communities/events.py# coding=utf-8 """Lightweight integration and denormalisation using events (signals).""" from __future__ import absolute_import, print_function, unicode_literals from abilian.core.signals import activity from blinker import ANY from abilian.sbe.apps.documents.models import Document from .models import Community @activity.connect_via(ANY) def update_community(sender, verb, actor, object, target=None): if isinstance(object, Community): object.touch() return if isinstance(target, Community): community = target community.touch() if isinstance(object, Document): if verb == "post": community.document_count += 1 elif verb == "delete": community.document_count -= 1 PK!("%abilian/sbe/apps/communities/forms.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import imghdr import PIL import sqlalchemy as sa from abilian.core.models.subjects import Group from abilian.web.forms import Form from abilian.web.forms.fields import FileField, Select2Field from abilian.web.forms.validators import length from abilian.web.forms.widgets import BooleanWidget, ImageInput, TextArea from flask import request from flask_babel import gettext as _ from flask_babel import lazy_gettext as _l from six import text_type from wtforms.fields import BooleanField, StringField, TextAreaField from wtforms.validators import ValidationError, data_required, optional from .models import Community def strip(s): return s.strip() def _group_choices(): m_prop = Group.members.property membership = m_prop.secondary query = Group.query.session.query( Group.id, Group.name, Community.name.label("community"), sa.sql.func.count(membership.c.user_id).label("members_count"), ) query = ( query.outerjoin(m_prop.secondary, m_prop.primaryjoin) .outerjoin(Community, Community.group.property.primaryjoin) .group_by(Group.id, Group.name, Community.name) .order_by(sa.sql.func.lower(Group.name)) .autoflush(False) ) choices = [("", "")] for g in query: label = "{} ({:d} membres)".format(g.name, g.members_count) if g.community: label += " — Communauté: {}".format(g.community) choices.append((text_type(g.id), label)) return choices class CommunityForm(Form): name = StringField(label=_l("Name"), validators=[data_required()]) description = TextAreaField( label=_l("Description"), validators=[data_required(), length(max=500)], widget=TextArea(resizeable="vertical"), ) linked_group = Select2Field( label=_l("Linked to group"), description=_l("Manages a group of users through this community members."), choices=_group_choices, ) image = FileField( label=_l("Image"), widget=ImageInput(width=65, height=65), validators=[optional()], ) type = Select2Field( label=_("Type"), validators=[data_required()], filters=(strip,), choices=[ (_l("informative"), "informative"), (_l("participative"), "participative"), ], ) has_documents = BooleanField( label=_l("Has documents"), widget=BooleanWidget(on_off_mode=True) ) has_wiki = BooleanField( label=_l("Has a wiki"), widget=BooleanWidget(on_off_mode=True) ) has_forum = BooleanField( label=_l("Has a forum"), widget=BooleanWidget(on_off_mode=True) ) def validate_name(self, field): name = field.data = field.data.strip() if name and field.object_data: # form is bound to an existing object, name is not empty if name != field.object_data: # name changed: check for duplicates if Community.query.filter(Community.name == name).count() > 0: raise ValidationError( _("A community with this name already exists") ) def validate_description(self, field): field.data = field.data.strip() # FIXME: code duplicated from the user edit form (UserProfileForm). # Needs to be refactored. def validate_image(self, field): data = request.form.get("image") if not data: return data = field.data filename = data.filename valid = any(map(filename.lower().endswith, (".png", ".jpg", ".jpeg"))) if not valid: raise ValidationError(_("Only PNG or JPG image files are accepted")) img_type = imghdr.what("ignored", data.read()) if img_type not in ("png", "jpeg"): raise ValidationError(_("Only PNG or JPG image files are accepted")) data.seek(0) try: # check this is actually an image file im = PIL.Image.open(data) im.load() except BaseException: raise ValidationError(_("Could not decode image file")) data.seek(0) field.data = data PK!`2??&abilian/sbe/apps/communities/models.py# coding=utf-8 """""" from __future__ import absolute_import, division, print_function, \ unicode_literals import logging import time from datetime import datetime from pathlib import Path from typing import Any, Dict import sqlalchemy as sa from abilian.core.entities import Entity from abilian.core.extensions import db from abilian.core.models import NOT_AUDITABLE, SEARCHABLE from abilian.core.models.blob import Blob from abilian.core.models.subjects import Group, User from abilian.i18n import _l from abilian.services.indexing import indexable_role from abilian.services.security import READ, WRITE, Admin from abilian.services.security import Manager as MANAGER from abilian.services.security import Permission from abilian.services.security import Reader as READER from abilian.services.security import Role, RoleType from abilian.services.security import Writer as WRITER from abilian.services.security import security from blinker import ANY from flask import current_app from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, \ String, Unicode, UniqueConstraint, and_ from sqlalchemy.event import listens_for from sqlalchemy.orm import backref, relation, relationship from sqlalchemy.orm.attributes import OP_APPEND, OP_REMOVE from abilian.sbe.apps.documents.models import Folder from abilian.sbe.apps.documents.repository import repository from . import signals logger = logging.getLogger(__name__) MEMBER = Role("member", label=_l("role_member"), assignable=False) VALID_ROLES = frozenset([READER, WRITER, MANAGER, MEMBER]) class Membership(db.Model): """Represents the membership of someone in a community.""" __tablename__ = "community_membership" id = Column(Integer, primary_key=True) user_id = Column(ForeignKey("user.id"), index=True, nullable=False) user = relationship( User, lazy="joined", backref=backref("communautes_membership", lazy="select") ) community_id = Column(ForeignKey("community.id"), index=True, nullable=False) community = relationship("Community", lazy="joined") role = Column(RoleType()) # should be either 'member' or 'manager' __table_args__ = (UniqueConstraint("user_id", "community_id"),) def __repr__(self): return "".format( repr(self.user), repr(self.community), str(self.role) ).encode("utf-8") def community_content(cls): """Class decorator to mark models considered as community content. This is required for proper indexation. """ cls.is_community_content = True def community_slug(self): return self.community and self.community.slug cls.community_slug = property(community_slug) index_to = cls.__indexation_args__.setdefault("index_to", ()) index_to += (("community_slug", ("community_slug",)),) cls.__indexation_args__["index_to"] = index_to def _indexable_roles_and_users(self): if not self.community: return [] return indexable_roles_and_users(self.community) cls._indexable_roles_and_users = property(_indexable_roles_and_users) return cls def indexable_roles_and_users(community): """Mixin to use to replace Entity._indexable_roles_and_users. Will be removed when communities are upgraded to use standard role based access (by setting permissions and using security service). """ return " ".join(indexable_role(user) for user in community.members) class Community(Entity): """Ad-hoc objects that hold properties about a community.""" __indexation_args__ = {} # type: Dict[str, Any] __indexation_args__.update(Entity.__indexation_args__) index_to = __indexation_args__.setdefault("index_to", ()) index_to += (("id", ("id", "community_id")),) __indexation_args__["index_to"] = index_to del index_to is_community_content = True # : A public description. description = Column(Unicode(500), default="", nullable=False, info=SEARCHABLE) #: An image or logo for this community. image_id = Column(ForeignKey(Blob.id), index=True) image = relationship(Blob, lazy="joined") #: The root folder for this community. folder_id = Column( Integer, ForeignKey(Folder.id, use_alter=True, name="fk_community_root_folder"), unique=True, ) folder = relation( Folder, single_parent=True, # required for delete-orphan primaryjoin=(folder_id == Folder.id), cascade="all, delete-orphan", backref=backref("_community", lazy="select", uselist=False), ) #: The group this community is linked to, if any. Memberships are then #: reflected group_id = Column(ForeignKey(Group.id), nullable=True, unique=True) group = relation(Group, foreign_keys=group_id, lazy="select") #: Memberships for this community. memberships = relationship(Membership, cascade="all, delete-orphan") #: direct access to :class:`User` members members = relationship( User, secondary=Membership.__table__, viewonly=True, backref=backref("communities", lazy="select", viewonly=True), ) #: Number of members in this community. membership_count = Column(Integer, default=0, nullable=False, info=NOT_AUDITABLE) #: Number of documents in this community. document_count = Column(Integer, default=0, nullable=False, info=NOT_AUDITABLE) #: Last time something happened in this community last_active_at = Column( DateTime, default=datetime.utcnow, nullable=False, info=NOT_AUDITABLE ) # Various features for this community: #: True if this community has a document management space has_documents = Column(Boolean, nullable=False, default=True) #: True if this community has a wiki has_wiki = Column(Boolean, nullable=False, default=True) #: True if this community has a forum has_forum = Column(Boolean, nullable=False, default=True) #: One of 'participative' or 'informative type = Column(String(20), nullable=False, default="informative") #: One of 'secret', 'public', 'open' (currently not used) visibility = Column(String(20), nullable=False, default="secret") #: Used for segmenting communities. Not used currently. category = Column(String(20), nullable=False, default="") #: True if regular members can send stuff by email to all members # Not used anymore. members_can_send_by_email = Column(Boolean, nullable=False, default=False) def __init__(self, **kw): self.has_documents = True self.membership_count = 0 self.document_count = 0 self.members_can_send_by_email = False Entity.__init__(self, **kw) if self.has_documents and not self.folder: # FIXME: this should be done in documents by using signals name = self.name if not name: # during creation, we may have to provide a temporary name for # subfolder, we don't want empty names on folders since they must be # unique among siblings name = "{}_{}-{}".format( self.__class__.__name__, str(self.id), time.asctime() ) self.folder = repository.root_folder.create_subfolder(name) # if not self.group: # self.group = Group(name=self.name) if not self.image: fn = Path(__file__).parent / "views" / "data" / "community.png" self.image = Blob(fn.open("rb").read()) @property def has_calendar(self): config = current_app.config return bool(config.get("ENABLE_CALENDAR")) def rename(self, name): self.name = name if self.folder: # FIXME: use signals self.folder.name = name def get_memberships(self, role=None): M = Membership memberships = M.query.filter(M.community_id == self.id) if role: memberships = memberships.filter(M.role == role) return memberships.all() def set_membership(self, user, role): # type: (User, Role) -> None """Add a member with the given role, or set the role of an existing member.""" assert isinstance(user, User) role = Role(role) if role not in VALID_ROLES: raise ValueError("Invalid role: {}".format(role)) session = sa.orm.object_session(self) or db.session() is_new = True M = Membership membership = ( session.query(M) .filter(and_(M.user_id == user.id, M.community_id == self.id)) .first() ) if not membership: membership = Membership(community=self, user=user, role=role) session.add(membership) self.membership_count += 1 else: is_new = False membership.role = role signals.membership_set.send(self, membership=membership, is_new=is_new) def remove_membership(self, user): M = Membership membership = M.query.filter( and_(M.user_id == user.id, M.community_id == self.id) ).first() if not membership: raise KeyError("User {} is not a member of community {}".format(user, self)) db.session.delete(membership) self.membership_count -= 1 signals.membership_removed.send(self, membership=membership) def update_roles_on_folder(self): if self.folder: self.ungrant_all_roles_on_folder() for membership in self.memberships: user = membership.user role = membership.role if role == MANAGER: security.grant_role(user, MANAGER, self.folder) else: if self.type == "participative": security.grant_role(user, WRITER, self.folder) else: security.grant_role(user, READER, self.folder) def ungrant_all_roles_on_folder(self): if self.folder: role_assignments = security.get_role_assignements(self.folder) for principal, role in role_assignments: security.ungrant_role(principal, role, self.folder) def get_role(self, user): """Returns the given user's role in this community.""" M = Membership membership = ( db.session() .query(M.role) .filter(and_(M.community_id == self.id, M.user_id == user.id)) .first() ) return membership.role if membership else None def has_member(self, user): return self.get_role(user) is not None def has_permission(self, user, permission): if not isinstance(permission, Permission): assert isinstance(permission, str) permission = Permission(permission) if user.has_role(Admin): return True role = self.get_role(user) if role == MANAGER: return True if role == MEMBER and permission in (READ, WRITE): return True return False def touch(self): self.last_active_at = datetime.utcnow() @property def _indexable_roles_and_users(self): return indexable_roles_and_users(self) def CommunityIdColumn(): return Column( ForeignKey(Community.id), nullable=False, info=SEARCHABLE | {"index_to": (("community_id", ("community_id",)),)}, ) # Handlers to keep community/group members in sync # _PROCESSED_ATTR = "__sbe_community_group_sync_processed__" @signals.membership_set.connect_via(ANY) def _membership_added(sender, membership, is_new): if not is_new: return if getattr(membership.user, _PROCESSED_ATTR, False) is OP_APPEND: return if sender.group and membership.user not in sender.group.members: logger.debug( "_membership_added(%r, %r, %r) user: %r", sender, membership, is_new, membership.user, ) setattr(membership.user, _PROCESSED_ATTR, OP_APPEND) sender.group.members.add(membership.user) @signals.membership_removed.connect_via(ANY) def membership_removed(sender, membership): if getattr(membership.user, _PROCESSED_ATTR, False) is OP_REMOVE: return if sender.group and membership.user in sender.group.members: logger.debug( "_membership_removed(%r, %r) user: %r", sender, membership, membership.user ) setattr(membership.user, _PROCESSED_ATTR, OP_REMOVE) sender.group.members.discard(membership.user) @listens_for(Community.members, "append") @listens_for(Community.members, "remove") def _on_member_change(community, user, initiator): group = community.group if not group: return logger.debug("_on_member_change(%r, %r, op=%r)", community, user, initiator.op) if getattr(user, _PROCESSED_ATTR, False) is initiator.op: return setattr(user, _PROCESSED_ATTR, initiator.op) if initiator.op is OP_APPEND: if user not in group.members: group.members.add(user) elif initiator.op is OP_REMOVE: if user in group.members: group.members.discard(user) @listens_for(Community.group, "set", active_history=True) def _on_linked_group_change(community, value, oldvalue, initiator): if value == oldvalue: return logger.debug("_on_linked_group_change(%r, %r, %r)", community, value, oldvalue) if oldvalue is not None and oldvalue.members: logger.debug( "_on_linked_group_change(%r, %r, %r): oldvalue clear()", community, value, oldvalue, ) oldvalue.members.clear() members = set(community.members) if value is not None and value.members != members: logger.debug( "_on_linked_group_change(%r, %r, %r): set value.members", community, value, oldvalue, ) value.members = members def _safe_get_community(group): session = sa.orm.object_session(group) if not session: return None with session.no_autoflush: try: return ( session.query(Community) .filter(Community.group == group) .options( sa.orm.joinedload(Community.group), sa.orm.joinedload(Community.members), ) .one() ) except sa.orm.exc.NoResultFound: return None @listens_for(Group.members, "append") @listens_for(Group.members, "remove") def _on_group_member_change(group, user, initiator): community = _safe_get_community(group) if not community: return op = initiator.op if getattr(user, _PROCESSED_ATTR, False) is op: return is_present = user in community.members setattr(user, _PROCESSED_ATTR, op) logger.debug( "_on_group_member_change(%r, %r, op=%r) community: %r", group, user, initiator.op, community, ) if (op is OP_APPEND and is_present) or (op is OP_REMOVE and not is_present): return if op is OP_APPEND: community.set_membership(user, MEMBER) elif op is OP_REMOVE: community.remove_membership(user) @listens_for(Group.members, "set", active_history=True) def _on_group_members_replace(group, value, oldvalue, initiator): if value == oldvalue: return community = _safe_get_community(group) if not community: return members = set(community.members) logger.debug( "_on_group_members_replace(%r, %r, %r) community: %r", group, value, oldvalue, community, ) for u in members - value: if getattr(u, _PROCESSED_ATTR, False) is OP_REMOVE: continue setattr(u, _PROCESSED_ATTR, OP_REMOVE) community.remove_membership(u) for u in value - members: if getattr(u, _PROCESSED_ATTR, False) is OP_APPEND: continue setattr(u, _PROCESSED_ATTR, OP_APPEND) community.set_membership(u, MEMBER) PK!4%*abilian/sbe/apps/communities/presenters.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.core.util import BasePresenter from flask_babel import lazy_gettext as _l class CommunityPresenter(BasePresenter): @property def breadcrumbs(self): return [ {"label": _l("Communities"), "path": "/communities/"}, {"label": self._model.name}, ] PK!we &abilian/sbe/apps/communities/search.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import logging import whoosh.fields as wf import whoosh.query as wq from flask import g from flask_login import current_user from .models import Membership logger = logging.getLogger(__name__) _COMMUNITY_CONTENT_FIELDNAME = "is_community_content" _COMMUNITY_CONTENT_FIELD = wf.BOOLEAN() _COMMUNITY_ID_FIELD = wf.NUMERIC( numtype=int, bits=64, signed=False, stored=True, unique=False ) _COMMUNITY_SLUG_FIELD = wf.ID(stored=True) _FIELDS = [ (_COMMUNITY_CONTENT_FIELDNAME, _COMMUNITY_CONTENT_FIELD), ("community_id", _COMMUNITY_ID_FIELD), ("community_slug", _COMMUNITY_SLUG_FIELD), ] def init_app(app): """Add community fields to indexing service schema.""" indexing = app.services["indexing"] indexing.register_search_filter(filter_user_communities) indexing.register_value_provider(mark_non_community_content) for _name, schema in indexing.schemas.items(): for fieldname, field in _FIELDS: if fieldname in schema: if schema[fieldname] is not field: logger.warning( 'Field "%s" already in schema %r, replacing with ' "expected fieldtype instance", fieldname, schema, ) del schema._fields[fieldname] else: continue schema.add(fieldname, field) def filter_user_communities(): if g.is_manager: return None filter_q = wq.Term(_COMMUNITY_CONTENT_FIELDNAME, False) if not current_user.is_anonymous: ids = ( Membership.query.filter(Membership.user == current_user) .order_by(Membership.community_id.asc()) .values(Membership.community_id) ) communities = [wq.Term("community_id", i[0]) for i in ids] if communities: communities = wq.And( [wq.Term(_COMMUNITY_CONTENT_FIELDNAME, True), wq.Or(communities)] ) filter_q = wq.Or([filter_q, communities]) return filter_q def mark_non_community_content(document, obj): if _COMMUNITY_CONTENT_FIELDNAME not in document: document[_COMMUNITY_CONTENT_FIELDNAME] = getattr( obj, _COMMUNITY_CONTENT_FIELDNAME, False ) return document PK!S  (abilian/sbe/apps/communities/security.py# coding=utf-8 """Decorators and helpers to check access to communities.""" from __future__ import absolute_import, print_function, unicode_literals from functools import wraps from abilian.services import get_service from abilian.services.security import MANAGE from flask import g from flask_login import current_user from werkzeug.exceptions import Forbidden def require_admin(func): @wraps(func) def decorated_view(*args, **kwargs): security = get_service("security") is_admin = security.has_role(current_user, "admin") if not is_admin: raise Forbidden() return func(*args, **kwargs) return decorated_view def require_manage(func): @wraps(func) def decorated_view(*args, **kwargs): community = getattr(g, "community") if community and community.has_permission(current_user, MANAGE): return func(*args, **kwargs) security = get_service("security") is_admin = security.has_role(current_user, "admin") if not is_admin: raise Forbidden() return func(*args, **kwargs) return decorated_view def require_access(func): @wraps(func) def decorated_view(*args, **kwargs): check_access() return func(*args, **kwargs) return decorated_view def check_access(community=None, user=None): if not has_access(community, user): raise Forbidden() def has_access(community=None, user=None): if not user: user = current_user if user.is_anonymous: return False security = get_service("security") is_admin = security.has_role(user, "admin") if is_admin: return True if not community: community = getattr(g, "community", None) if community is not None: return community.get_role(user) is not None return False def is_manager(context=None, user=None): security = get_service("security") if not user: user = current_user if user.is_anonymous: return False if context: community = context.get("object").community else: community = g.community if community.has_permission(user, MANAGE) or user == community.creator: return True if security.has_role(user, "admin"): return True return False PK!v#  'abilian/sbe/apps/communities/signals.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from blinker.base import Namespace # pylint: disable=C0103 # invalid constant name ns = Namespace() #: sent when membership is set. Sender is community, arguments are: #: :class:`.models.Membership` instance, :bool:`is_new` membership_set = ns.signal("membership_set") #: sent just before membership is removed. Sender is community, arguments: # :class:`.models.Membership` instance membership_removed = ns.signal("membership_removed") PK!;abilian/sbe/apps/communities/templates/community/_base.html{#- base template for communities (as container for social apps) #} {% extends "base.html" %} {% from "macros/box.html" import m_box_forum %} {%- set steps =[_("Add Members"), _("Existing and new members"), _("Create Accounts")] %} {% block header %}

{{ g.community.name }} {% if threads %} - {{ threads|length }} Conversation(s) {% endif %}

{%- set tabs = actions.for_category('communities:tabs') %} {% endblock %} {% block main %}
{%- block content %} {%- endblock %}
{%- block forumcontent %} {%- endblock %} {%- block memberscontent %} {%- endblock %} {%- block documentcontent %} {%- endblock %} {%- block modals %} {% endblock %} {% endblock %} PK!@abilian/sbe/apps/communities/templates/community/_forumbase.html{#- base template for communities (as container for social apps) #} {% extends "base.html" %} {% from "macros/box.html" import m_box_forum %} {% block header %}

{{ g.community.name }}{% if threads %} - {{ threads|length }} Conversation(s){% endif %}

{%- set tabs = actions.for_category('communities:tabs') %} {% endblock %} {% block main %}
{%- block content %} {%- endblock %}
{%- block forumcontent %} {%- endblock %} {%- block forumsidebar %} {%- endblock %}
{%- block modals %} {% endblock %} {% endblock %} {% macro forum_menu(action) %} {% endmacro %} PK!0 :abilian/sbe/apps/communities/templates/community/edit.html{% extends "default/object_edit.html" %} {% from "macros/box.html" import m_box_content with context %} {% from "macros/form.html" import m_field with context %} {% block after_form %} {% call m_box_content(_("Delete community"), color="danger") %}

Supprimer la communauté (attention, action irréversible!)

{{ form.csrf_token }}
{%- deferJS %} {%- enddeferJS %} {%- endcall %} {% endblock %} PK!թ:abilian/sbe/apps/communities/templates/community/home.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box, m_box_menu with context %} {% block header %}{% endblock %} {% block content %} {%- call m_box(title=_("My communities")) %} {%- set is_admin = current_user.has_role('admin') %}
{%- if my_communities %} {%- for community in my_communities %} {%- endfor %}
{{ community.name }}
{{ community.description }}
{{ community.membership_count }} - {{ community.document_count }} - {{ community.last_active_at | age }}
{% else %} {{ _("You're not a member of a community yet.") }} {% endif %} {% endcall %} {% endblock %} {% block sidebar %} {% if current_user.has_role("admin") %} {% call m_box_menu() %}
{{ _("Create new community") }}
{% endcall %} {% endif %} {% endblock %} PK!,_x&&<abilian/sbe/apps/communities/templates/community/macros.html{%- from "macros/user.html" import m_user_link, m_user_photo %} {% macro viewers_snapshot(viewers, label=None, limit=4) %} {% if not label %} {% set label = _("Read by") %} {% endif %} {% if viewers %} {% set nb_viewers = viewers|length %}
{{ label }} : {% for viewer in viewers %} {% if loop.index <= limit %} {{ m_user_link(viewer.user) }} {%- if loop.index < nb_viewers %},{% endif %} {% else %} {% if limit < nb_viewers %} {% if loop.last %} ... {% endif %} {% endif %} {% endif %} {% endfor %}
{% endif %} {% endmacro %} {% macro show_all_viewers(viewers, label=None) %} {% if not label %} {% set label = _("Read by") %} {% endif %} {% if not viewers %} {% set viewers = [] %} {% endif %} {% set nb_viewers=viewers|length %}

{{ label }} {{ nb_viewers }} {{ _("member") }} {%- if nb_viewers > 1 %}s{% endif %}

{% if viewers %} {% for viewer in viewers %}

{% call m_user_link(viewer.user) %} {{ m_user_photo(viewer.user, size=30) }} {% endcall %} {{ m_user_link(viewer.user) }} - {{ viewer.viewed_at | age(date_threshold='day') }}

{% endfor %} {% endif %} {% endmacro %} {% macro wizard_steps(steps,active_n) %}
{% endmacro %} PK!=abilian/sbe/apps/communities/templates/community/members.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% import "community/members_macros.html" as macros with context %} {% block memberscontent %}
{% call m_box_content(_("Members")) %} {%- if is_manager %} {%- endif %}

{% set table_id = uuid() %} {{ macros.thead() }} {% for user, m_id, role, last_activity_date in memberships %} {{ macros.member_row(user, m_id, role, last_activity_date) }} {% endfor %}
{%- deferJS %} {%- enddeferJS %}
{% endcall %}
{% endblock %} {% block sidebar %} {%- set actions = actions.for_category('members:menu') %} {%- if actions %} {% call m_box_menu() %} {% endcall %} {%- endif %} {% endblock %} {% macro add_member() %}

{{ _("Add a member") }}

{%- deferJS %} {%- enddeferJS %} {% endmacro %} PK!^CCDabilian/sbe/apps/communities/templates/community/members_macros.html{% import "community/members_std_macros.html" as std with context %} {% import "community/wizard_users_std_macros.html" as w_std with context %} {% macro thead() %} {{ std.std_thead() }} {% endmacro %} {% macro wizard_thead() %} {{ w_std.std_thead() }} {% endmacro %} {% macro table_config() %} {{ std.std_table_config() }} {% endmacro %} {% macro member_row(user, m_id, role, last_activity_date) %} {{ std.std_member_row(user, m_id, role, last_activity_date) }} {% endmacro %} {% macro wizard_member_row(user) %} {{ w_std.std_member_row(user) }} {% endmacro %} PK!ct}mmHabilian/sbe/apps/communities/templates/community/members_std_macros.html{# standard macros for the 'members' page. DO NOT OVERRIDE. Instead, you should override 'members_macros.html'. This allow to override only some of the macros without copying all others. #} {% from "macros/user.html" import m_user_link, m_user_photo %} {% macro std_thead() %} Last Name First Name {{ _('Name') }} {{ _('Posts') }} {# last activity seconds since epoch #} {{ _('Last activity in this community') }}
{{ _('Role') }}
{% if is_manager %} {{ _('Action') }} {% endif %} {% endmacro %} {% macro std_table_config() %} { "aoColumns": [ { "bVisible": false }, { "bVisible": false }, { "aDataSort": [0, 1], "asSorting": [ "asc", "desc" ] }, { "asSorting": [ "asc", "desc" ]}, { "bVisible": false }, { "aDataSort": [3], "asSorting": [ "asc", "desc" ] }, { "asSorting": [ "asc", "desc" ]} {%- if is_manager %} , { } {% endif %} ], "sPaginationType": "bootstrap", "bFilter": true, "bLengthChange": false } {% endmacro %} {% macro std_member_row(user, m_id, role, last_activity_date) %} {{ user.last_name }} {{ user.first_name }}
{% call m_user_link(user, css="media-object") %} {{ m_user_photo(user, size="32") }} {%- endcall %}
{%- if not user.can_login %} {%- endif %} {{ m_user_link(user) }}
({{ user.email }}) {%- if not user.can_login %}
{%- endif %}
{{ threads_count[user] }} {{ seconds_since_epoch(last_activity_date) }} {{ last_activity_date | age(add_direction=False, date_threshold='day') }} {%- if is_manager == False %} {{ _(role) }} {%- else %}
{{ csrf.field() }}
{{ csrf.field() }}
{% endif %} {% endmacro %} PK!-NMMGabilian/sbe/apps/communities/templates/community/wizard_add_emails.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% import "community/members_macros.html" as macros with context %} {%- from "community/macros.html" import wizard_steps -%} {% block memberscontent %} {% call m_box_content(_("Members Wizard")) %} {{ wizard_steps(steps, 1) }}



{{ _("Add new members") }}

{{ _("Insert or import new members into your community") }}





{{ _("Download CSV file model") }}


{{ _('Next step') }}
{{ csrf.field() }}
{{ csrf.field() }}

{%- deferJS %} {%- enddeferJS %}
{% endcall %} {% endblock %} PK!DnJabilian/sbe/apps/communities/templates/community/wizard_check_members.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% import "community/members_macros.html" as macros with context %} {%- from "community/macros.html" import wizard_steps -%} {% from "macros/user.html" import m_user_link, m_user_photo %} {% block memberscontent %} {% call m_box_content(_("Members Wizard")) %} {{ wizard_steps(steps,2) }}

{{ _("Existing and new members") }}

{{ _("Check if the user is already a member of the community and if he currently has an account on the platform") }}


{% if existing_members_objects %}
{{ _("The users below are already members of the community :") }}

{% for member in existing_members_objects %}

{% call m_user_link(member, css="media-object") %} {{ m_user_photo(member, size="32") }}{{ member }} ({{ member.email }}) {%- endcall %}

{% endfor %}
{% endif %}
{% set table_id = uuid() %}
{{ csrf.field() }}
{% endcall %} {% endblock %} PK!'I~!~!Iabilian/sbe/apps/communities/templates/community/wizard_new_accounts.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% import "community/members_macros.html" as macros with context %} {%- from "community/macros.html" import wizard_steps -%} {% from "macros/user.html" import m_user_link, m_user_photo %} {% block memberscontent %} {% call m_box_content(_("Members Wizard")) %} {{ wizard_steps(steps,3) }}

{{ _("New Users Accounts") }}

{{ _("The members below are not registered on the platform. Please create their accounts so that they can join the community.") }}


{{ _("Complete the information below to create a new user. They will receive a mail instruction to connect.") }}

{% set table_id = uuid() %} {% set nb_new_accounts = (new_accounts|length) %} {% if new_accounts %} {% for user in new_accounts %} {% endfor %} {% endif %}
Email First Name * Last Name * Role
{{ user.email }}
{{ _("Save") }}
{{ csrf.field() }}
{% endcall %} {% endblock %} {% block sidebar %} {%- set actions = actions.for_category('members:menu') %} {%- if actions %} {% call m_box_menu() %} {% endcall %} {%- endif %} {% endblock %} PK!`1aaMabilian/sbe/apps/communities/templates/community/wizard_users_std_macros.html{# standard macros for the 'members' page. DO NOT OVERRIDE. Instead, you should override 'members_macros.html'. This allow to override only some of the macros without copying all others. #} {% from "macros/user.html" import m_user_link, m_user_photo %} {% macro std_thead() %} Last Name First Name {{ _('Name') }} {# last activity seconds since epoch #} {{ _('Role') }} {% if is_manager %} {{ _('Action') }} {% endif %} {% endmacro %} {% macro std_table_config() %} { "aoColumns": [ { "bVisible": false }, { "bVisible": false }, { "aDataSort": [0, 1], "asSorting": [ "asc", "desc" ] }, { "bVisible": false }, { "aDataSort": [3], "asSorting": [ "asc", "desc" ] }, { "asSorting": [ "asc", "desc" ]} {%- if is_manager %} , { } {% endif %} ], "sPaginationType": "bootstrap", "bFilter": true, "bLengthChange": false } {% endmacro %} {% macro std_member_row(user, m_id, role, last_activity_date) %} {{ user.last_name }} {{ user.first_name }}
{% call m_user_link(user, css="media-object") %} {{ m_user_photo(user, size="32") }} {%- endcall %}
{%- if not user.can_login %} {%- endif %} {{ m_user_link(user) }}
({{ user.email }}) {%- if not user.can_login %}
{%- endif %}
{%- if is_manager == False %} {{ _(role) }} {%- else %}
{{ csrf.field() }}
{% endif %} {% endmacro %} PK!e.abilian/sbe/apps/communities/tests/__init__.py# coding=utf-8 PK!DF.abilian/sbe/apps/communities/tests/fixtures.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.core.models.subjects import User from pytest import fixture from ..models import READER, Community @fixture def community(db): community = Community(name="My Community") db.session.add(community) db.session.flush() return community @fixture def community1(db): community = Community(name="My Community") db.session.add(community) user = User(email="user_1@example.com", password="azerty", can_login=True) db.session.add(user) community.set_membership(user, READER) community.test_user = user db.session.flush() return community @fixture def community2(db): community = Community(name="Another Community") db.session.add(community) user = User(email="user_2@example.com", password="azerty", can_login=True) db.session.add(user) community.set_membership(user, READER) community.test_user = user db.session.flush() return community PK!#221abilian/sbe/apps/communities/tests/test_common.py# coding=utf-8 # Note: this test suite is using pytest instead of the unittest-based scaffolding # provided by SBE. Hopefully one day all of SBE will follow. from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime, timedelta import abilian.i18n import pytest from abilian.core.signals import activity from abilian.sbe.app import create_app from abilian.sbe.apps.communities.common import activity_time_format @pytest.fixture def app(): app = create_app() # We need some incantations here to make babel work in the test babel = abilian.i18n.babel babel.locale_selector_func = None yield app # Signals are globals and apparently need to be cleaned up. # At this point, only the "activity" signal seems to have a side effect. activity._clear_state() def test_activity_time_format(app): # We need the app context because of Babel. with app.app_context(): then = datetime(2017, 1, 1, 12, 0, 0) now = then + timedelta(0, 5) assert activity_time_format(then, now) == "5s" now = then + timedelta(0, 5 * 60) assert activity_time_format(then, now) == "5m" now = then + timedelta(0, 5 * 60 * 60) assert activity_time_format(then, now) == "5h" now = then + timedelta(1, 5) assert activity_time_format(then, now) == "1d" now = then + timedelta(60, 5) assert activity_time_format(then, now) == "Jan 1" now = then + timedelta(365 + 60, 5) assert activity_time_format(then, now) == "Jan 2017" PK!qv:abilian/sbe/apps/communities/tests/test_community_noweb.py# coding=utf-8 """Tests from test_community are currently refactored using pytest in this module.""" from __future__ import absolute_import, division, print_function, \ unicode_literals import pytest import six import sqlalchemy as sa from abilian.core.entities import Entity from abilian.core.models.subjects import User from abilian.testing.util import login from mock import MagicMock from pytest import fixture from sqlalchemy import orm from abilian.sbe.apps.documents.models import Folder from .. import signals, views from ..models import MEMBER, READER, Community, CommunityIdColumn, \ community_content @fixture def community(db_session): community = Community(name="My Community") db_session.add(community) db_session.commit() return community # # Actual tests # def test_instanciation(db): community = Community(name="My Community") assert isinstance(community.folder, Folder) # assert isinstance(community.group, Group) def test_default_view_kw(): # test exceptions are handled if passed an object with 'community' attribute # and no community_id in kwargs. and ValueError is properly raised if six.PY2: dummy = type(b"Dummy", (object,), {b"community": None})() else: dummy = type("Dummy", (object,), {"community": None})() with pytest.raises(ValueError) as exc_info: views.default_view_kw({}, dummy, "dummy", 1) if six.PY2: assert exc_info.value.message == "Cannot find community_id value" else: assert exc_info.value.args == ("Cannot find community_id value",) def test_default_url(app, community): url = app.default_view.url_for(community) assert url.endswith("/communities/my-community/") def test_can_recreate_with_same_name(community, db): name = community.name db.session.delete(community) db.session.commit() community = Community(name=name) db.session.add(community) # if community.folder was not deleted, this will raise IntegrityError. Test # passes if no exceptions is raised db.session.commit() def test_rename(community): NEW_NAME = "My new name" community.rename(NEW_NAME) assert community.name == NEW_NAME assert community.folder.name == NEW_NAME def test_auto_slug(community): assert community.slug == "my-community" def test_membership(community, db): user = User(email="user@example.com") memberships = community.memberships assert memberships == [] # setup signals testers with mocks. when_set = MagicMock() when_set.mock_add_spec(["__name__"]) # required for signals signals.membership_set.connect(when_set) when_removed = MagicMock() when_removed.mock_add_spec(["__name__"]) signals.membership_removed.connect(when_removed) # invalid role with pytest.raises(ValueError): community.set_membership(user, "dummy role name") assert not when_set.called assert not when_removed.called # simple member community.set_membership(user, "member") db.session.commit() memberships = community.memberships assert len(memberships) == 1 assert memberships[0].user == user assert memberships[0].role is MEMBER when_set.assert_called_once_with(community, is_new=True, membership=memberships[0]) assert not when_removed.called when_set.reset_mock() assert community.get_role(user) is MEMBER assert community.get_memberships() == [memberships[0]] assert community.get_memberships("member") == [memberships[0]] assert community.get_memberships("manager") == [] # change user role community.set_membership(user, "manager") db.session.commit() memberships = community.memberships assert len(memberships) == 1 assert memberships[0].user == user assert memberships[0].role == "manager" assert community.get_role(user) == "manager" when_set.assert_called_once_with(community, is_new=False, membership=memberships[0]) assert not when_removed.called when_set.reset_mock() # remove user membership = memberships[0] community.remove_membership(user) db.session.commit() memberships = community.memberships assert memberships == [] assert not when_set.called when_removed.assert_called_once_with(community, membership=membership) def test_folder_roles(community, db, app): user = User(email="user@example.com") folder = community.folder community.set_membership(user, "member") db.session.commit() security = app.services["security"] assert security.get_roles(user, folder) == ["reader"] # this tests a bug, where local roles whould disappear when setting # membership twice community.set_membership(user, "member") assert security.get_roles(user, folder) == ["reader"] def test_community_content_decorator(community, db): @community_content class CommunityContent(Entity): community_id = CommunityIdColumn() community = sa.orm.relation(Community, foreign_keys=[community_id]) sa.orm.configure_mappers() conn = db.session.connection() for table in sa.inspect(CommunityContent).tables: if not table.exists(conn): table.create(conn) cc = CommunityContent(name="my content", community=community) db.session.add(cc) db.session.flush() assert hasattr(cc, "community_slug") assert cc.community_slug == "my-community" assert cc.slug == "my-content" index_to = dict(CommunityContent.__indexation_args__["index_to"]) assert "community_slug" in index_to ########################################################################## def test_community_indexed(app, db, req_ctx): index_service = app.services["indexing"] index_service.start() security_service = app.services["security"] security_service.start() obj_types = (Community.entity_type,) user_no_community = User(email="no_community@example.com") db.session.add(user_no_community) community1 = Community(name="My Community") db.session.add(community1) community2 = Community(name="Other community") db.session.add(community2) user = User(email="user_1@example.com") db.session.add(user) community1.set_membership(user, READER) user_c2 = User(email="user_2@example.com") db.session.add(user_c2) community2.set_membership(user_c2, READER) db.session.commit() with login(user_no_community): res = index_service.search("community", object_types=obj_types) assert len(res) == 0 with login(user): res = index_service.search("community", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == community1.object_key with login(user_c2): res = index_service.search("community", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == community2.object_key def test_default_view_kw_with_hit(app, db, community, req_ctx): index_service = app.services["indexing"] index_service.start() security_service = app.services["security"] security_service.start() user = User(email="user_1@example.com") db.session.add(user) community.set_membership(user, READER) obj_types = (Community.entity_type,) with login(user): hit = index_service.search("community", object_types=obj_types)[0] kw = views.default_view_kw({}, hit, hit["object_type"], hit["id"]) assert kw == {"community_id": community.slug} PK!8abilian/sbe/apps/communities/tests/test_community_web.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from abilian.services.security import Admin from abilian.services.security.service import SecurityService from abilian.testing.util import client_login from flask import url_for from ..models import Community def test_index(community1, app, db, client, req_ctx): security_service = app.services["security"] # type: SecurityService security_service.start() user = community1.test_user with client_login(client, user): response = client.get(url_for("communities.index")) assert response.status_code == 200 def test_community_home(community1, community2, app, client, req_ctx): security_service = app.services["security"] # type: SecurityService security_service.start() url = app.default_view.url_for(community1) user1 = community1.test_user with client_login(client, user1): response = client.get(url) assert response.status_code == 302 expected_url = url_for( "wall.index", community_id=community1.slug, _external=True ) assert response.location == expected_url user2 = community2.test_user with client_login(client, user2): response = client.get(url) assert response.status_code == 403 def test_new(community1, app, client, db, req_ctx): security_service = app.services["security"] # type: SecurityService # security_service.use_cache = False security_service.start() user = community1.test_user with client_login(client, user): response = client.get(url_for("communities.new")) assert response.status_code == 403 security_service.grant_role(user, Admin) db.session.flush() with client_login(client, user): response = client.get(url_for("communities.new")) assert response.status_code == 200 def test_community_settings(app, client, community1, req_ctx): security_service = app.services["security"] # type: SecurityService security_service.start() url = url_for("communities.settings", community_id=community1.slug) user = community1.test_user with client_login(client, user): response = client.get(url) assert response.status_code == 403 app.services["security"].grant_role(user, Admin) response = client.get(url) assert response.status_code == 200 data = { "__action": "edit", "name": "edited community", "description": "my community", "linked_group": "", "type": "participative", } response = client.post(url, data=data, follow_redirects=True) assert response.status_code == 200 assert "edited community" in response.get_data(as_text=True) def test_members(app, client, db, community1, community2, req_ctx): security_service = app.services["security"] # type: SecurityService security_service.start() user1 = community1.test_user user2 = community2.test_user with client_login(client, user1): url = url_for("communities.members", community_id=community1.slug) response = client.get(url) assert response.status_code == 200 # test add user data = {"action": "add-user-role", "user": user2.id} response = client.post(url, data=data) assert response.status_code == 403 security_service.grant_role(user1, Admin) data = {"action": "add-user-role", "user": user2.id, "role": "member"} response = client.post(url, data=data, follow_redirects=True) assert response.status_code == 200 membership = [m for m in community1.memberships if m.user == user2][0] assert membership.role == "member" data["action"] = "set-user-role" data["role"] = "manager" response = client.post(url, data=data, follow_redirects=True) assert response.status_code == 200 db.session.expire(membership) assert membership.role == "manager" # Community.query.session is not self.db.session, but web app # session. community = Community.query.get(community1.id) assert user2 in community.members # test delete data = { "action": "delete", "user": user2.id, "membership": [m.id for m in community1.memberships if m.user == user2][0], } response = client.post(url, data=data, follow_redirects=True) assert response.status_code == 200 assert user2 not in community.members PK!n9ff1abilian/sbe/apps/communities/tests/test_wizard.py# coding=utf-8 # Note: this test suite is using pytest instead of the unittest-based scaffolding # provided by SBE. Hopefully one day all of SBE will follow. from __future__ import absolute_import, print_function, unicode_literals from tempfile import NamedTemporaryFile import pytest from abilian.core.models.subjects import User from flask import g from abilian.sbe.apps.communities.models import READER, Community from abilian.sbe.apps.communities.views.wizard import wizard_extract_data, \ wizard_read_csv @pytest.fixture def csv_file(): # create a tmp csv file csv = NamedTemporaryFile("w+", suffix=".csv", prefix="tmp_", delete=False) csv.write("user_1@example.com;userone;userone;manager\n") csv.write("user_2@example.com;usertwo;usertwo;member\n") csv.write("user_7@example.com;userseven;userseven;member\n") # writing a wrong line csv.write("user1@example.com;userthree;userthree\n") csv.write("example.com;example;userfour;member\n") csv.seek(0) csv.filename = csv.name return csv def test_wizard_read_csv(csv_file): wizard_read = wizard_read_csv(csv_file) assert wizard_read == [ { "first_name": "userone", "last_name": "userone", "role": "manager", "email": "user_1@example.com", }, { "first_name": "usertwo", "last_name": "usertwo", "role": "member", "email": "user_2@example.com", }, { "first_name": "userseven", "last_name": "userseven", "role": "member", "email": "user_7@example.com", }, ] def test_wizard_extract_data(db, csv_file): session = db.session community = Community(name="Hp") g.community = community user1 = User(email="user_1@example.com") user2 = User(email="user_2@example.com") user3 = User(email="user_3@example.com") new_emails = [ "user_1@example.com", "user_2@example.com", "user_3@example.com", "user_4@example.com", "user_5@example.com", ] # creating community session.add(community) # creating users session.add(user1) session.add(user2) session.add(user3) session.flush() # add user1 to the community community.set_membership(user1, READER) session.flush() # check wizard function in case of email list existing_accounts_objects, existing_members_objects, accounts_list = wizard_extract_data( new_emails ) assert set(existing_accounts_objects) == {user2, user3} assert existing_members_objects == [user1] def sorter(x): return x["email"] assert sorted(accounts_list, key=sorter) == sorted( [ { "status": "existing", "first_name": None, "last_name": None, "role": "member", "email": "user_2@example.com", }, { "status": "existing", "first_name": None, "last_name": None, "role": "member", "email": "user_3@example.com", }, { "status": "new", "first_name": "", "last_name": "", "role": "member", "email": "user_5@example.com", }, { "status": "new", "first_name": "", "last_name": "", "role": "member", "email": "user_4@example.com", }, ], key=sorter, ) # check wizard function in case of csv file existing_accounts_objects, existing_members_objects, accounts_list = wizard_extract_data( wizard_read_csv(csv_file), is_csv=True ) assert existing_accounts_objects == { "csv_roles": { "user_1@example.com": "manager", "user_2@example.com": "member", "user_7@example.com": "member", }, "account_objects": [user2], } assert existing_members_objects == [user1] assert sorted(accounts_list, key=sorter) == sorted( [ { "status": "existing", "first_name": None, "last_name": None, "role": "member", "email": "user_2@example.com", }, { "status": "new", "first_name": "userseven", "last_name": "userseven", "role": "member", "email": "user_7@example.com", }, ], key=sorter, ) PK!4].abilian/sbe/apps/communities/views/__init__.py# coding=utf-8 from __future__ import absolute_import from . import views, wizard # noqa from .views import BaseCommunityView, communities, default_view_kw, tab # noqa PK!bu5abilian/sbe/apps/communities/views/data/community.pngPNG  IHDR\rf pHYs  IDATxyU}g*v amE)0a_U9`Y*RA+T( @0 [ d@!ɷ $;sgy3d|r;"H: )D2ɘ@$c )D2ɘ@$c )D2ɘ@$c )D2ɘ@$c K]̆llV,Y)`OkZi陀6>TGk}zڤ==̶$t#5;HmJ (z|8Nskچ4Hlo'& m9XwUCۓ.@ǙDwi nSj#23.>q)_pH蠢_eEA{:?)fv~"dt1fv$p}:ԅ):6H]r ϥ.DGr k"dQA8I] NS3[ |XL#LJy$)E̬x0 &; 0?/5:r4p nfn_Ķ:t~C,/ٚ(`3_}F%k_PoYm6-~eTjm0صk=0G"rv[6!Eԅ4`0m.̎#j> x:rmtjfv 8pq;?Q#ef}fv)av_ºO..Nf6`7:p]R\ *)}3MEZ=p(ŗc@Sk(]kw~z7hC[W}-g:>puJ]@LfvkcƢ@DhYdD0PV.yXQ2 N(0& ~df+Tm(Pu(^멾)H>잺%lCUI0U3Qq$7SRPUqwu,frvふ+QmRA/uPO.dfƨ[>/u!ez@s3걮@ n5Kr 6a%`ET3v`f;a/>Zv*>dͺP@;uܕty,b€.8;SPDma+V +۔. $h>D.C]JtaWGҹu0g.".祟]%й* h6?KSIF2U`Nu[Kr;ߤcw._XRFcVv*Pz Br P8ݩu00ղ+V *۔) 'צcqSө mʓ 4`z"1Pc/A(W _VX). q@gE8G/ P]󽶚ܗ.Xϻ=y%mTpU:>(Vw!sˀRױ/5b2mǽ>U_Yq:]:?(́54*+Wz/@4)#ν2c+h~k >6xԣvkyiUt+oUi&:`W7 [ѮSžY4򛁊?Dx%tO]DӊŽZD^( <ĥ,0!Ti$ʫρ{H9%#3.}{F~ROGyr?@ķ-LΝgyFǣܜyźG9w~w |6u1:?9 )&v/o #"{S =󛯶Dݣ_]Fwr۔7i~ӏ[v`h(w?pEnpqĘ",հAu8d*pp_Ÿap^U-%-%bv$1^m]j<Xj#fg(}\°oXIqnK`-Rm>v35oYd3o<i$XG}00C206`w,Cbf[?;xz〛ܽgڲHx:27Z \BԆȪ 4_wW^2˩loTfQmDs o} LM5t=Y̾DSke N]Hn 2?"ܫFv5pR^#h#@CAR ecbeT3v&߀/޵,k&ff_hzuFy;O}cX2G5SݗI8,{0]1Uԟk]}bf >weBݧϾ/dk|:E#j޹V \My< b_Hy\nF *фanj YNvJ >cWk]&nNZ^75R&v߶uu~фw?_B¼ad^_U`1S./#HkZ"O"#m|B®NxxK`0V~WPb 6@I bX9R;9o:VF.{eimyɉijcZh>tX, {߆} kؚNN/GQ>NDPdM 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H"SdL 1H".BDXz9u+ hih }hI K]@ =ܜи>`07u%-EHt~[ ^&wRW"_uw18?u-r]<"`FjZbpe"d)OFlC18 8 GmSunQ:W# Xb$|<GnwJr;tGOI~Xj(C@>ﭡDn9wju-VW. ×XՀy-]]l\K8MY\nF,; pCZԵc;ׂ~]}bF >sepO݊/_f6 l\8Jr"T1o6$܃#af}v$g;uCPj?/wSJۤEHg=:u0&m26Hw礮c-Up0u6w>uK)8]6j0pR"g|hsE_,H@w ؟v]_@w ž4oVy(?~^{{pH k!E߫$\wc9D>Gۗ \w?p4P&~>+Fb4df7;+F[t(}RE fޮp``HyID4Q޹ubYsvdL 1@{I]@DFI5#uY6@{s RAPݩ d,mĚ0bԵT47M],ZOqC:"Y>m<[ԅȲŊIMK]G󷛎ZVK]MvvStmE[FZi?N:ݟЍ2Fw:̶nH]rLv'S"#):n,i>EGef[_!") def view(): ... """ is_community = obj_type == Community.entity_type community_id = kw.get("community_id") if is_community or community_id is None: # when it's a community, default_view sets community_id to 'id', we want to # override with the slug value. if obj: if isinstance(obj, (Hit, dict)): community_id = obj.get("slug" if is_community else "community_slug") elif is_community: community_id = obj.slug elif community_id is None and hasattr(obj, "community"): try: community_id = obj.community.slug except AttributeError: pass if community_id is not None: kw["community_id"] = community_id else: raise ValueError("Cannot find community_id value") return kw # # Routes # @route("/") @login_required def index(): query = Community.query sort_order = request.args.get("sort", "").strip() if not sort_order: sort_order = session.get("sort_communities_order", "alpha") if sort_order == "activity": query = query.order_by(Community.last_active_at.desc()) else: query = query.order_by(Community.name) session["sort_communities_order"] = sort_order if not current_user.has_role("admin"): # Filter with permissions query = query.join(Membership).filter(Membership.user == current_user) ctx = {"my_communities": query.all(), "sort_order": sort_order} return render_template("community/home.html", **ctx) @route("//") @views.default_view(communities, Community, "community_id", kw_func=default_view_kw) def community(): return redirect(url_for("wall.index", community_id=g.community.slug)) @route("/json2") def list_json2(): """JSON endpoint, used for filling select boxes dynamically.""" # TODO: make generic ? args = request.args q = args.get("q").replace("%", " ") if not q or len(q) < 2: raise BadRequest() query = ( db.session.query(Community.id, Community.name) .filter(Community.name.ilike("%" + q + "%")) .distinct() .order_by(Community.name) .limit(50) ) query_result = query.all() result = {"results": [{"id": r[0], "text": r[1]} for r in query_result]} return jsonify(result) # edit views class BaseCommunityView(object): Model = Community pk = "community_id" Form = CommunityForm base_template = "community/_base.html" decorators = [require_admin] def init_object(self, args, kwargs): self.obj = g.community._model return args, kwargs def view_url(self): return url_for(self.view_endpoint, community_id=self.obj.slug) def get_form_kwargs(self): kwargs = super(BaseCommunityView, self).get_form_kwargs() image = self.obj.image if image and "community" in g: setattr(image, "url", image_url(self.obj, s=500)) kwargs["image"] = image return kwargs class CommunityEdit(BaseCommunityView, views.ObjectEdit): template = "community/edit.html" title = _l("Edit community") decorators = views.ObjectEdit.decorators + (require_admin, tab("settings")) def breadcrumb(self): return BreadcrumbItem( label=_("Settings"), icon="cog", url=Endpoint("communities.settings", community_id=g.community.slug), ) def before_populate_obj(self): form = self.form name = form.name.data if name != self.obj.name: self.obj.rename(name) del form.name type = form.type.data if type != self.obj.type: self.obj.type = type self.obj.update_roles_on_folder() del form.type self.linked_group = form.linked_group.data or None if self.linked_group: self.linked_group = Group.query.get(int(self.linked_group)) del form.linked_group def after_populate_obj(self): self.obj.group = self.linked_group add_url( "//settings", view_func=CommunityEdit.as_view( "settings", view_endpoint=".community", message_success=_l("Community settings saved successfully."), ), ) class CommunityCreate(views.ObjectCreate, CommunityEdit): title = _l("Create community") decorators = views.ObjectCreate.decorators + (require_admin,) template = views.ObjectCreate.template base_template = views.ObjectCreate.base_template def breadcrumb(self): return BreadcrumbItem(label=_("Create new community")) def message_success(self): return _("Community %(name)s created successfully", name=self.obj.name) add_url("/new", view_func=CommunityCreate.as_view("new", view_endpoint=".community")) class CommunityDelete(BaseCommunityView, views.ObjectDelete): get_form_kwargs = views.ObjectDelete.get_form_kwargs add_url( "//destroy", methods=["POST"], view_func=CommunityDelete.as_view( "delete", message_success=_l("Community destroyed.") ), ) # Community Image _DEFAULT_IMAGE = Path(__file__).parent / "data" / "community.png" _DEFAULT_IMAGE_MD5 = hashlib.md5(_DEFAULT_IMAGE.open("rb").read()).hexdigest() route("/_default_image")( image_views.StaticImageView.as_view( "community_default_image", set_expire=True, image=_DEFAULT_IMAGE ) ) class CommunityImageView(image_views.BlobView): id_arg = "blob_id" def prepare_args(self, args, kwargs): community = g.community if not community: raise NotFound() kwargs[self.id_arg] = community.image.id # image = open(join(dirname(__file__), "data", "community.png"), 'rb') return super(CommunityImageView, self).prepare_args(args, kwargs) image = CommunityImageView.as_view("image", max_size=500, set_expire=True) route("//image")(image) def image_url(community, **kwargs): """Return proper URL for image url.""" if not community or not community.image: kwargs["md5"] = _DEFAULT_IMAGE_MD5 return url_for("communities.community_default_image", **kwargs) kwargs["community_id"] = community.slug kwargs["md5"] = community.image.md5 return url_for("communities.image", **kwargs) def _members_query(): """Helper used in members views.""" last_activity_date = sa.sql.functions.max(ActivityEntry.happened_at).label( "last_activity_date" ) memberships = ( User.query.options(sa.orm.undefer("photo")) .join(Membership) .outerjoin( ActivityEntry, sa.sql.and_( ActivityEntry.actor_id == User.id, ActivityEntry.target_id == Membership.community_id, ), ) .filter(Membership.community == g.community, User.can_login == True) .add_columns(Membership.id, Membership.role, last_activity_date) .group_by(User, Membership.id, Membership.role) .order_by(User.last_name.asc(), User.first_name.asc()) ) return memberships @route("//members") @tab("members") def members(): g.breadcrumb.append( BreadcrumbItem( label=_("Members"), url=Endpoint("communities.members", community_id=g.community.slug), ) ) memberships = _members_query().all() community_threads_users = [thread.creator for thread in g.community.threads] threads_count = Counter(community_threads_users) ctx = { "seconds_since_epoch": seconds_since_epoch, "is_manager": is_manager(user=current_user), "memberships": memberships, "threads_count": threads_count, } return render_template("community/members.html", **ctx) @route("//members", methods=["POST"]) @csrf.protect @require_manage def members_post(): community = g.community._model action = request.form.get("action") user_id = request.form.get("user") if not user_id: flash(_("You must provide a user."), "error") return redirect(url_for(".members", community_id=community.slug)) user_id = int(user_id) user = User.query.get(user_id) if action in ("add-user-role", "set-user-role"): role = request.form.get("role").lower() community.set_membership(user, role) if action == "add-user-role": app = unwrap(current_app) activity.send(app, actor=user, verb="join", object=community) db.session.commit() return redirect(url_for(".members", community_id=community.slug)) elif action == "delete": membership_id = int(request.form["membership"]) membership = Membership.query.get(membership_id) if membership.user_id != user_id: raise InternalServerError() community.remove_membership(user) app = unwrap(current_app) activity.send(app, actor=user, verb="leave", object=community) db.session.commit() return redirect(url_for(".members", community_id=community.slug)) else: raise BadRequest("Unknown action: {}".format(repr(action))) MEMBERS_EXPORT_HEADERS = [ _l("Name"), _l("email"), _l("Last activity in this community"), _l("Role"), ] MEMBERS_EXPORT_ATTRS = ["User", "User.email", "last_activity_date", "role"] HEADER_FONT = openpyxl.styles.Font(bold=True) HEADER_ALIGN = openpyxl.styles.Alignment( horizontal="center", vertical="top", wrapText=True ) XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @route("//members/excel") @tab("members") def members_excel_export(): community = g.community attributes = [attrgetter(a) for a in MEMBERS_EXPORT_ATTRS] BaseModel = db.Model wb = openpyxl.Workbook() if wb.worksheets: wb.remove_sheet(wb.active) ws_title = _("%(community)s members", community=community.name) ws_title = ws_title.strip() if len(ws_title) > 31: # sheet title cannot exceed 31 char. max length ws_title = ws_title[:30] + "…" ws = wb.create_sheet(title=ws_title) row = 0 cells = [] cols_width = [] for _col, label in enumerate(MEMBERS_EXPORT_HEADERS, 1): value = text_type(label) cell = WriteOnlyCell(ws, value=value) cell.font = HEADER_FONT cell.alignment = HEADER_ALIGN cells.append(cell) cols_width.append(len(value) + 1) ws.append(cells) for membership_info in _members_query().all(): row += 1 cells = [] for col, getter in enumerate(attributes): value = None try: value = getter(membership_info) except AttributeError: pass if isinstance(value, (BaseModel, Role)): value = text_type(value) cell = WriteOnlyCell(ws, value=value) cells.append(value) # estimate width value = text_type(cell.value) width = max(len(l) for l in value.split("\n")) + 1 cols_width[col] = max(width, cols_width[col]) ws.append(cells) # adjust columns width MIN_WIDTH = 3 MAX_WIDTH = openpyxl.utils.units.BASE_COL_WIDTH * 4 for idx, width in enumerate(cols_width, 1): letter = openpyxl.utils.get_column_letter(idx) width = min(max(width, MIN_WIDTH), MAX_WIDTH) ws.column_dimensions[letter].width = width fd = BytesIO() wb.save(fd) fd.seek(0) response = current_app.response_class(fd, mimetype=XLSX_MIME) filename = "{}-members-{}.xlsx".format( community.slug, strftime("%d:%m:%Y-%H:%M:%S", gmtime()) ) response.headers["content-disposition"] = 'attachment;filename="{}"'.format( filename ) return response # # Hack to redirect from urls used by the search engine. # @route("/doc/") def doc(doc_id): doc = Document.query.get(doc_id) if doc is None: raise NotFound() folder = doc.parent while True: parent = folder.parent if parent.is_root_folder: break folder = parent target_community = Community.query.filter(Community.folder_id == folder.id).one() location = url_for( "documents.document_view", community_id=target_community.slug, doc_id=doc.id ) return redirect(location) PK!0$$,abilian/sbe/apps/communities/views/wizard.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import csv import json from os.path import splitext from abilian.core.extensions import db from abilian.core.models.subjects import User from abilian.core.signals import activity from abilian.core.util import unwrap from abilian.i18n import _ from abilian.services.auth.views import send_reset_password_instructions from abilian.web import csrf from abilian.web.action import Endpoint from abilian.web.nav import BreadcrumbItem from flask import current_app, flash, g, redirect, render_template, request, \ url_for from six import PY2 from validate_email import validate_email from .views import route, tab def wizard_extract_data(emails, is_csv=False): """Filter data and extract existing accounts, existing members and new emails.""" if is_csv: csv_data = emails existing_account_csv_roles = {user["email"]: user["role"] for user in csv_data} emails = [user["email"] for user in emails] emails = [email.strip() for email in emails] already_member_emails = [ member.email for member in g.community.members if member.email in emails ] not_member_emails = set(emails) - set(already_member_emails) existing_members_objects = [ user for user in g.community.members if user.email in already_member_emails ] existing_accounts_objects = User.query.filter( User.email.in_(not_member_emails) ).all() existing_account_emails = [user.email for user in existing_accounts_objects] emails_without_account = set(not_member_emails) - set(existing_account_emails) accounts_list = [] for user in existing_accounts_objects: account = {} account["email"] = user.email account["first_name"] = user.first_name account["last_name"] = user.last_name account["role"] = existing_account_csv_roles[user.email] if is_csv else "member" account["status"] = "existing" accounts_list.append(account) if is_csv: emails_without_account = [ csv_account for csv_account in csv_data if csv_account["email"] in emails_without_account ] existing_accounts_objects = { "account_objects": existing_accounts_objects, "csv_roles": existing_account_csv_roles, } for csv_account in emails_without_account: account = {} account["email"] = csv_account["email"] account["first_name"] = csv_account["first_name"] account["last_name"] = csv_account["last_name"] account["role"] = csv_account["role"] account["status"] = "new" accounts_list.append(account) else: for email in emails_without_account: account = {} account["email"] = email account["first_name"] = "" account["last_name"] = "" account["role"] = "member" account["status"] = "new" accounts_list.append(account) return existing_accounts_objects, existing_members_objects, accounts_list def wizard_read_csv(csv_file): """Read new members data from CSV file.""" file_extension = splitext(csv_file.filename)[-1] if file_extension != ".csv": return [] if PY2: contents = csv.reader(csv_file, delimiter=b";") else: contents = csv.reader(csv_file, delimiter=";") new_accounts = [] for row in contents: account = {} if len(row) != 4: continue email = row[0].strip() first_name = row[1].strip() last_name = row[2].strip() role = row[3].strip() if not validate_email(email): continue if role.lower() not in ["manager", "member"]: continue account["email"] = email account["first_name"] = first_name account["last_name"] = last_name account["role"] = role new_accounts.append(account) return new_accounts @route("//members/wizard/step1") @tab("members") def wizard_data_insertion(): """Insert new members data into the community via emails or CSV file.""" g.breadcrumb.append( BreadcrumbItem( label=_("Members"), url=Endpoint("communities.members", community_id=g.community.slug), ) ) return render_template("community/wizard_add_emails.html") @route("//members/wizard/step2", methods=["GET", "POST"]) @csrf.protect @tab("members") def wizard_check_data(): """Filter and detect existing members, existing accounts and new emails.""" if request.method == "GET": return redirect(url_for(".members", community_id=g.community.slug)) g.breadcrumb.append( BreadcrumbItem( label=_("Members"), url=Endpoint("communities.members", community_id=g.community.slug), ) ) is_csv = False if request.form.get("wizard-emails"): wizard_emails = request.form.get("wizard-emails").split(",") existing_accounts_object, existing_members_objects, final_email_list = wizard_extract_data( wizard_emails ) final_email_list_json = json.dumps(final_email_list) else: is_csv = True accounts_data = wizard_read_csv(request.files["csv_file"]) if not accounts_data: flash(_("To add new members, please follow the CSV file model."), "warning") return redirect( url_for(".wizard_data_insertion", community_id=g.community.slug) ) existing_accounts, existing_members_objects, final_email_list = wizard_extract_data( accounts_data, is_csv=True ) existing_accounts_object = existing_accounts["account_objects"] existing_accounts_csv_roles = existing_accounts["csv_roles"] final_email_list_json = json.dumps(final_email_list) if not final_email_list: flash(_("No new members were found"), "warning") return redirect( url_for(".wizard_data_insertion", community_id=g.community.slug) ) ctx = { "existing_accounts_object": existing_accounts_object, "csv_roles": existing_accounts_csv_roles if is_csv else False, "wizard_emails": final_email_list_json, "existing_members_objects": existing_members_objects, } return render_template("community/wizard_check_members.html", **ctx) @route("//members/wizard/step3", methods=["GET", "POST"]) @csrf.protect @tab("members") def wizard_new_accounts(): """Complete new emails information.""" if request.method == "GET": return redirect(url_for(".members", community_id=g.community.slug)) g.breadcrumb.append( BreadcrumbItem( label=_("Members"), url=Endpoint("communities.members", community_id=g.community.slug), ) ) wizard_emails = request.form.get("wizard-emails") wizard_accounts = json.loads(wizard_emails) wizard_existing_account = {} new_accounts = [] for user in wizard_accounts: if user["status"] == "existing": wizard_existing_account[user["email"]] = user["role"] elif user["status"] == "new": new_accounts.append(user) existing_account = json.dumps(wizard_existing_account) return render_template( "community/wizard_new_accounts.html", existing_account=existing_account, new_accounts=new_accounts, ) @route("//members/wizard/complete", methods=["POST"]) @csrf.protect def wizard_saving(): """Automatically add existing accounts to the current community. Create accounts for new emails, add them to the community and send them a password reset email. """ community = g.community._model existing_accounts = request.form.get("existing_account") existing_accounts = json.loads(existing_accounts) new_accounts = request.form.get("new_accounts") new_accounts = json.loads(new_accounts) if not (existing_accounts or new_accounts): flash(_("No new members were found"), "warning") return redirect(url_for(".members", community_id=g.community.slug)) if existing_accounts: for email, role in existing_accounts.items(): user = User.query.filter(User.email == email).first() community.set_membership(user, role) app = unwrap(current_app) activity.send(app, actor=user, verb="join", object=community) db.session.commit() if new_accounts: for account in new_accounts: email = account["email"] first_name = account["first_name"] last_name = account["last_name"] role = account["role"] user = User( email=email, last_name=last_name, first_name=first_name, can_login=True ) db.session.add(user) community.set_membership(user, role) app = unwrap(current_app) activity.send(app, actor=user, verb="join", object=community) db.session.commit() send_reset_password_instructions(user) flash(_("New members added successfully"), "success") return redirect(url_for(".members", community_id=community.slug)) PK!*ww&abilian/sbe/apps/documents/__init__.py# coding=utf-8 """Folders / Documents module.""" from __future__ import absolute_import from abilian.sbe.extension import sbe def register_plugin(app): sbe.init_app(app) from .views import blueprint from .models import setup_listener from .commands import manager # pylint: disable=bad-python3-import from . import signals # noqa from . import lock app.register_blueprint(blueprint) setup_listener() # set default lock lifetime app.config.setdefault("SBE_LOCK_LIFETIME", lock.DEFAULT_LIFETIME) if app.script_manager: app.script_manager.add_command("documents", manager) PK!Mʵ&&%abilian/sbe/apps/documents/actions.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from typing import Any, Dict from abilian.i18n import _l from abilian.services.security import MANAGE, WRITE, security from abilian.web.action import Action, FAIcon, ModalActionMixin, actions from flask import g from flask import url_for as url_for_orig from flask_login import current_user from abilian.sbe.apps.communities.security import is_manager from .repository import repository def url_for(endpoint, **kw): return url_for_orig(endpoint, community_id=g.community.slug, **kw) class CmisContentAction(Action): sbe_type = None # type: str permission = None # type: str def __init__(self, *args, **kwargs): if "permission" in kwargs: self.permission = kwargs.pop("permission") Action.__init__(self, *args, **kwargs) def pre_condition(self, ctx): # type: (Dict[str, Any]) -> bool obj = ctx["object"] ok = obj.sbe_type == self.sbe_type if ok and self.permission is not None: ok = self.has_access(self.permission, obj) return ok def has_access(self, permission, obj): # type: (str, Any) -> bool return repository.has_permission(current_user, permission, obj) class BaseFolderAction(CmisContentAction): """Apply to all folders, including root folder.""" sbe_type = "cmis:folder" class FolderButtonAction(BaseFolderAction): _std_template_string = ( '" ) _modal_template_string = ( '' "{{ action.icon }}" ) def __init__(self, *args, **kwargs): self.modal = False if "modal" in kwargs: self.modal = kwargs.pop("modal") css_class = kwargs.pop("css_class", "btn-default") self.CSS_CLASS = self.CSS_CLASS + " " + css_class BaseFolderAction.__init__(self, *args, **kwargs) @property def template_string(self): return self._modal_template_string if self.modal else self._std_template_string class FolderAction(BaseFolderAction): """Apply to all folders except root folder.""" sbe_type = "cmis:folder" def pre_condition(self, ctx): return ( super(FolderAction, self).pre_condition(ctx) and ctx["object"] is not repository.root_folder ) class FolderPermisionsAction(BaseFolderAction): """Apply to all folders except root folder.""" sbe_type = "cmis:folder" def pre_condition(self, ctx): return ( super(BaseFolderAction, self).pre_condition(ctx) and ctx["object"].depth > 1 ) class FolderModalAction(ModalActionMixin, FolderAction): pass class DocumentAction(CmisContentAction): sbe_type = "cmis:document" class DocumentModalAction(ModalActionMixin, DocumentAction): pass class RootFolderAction(CmisContentAction): """Apply only for root folder.""" def pre_condition(self, ctx): return ctx["object"] is repository.root_folder _checkin_template_action = """
{{ csrf.field() }}
""" _lock_template_action = """
{{ csrf.field() }}
""" _actions = ( # Folder listing action buttons ########## FolderButtonAction( "documents:folder-listing", "download", _l("Download"), icon="download" ), FolderButtonAction( "documents:folder-listing", "move-files", _l("Move to another folder"), icon="move", url="#modal-move-files", modal=True, permission=WRITE, ), FolderButtonAction( "documents:folder-listing", "delete", _l("Delete"), permission=WRITE, icon="trash", css_class="btn-danger", ), FolderButtonAction( "documents:folder-listing", "change-owner", _l("Change owner"), icon="user", url="#modal-change-owner", modal=True, permission=MANAGE, ), # Folder left bar actions ########## # view RootFolderAction( "documents:content", "view", _l("List content"), icon="list", condition=(lambda ctx: security.has_role(current_user, "admin")), url=lambda ctx: url_for(".folder_view", folder_id=ctx["object"].id), ), # view FolderAction( "documents:content", "view", _l("List content"), icon="list", url=lambda ctx: url_for(".folder_view", folder_id=ctx["object"].id), ), # Descendants FolderAction( "documents:content", "descendants", _l("View descendants"), icon=FAIcon("code-fork fa-rotate-90"), url=lambda ctx: url_for(".descendants_view", folder_id=ctx["object"].id), ), # upload FolderModalAction( "documents:content", "upload_files", _l("Upload file(s)"), icon="upload", url="#modal-upload-files", permission=WRITE, ), # edit FolderModalAction( "documents:content", "edit", _l("Edit properties"), icon="pencil", url="#modal-edit", permission=WRITE, ), # new folder FolderModalAction( "documents:content", "new_folder", _l("New folder"), icon="plus", url="#modal-new-folder", permission=WRITE, ), # # members # FolderAction( # 'documents:content', 'members', _l('Members'), icon='user', # url=lambda ctx: url_for(".members", folder_id=ctx['object'].id)), # permissions FolderPermisionsAction( "documents:content", "permissions", _l("Permissions"), icon="lock", url=lambda ctx: url_for(".permissions", folder_id=ctx["object"].id), permission=MANAGE, ), # Document actions ########## # View / preview in browser DocumentAction( "documents:content", "preview", _l("View in browser"), icon="eye-open", url=lambda ctx: url_for(".document_preview", doc_id=ctx["object"].id), condition=( lambda ctx: ctx["object"].antivirus_ok and ctx["object"].content_type in ("text/html", "text/plain", "application/pdf") ), ), # viewers DocumentModalAction( "documents:content", "document_viewers", _l("Viewers list"), icon="user", condition=lambda ctx: is_manager(context=ctx), url=lambda ctx: url_for(".document_viewers", doc_id=ctx["object"].id), ), # edit DocumentModalAction( "documents:content", "edit", _l("Edit properties"), icon="pencil", url="#modal-edit", permission=WRITE, ), # Checkin / Checkout DocumentAction( "documents:content", "checkout", _l("Checkout (Download for edit)"), icon="download", url=lambda ctx: url_for(".checkin_checkout", doc_id=ctx["object"].id), condition=lambda ctx: ctx["object"].lock is None, template_string=_checkin_template_action, ), # DocumentAction( # 'documents:content', 'lock', _l(u'Lock for edit'), # icon='lock', # url=lambda ctx: url_for('.checkin_checkout', doc_id=ctx['object'].id), # condition=lambda ctx: ctx['object'].lock is None, # template_string=_lock_template_action, # ), DocumentAction( "documents:content", "unlock", _l("Unlock"), icon=FAIcon("unlock"), url=lambda ctx: url_for(".checkin_checkout", doc_id=ctx["object"].id), condition=lambda ctx: ctx["object"].lock is not None, template_string=_lock_template_action, ), # upload-new / checkin DocumentModalAction( "documents:content", "upload", _l("Upload new version"), icon="upload", url="#modal-upload-new-version", # either not locked, either user is owner condition=lambda ctx: not ctx["object"].lock or ctx["object"].lock.is_owner(), permission=WRITE, ), # send by email DocumentModalAction( "documents:content", "send_by_email", _l("Send by email"), icon="envelope", url="#modal-send-by-email", condition=lambda ctx: ctx["object"].antivirus_ok, permission=WRITE, ), # delete DocumentModalAction( "documents:content", "delete", _l("Delete"), icon="trash", url="#modal-delete", permission=WRITE, ), # refresh preview DocumentAction( "documents:content", "refresh_preview", _l("Refresh preview"), icon="refresh", url=lambda ctx: url_for(".refresh_preview", doc_id=ctx["object"].id), condition=lambda ctx: ctx["object"].antivirus_ok, permission=MANAGE, ), ) def register_actions(state): if not actions.installed(state.app): return with state.app.app_context(): actions.register(*_actions) PK!|+abilian/sbe/apps/documents/cmis/__init__.py# coding=utf-8 """CMIS (REST aka AtomPub bind) interface to the document repository.""" from __future__ import absolute_import from . import atompub # noqa PK!TގD.D.*abilian/sbe/apps/documents/cmis/atompub.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.core.extensions import db from flask import Blueprint, Response, make_response, render_template, request from sqlalchemy.orm.exc import NoResultFound from werkzeug.exceptions import NotFound, Unauthorized from abilian.sbe.apps.documents.repository import repository from .parser import Entry from .renderer import Feed, to_xml # # Constants # ROOT = "http://localhost:5000/cmis/atompub" # MIME TYPES (cf. section 3.3 of the CMIS specs) MIME_TYPE_ATOM_FEED = "application/atom+xml;type=feed" MIME_TYPE_ATOM_ENTRY = "application/atom+xml;type=entry" MIME_TYPE_ATOM_SERVICE = "application/atomsvc+xml" MIME_TYPE_CMIS_ATOM = "application/cmisatom+xml" MIME_TYPE_CMIS_QUERY = "application/cmisquery+xml" MIME_TYPE_CMIS_ALLOWABLE_ACTIONS = "application/cmisallowableactions+xml" MIME_TYPE_CMIS_TREE = "application/cmistree+xml" MIME_TYPE_CMIS_ACL = "application/cmisacl+xml" atompub = Blueprint("cmis", __name__, url_prefix="/cmis/atompub") route = atompub.route # # Dummy for now # class Logger(object): def debug(self, msg): print(msg) log = Logger() def log_result(result): print(78 * "-") print("Response:") print(result) print(78 * "-") def produces(*mimetypes): def decorator(f): return f return decorator def consumes(mimetype): def decorator(f): return f return decorator # NOT WORKING # def decorator(f): # def g(*args, **kw): # assert mimetype == request.content_type # return f(*args, **kw) # return g # return decorator # NOT WORKING def render(template, status=200, mimetype=None): def decorator(f): def g(*args, **kw): print(f, args, kw) result = f(*args, **kw) rendered = render_template(template, **result) headers = {"Content-Type": mimetype} response = make_response(rendered, status, headers) print(response) return response return g return decorator BOOLEAN_OPTIONS = ["includeAllowableActions", "includeACL", "includePolicyIds"] def get_options(args): d = {} for k in BOOLEAN_OPTIONS: v = args.get(k, "false").strip() if v == "true": v = True elif v in ("false", ""): v = False else: raise Exception("Unexpected parameter value for {}: {}".format(k, v)) d[k] = v return d def get_document(id): doc = repository.get_document_by_id(id) if not doc: raise NotFound return doc def get_folder(id): obj = repository.get_folder_by_id(id) if not obj: raise NotFound return obj def get_object(id): obj = repository.get_object_by_id(id) if not obj: raise NotFound return obj # # Authentication (basic) # # @atompub.before_request def authenticate(): if not request.authorization: raise Unauthorized() username = request.authorization.username password = request.authorization.password if (username, password) != ("admin", "admin"): raise Unauthorized() # @atompub.errorhandler(401) def custom_401(error): print("custom_401") return Response( "Authentication required", 401, {"WWWAuthenticate": 'Basic realm="Login Required"'}, ) @atompub.errorhandler(NoResultFound) def not_found_error_handler(error): """Converts SQLAlchemy NoResultFound exception to an HTTP error.""" return "This object does not exist", 404 # # Service Document # # @render("cmis/service.xml", mimetype=MIME_TYPE_ATOM_SERVICE) @route("/") @produces(MIME_TYPE_ATOM_SERVICE) def getRepositoryInfo(): log.debug("repositoryInfo called") root_folder = repository.root_folder ctx = {"ROOT": ROOT, "root_folder": root_folder} result = render_template("cmis/service.xml", **ctx) response = Response(result, mimetype=MIME_TYPE_ATOM_SERVICE) # return {'root': ROOT} return response # # Service Collections # @route("/types") def getTypeChildren(): log.debug("getTypeChildren called") result = render_template("cmis/types.xml", ROOT=ROOT) return Response(result, mimetype=MIME_TYPE_ATOM_FEED) @route("/types", methods=["POST"]) def createType(): log.debug("createType called") raise NotImplementedError() # # Entries (Documents, Folders, Relationships, Policies & Items) # @route("/entry") def getObject(): id = request.args.get("id") path = request.args.get("path") log.debug("getObject called on id={}, path={}".format(id, path)) log.debug("URL: " + request.url) options = get_options(request.args) log.debug("Options: %s" % options) if id: obj = repository.get_object_by_id(id) elif path: obj = repository.get_object_by_path(path) else: raise NotFound("You must supply either an id or a path.") if not obj: raise NotFound("Object not found") result = to_xml(obj, **options) return Response(result, mimetype=MIME_TYPE_ATOM_ENTRY) @route("/entry", methods=["PUT"]) def updateProperties(): id = request.args.get("id") if not id: path = request.args.get("path") else: path = "" log.debug("updateProperties called on id={}, path={}".format(id, path)) log.debug("URL: " + request.url) obj = get_object(id) return Response(to_xml(obj), mimetype=MIME_TYPE_ATOM_ENTRY) @route("/entry", methods=["DELETE"]) def deleteObject(): id = request.args.get("id") if not id: path = request.args.get("path") else: path = "" log.debug("deleteObject called on id={}, path={}".format(id, path)) log.debug("URL: " + request.url) obj = get_object(id) # TODO: remove parent = obj.parent if parent: child_count_0 = len(parent.children) db.session.delete(obj) db.session.commit() if parent: child_count_1 = len(parent.children) assert child_count_1 == child_count_0 - 1 return ("", 204, {}) # # Content Streams Ressource # @route("/content") def getContentStream(): id = request.args.get("id") log.debug("getContentStream called on " + id) document = get_document(id) return Response(document.content, mimetype=document.content_type) # setContentStream + appendContentStream @route("/content", methods=["PUT"]) def setContentStream(): id = request.args.get("id") log.debug("setContentStream called on " + id) document = get_document(id) created = document.content is None document.content = request.data document.content_type = request.content_type db.session.commit() if created: return ("", 201, {}) else: return ("", 204, {}) @route("/content", methods=["DELETE"]) def deleteContentStream(): id = request.args.get("id") log.debug("deleteContentStream called on " + id) document = get_document(id) document.content = None db.session.commit() return ("", 204, {}) # # Allowable Actions Resource # @route("/allowableactions") def getAllowableActions(): id = request.args.get("id") log.debug("getAllowableActions called on " + id) obj = get_object(id) args = {"ROOT": ROOT, "object": obj} result = render_template("cmis/allowableactions.xml", **args) return Response(result, mimetype=MIME_TYPE_CMIS_ALLOWABLE_ACTIONS) # # Type Entries # @route("/type") def getTypeDefinition(): type_id = request.args.get("id") log.debug("getTypeDefinition called on " + type_id) if type_id == "cmis:document": result = render_template("cmis/type-document.xml", ROOT=ROOT) elif type_id == "cmis:folder": result = render_template("cmis/type-folder.xml", ROOT=ROOT) elif type_id == "cmis:relationship": result = render_template("cmis/type-relationship.xml", ROOT=ROOT) elif type_id == "cmis:policy": result = render_template("cmis/type-policy.xml", ROOT=ROOT) else: raise NotImplementedError() return Response(result, mimetype=MIME_TYPE_ATOM_ENTRY) @route("/type", methods=["POST"]) def updateType(): raise NotImplementedError() @route("/type", methods=["DELETE"]) def deleteType(): raise NotImplementedError() # return ("", 204, {}) # # Folder Children collection # @route("/children") def getChildren(): id = request.args.get("id") log.debug("getChildren called on " + id) log.debug("URL: " + request.url) folder = get_folder(id) args = {"ROOT": ROOT, "folder": folder, "to_xml": to_xml} result = render_template("cmis/children.xml", **args) log_result(result) return Response(result, mimetype=MIME_TYPE_ATOM_ENTRY) @route("/children", methods=["POST"]) def createObject(): id = request.args.get("id") log.debug("createObject called on " + id) log.debug("URL: " + request.url) print("Received:") print(request.data) folder = get_folder(id) entry = Entry(request.data) name = entry.name type = entry.type if type == "cmis:folder": new_object = folder.create_subfolder(name) db.session.commit() elif type == "cmis:document": new_object = folder.create_document(name) if entry.content: new_object.content = entry.content new_object.content_type = entry.content_type db.session.commit() else: raise Exception("Unknown object type: %s" % type) result = to_xml(new_object) log_result(result) return Response(result, status=201, mimetype=MIME_TYPE_ATOM_ENTRY) # TODO: # URI newloc = null; # try { # newloc = new URI(getBase() + "/node/" + newdoc.getId()); # } catch (URISyntaxException e) { # // Shouldn't happen. # e.printStackTrace(); # } # String output = getTemplate("entry.ftl").arg("entry", newdoc) # .arg("parent", parent).arg("objectType", objectType).render(); # // XXX: .type() is here because of a bug in resteasy # return Response.created(newloc).entity(output).type(MIME_TYPE_ATOM_ENTRY).build(); # # Feeds # @route("/parents") def getObjectParents(): """Object Parents Feed (GET).""" id = request.args.get("id") log.debug("getObjectParents called on " + id) obj = get_object(id) if obj.parent: feed = Feed(obj, [obj.parent]) else: feed = Feed(obj, []) result = feed.to_xml() return Response(result, mimetype=MIME_TYPE_ATOM_FEED) # Changes Feed (GET) @route("/changes") def getContentChanges(): log.debug("getContentChanges called") raise NotImplementedError() # Folder Descendants Feed (GET, DELETE) @route("/descendants") def getDescendants(): id = request.args.get("id") log.debug("getObjectParents called on " + id) raise NotImplementedError() @route("/descendants", methods=["DELETE"]) @route("/foldertree", methods=["DELETE"]) def deleteTree(): id = request.args.get("id") log.debug("deleteTree called on " + id) obj = get_object(id) db.session.delete(obj) db.session.commit() return ("", 204, {}) # Folder Tree Feed (GET, DELETE: see above) @route("/foldertree") def getFolderTree(): id = request.args.get("id") log.debug("getFolderTree called on " + id) raise NotImplementedError() # All Versions Feed (GET) def getAllVersions(): pass # Type Descendants Feed (GET) @route("/typedesc") def getTypeDescendants(): type_id = request.args.get("typeId") log.debug("getTypeDescendants called on " + type_id) feed = Feed({}, []) result = feed.to_xml() return Response(result, mimetype=MIME_TYPE_ATOM_FEED) # # Query Collection # @route("/query", methods=["POST"]) def query(): q = request.form.get("q") log.debug("query called: " + q) raise NotImplementedError() PK!.K )abilian/sbe/apps/documents/cmis/parser.py# coding=utf-8 """Parses XML messages and converts them to objects.""" from __future__ import absolute_import, print_function, unicode_literals import base64 from datetime import datetime from lxml import objectify ATOM_NS = "http://www.w3.org/2005/Atom" APP_NS = "http://www.w3.org/2007/app" CMISRA_NS = "http://docs.oasis-open.org/ns/cmis/restatom/200908/" CMIS_NS = "http://docs.oasis-open.org/ns/cmis/core/200908/" class Entry(object): def __init__(self, xml=None): self.properties = {} self.links = [] self.content = None self.content_type = None if xml: self.parse(xml) def parse(self, xml): root = objectify.fromstring(xml) object = root["{%s}object" % CMISRA_NS] properties = object["{%s}properties" % CMIS_NS] for element in properties.iterchildren(): property = Property(element) self.properties[property.property_definition_id] = property content_element = getattr(root, "{%s}content" % CMISRA_NS, None) if content_element is not None: self.content_type = content_element.mediatype.text self.content = base64.b64decode(content_element.base64.text) @property def name(self): return self.properties["cmis:name"].value @property def type(self): return self.properties["cmis:objectTypeId"].value class Property(object): """A property MAY hold zero, one, or more typed data value(s). Each property MAY be single-valued or multi-valued. A single-valued property contains a single data value, whereas a multi-valued property contains an ordered list of data values of the same type. The ordering of values in a multi-valued property SHOULD be preserved by the repository. A property, either single-valued or multi-valued, MAY be in a "not set" state. CMIS does not support "null" property value. If a multi-valued property is not in a "not set" state, its property value MUST be a non-empty list of individual values. Each individual value in the list MUST NOT be in a "not set" state and MUST conform to the property's property-type. A multi-valued property is either set or not set in its entirety. An individual value of a multi-valued property MUST NOT be in an individual "value not set" state and hold a position in the list of values. An empty list of values MUST NOT be allowed. Every property is typed. The property-type defines the data type of the data value(s) held by the property. CMIS specifies the following property-types. """ def __init__(self, element=None): if element is not None: self.parse(element) def parse(self, element): tag = element.tag self.type = tag[tag.index("}") + 1 + len("property") :].lower() self.property_definition_id = element.attrib["propertyDefinitionId"] self.local_name = element.attrib.get("localName") self.display_name = element.attrib.get("displayName") self.query_name = element.attrib.get("queryName") value_elem = getattr(element, "{%s}value" % CMIS_NS) if value_elem: value = value_elem.text else: self.value = value = None if value: if self.type in ("id", "string"): self.value = value elif self.type == "datetime": # FIXME self.value = datetime(value) else: raise Exception("Unknown value type: %s" % self.type) PK!u+abilian/sbe/apps/documents/cmis/renderer.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from flask import render_template # TEMP ROOT = "http://localhost:5000/cmis/atompub" XML_HEADER = "\n" class Feed(object): def __init__(self, object, collection): self.object = object self.collection = collection def to_xml(self, **options): ctx = { "ROOT": ROOT, "object": self.object, "collection": self.collection, "to_xml": to_xml, } return render_template("cmis/feed.xml", **ctx) class Entry(object): def __init__(self, obj): self.obj = obj def to_xml(self, **options): ctx = { "ROOT": ROOT, "folder": self.obj, "document": self.obj, "options": options, "to_xml": to_xml, } if self.obj.sbe_type == "cmis:folder": result = render_template("cmis/folder.xml", **ctx) elif self.obj.sbe_type == "cmis:document": result = render_template("cmis/document.xml", **ctx) else: raise Exception("Unknown base object type: %s" % self.obj.sbe_type) if "no_xml_header" not in options: result = XML_HEADER + result return result def to_xml(obj, **options): entry = Entry(obj) return entry.to_xml(**options) PK!`5&abilian/sbe/apps/documents/commands.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import sqlalchemy as sa from flask_script import Manager from . import tasks from .models import Document manager = Manager(description="SBE documents actions", help="SBE documents actions") @manager.command def antivirus(): """Schedule documents to antivirus scan.""" documents = Document.query.filter(Document.content_blob != None).options( sa.orm.noload("creator"), sa.orm.noload("owner"), sa.orm.joinedload("content_blob"), ) total = 0 count = 0 for d in documents.yield_per(1000): total += 1 meta = d.content_blob.meta if "antivirus" not in meta and "antivirus_task" not in meta: tasks.antivirus_scan.delay(d.id) count += 1 print("{count}/{total} documents scheduled".format(count=count, total=total)) PK!G88"abilian/sbe/apps/documents/lock.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime, timedelta import dateutil.parser from abilian.core.util import utcnow from flask import current_app from flask_login import current_user from six import raise_from, text_type DEFAULT_LIFETIME = 3600 class Lock(object): """Represent a lock on a document.""" def __init__(self, user_id, user, date, *args, **kwargs): self.user_id = user_id self.user = user if not isinstance(date, datetime): try: date = dateutil.parser.parse(date) except Exception as e: raise_from(ValueError("Error parsing date: {!r}".format(date)), e) self.date = date @staticmethod def new(): return Lock(current_user.id, text_type(current_user), utcnow()) def as_dict(self): """Return a dict suitable for serialization to JSON.""" return { "user_id": self.user_id, "user": self.user, "date": self.date.isoformat(), } @staticmethod def from_dict(d): """Deserialize from a `dict` created by :meth:`as_dict`.""" return Lock(**d) @property def lifetime(self): return current_app.config.get("SBE_LOCK_LIFETIME", DEFAULT_LIFETIME) @property def expired(self): return (utcnow() - self.date) > timedelta(seconds=self.lifetime) def is_owner(self, user=None): if user is None: user = current_user return self.user_id == user.id PK! # title @hybrid_property def title(self): return self._title @title.setter def title(self, title): # set title before setting name, so that we don't enter an infinite loop # with _cmis_sync_name_title self._title = title if self.name != title: self.name = title def clone(self, title=None, parent=None): if not title: title = self.title new_obj = self.__class__(title=title, name=title, parent=parent) state = vars(self) for k, v in state.items(): if not k.startswith("_") and k not in [ "uid", "id", "parent", "title", "name", "path", "subfolders", "documents", ]: setattr(new_obj, k, v) if self.parent: assert self in self.parent.children return new_obj @property def path(self): if self.parent: return self.parent.path + "/" + self.title else: return "" @property def is_folder(self): return self.sbe_type == "cmis:folder" @property def is_document(self): return self.sbe_type == "cmis:document" @property def is_root_folder(self): return self._parent_id is None @property def community(self): if not self.is_folder: return self.parent and self.parent.community if self._community: return self._community return self.parent and self.parent.community @listens_for(CmisObject.name, "set", propagate=True, active_history=True) def _cmis_sync_name_title(entity, new_value, old_value, initiator): """Synchronize CmisObject name -> title. CmisObject.title -> name is done via hybrid_property, avoiding infinite loop (since "set" is received before attribute has received value) """ if entity.title != new_value: entity.title = new_value return new_value class PathAndSecurityIndexable(object): """Mixin for folder and documents indexation.""" __indexation_args__ = { "index_to": ( ("_indexable_parent_ids", ("parent_ids",)), ("_indexable_roles_and_users", ("allowed_roles_and_users",)), ) } def _iter_to_root(self, skip_self=False): obj = self if not skip_self else self.parent while obj: yield obj obj = obj.parent @property def _indexable_parent_ids(self): """Return a string made of ids separated by a slash: "/1/3/4/5", "5" being self.parent.id.""" ids = [text_type(obj.id) for obj in self._iter_to_root(skip_self=True)] return "/" + "/".join(reversed(ids)) @property def _indexable_roles_and_users(self): """Returns a string made of type:id elements, like "user:2 group:1 user:6".""" iter_from_root = reversed(list(self._iter_to_root())) if self.parent: # skip root folder only on non-root folder! next(iter_from_root) allowed = {o[0] for o in security.get_role_assignements(next(iter_from_root))} for obj in iter_from_root: if obj.inherit_security: continue obj_allowed = {o[0] for o in security.get_role_assignements(obj)} if Anonymous in obj_allowed: continue parent_allowed = allowed # pure intersection: users and groups in both are preserved allowed = allowed & obj_allowed remaining = parent_allowed - obj_allowed # find users who can access 'obj' because of their group memberships # 1. extends groups in obj_allowed with their actual member list extended_allowed = set( itertools.chain( *(p.members if isinstance(p, Group) else (p,) for p in obj_allowed) ) ) # 2. remaining_users are users explicitly listed in parents but not on # obj. Are they in a group? remaining_users = {o for o in remaining if isinstance(o, User)} allowed |= remaining_users & extended_allowed # remaining groups: find if some users are eligible remaining_groups_members = set( itertools.chain(*(p.members for p in remaining if isinstance(p, Group))) ) allowed |= remaining_groups_members - extended_allowed # admin role is always granted access allowed.add(Admin) return " ".join(indexable_role(p) for p in allowed) class Folder(PathAndSecurityIndexable, CmisObject): __tablename__ = None # type: str sbe_type = "cmis:folder" __indexable__ = True __indexation_args__ = {} __indexation_args__.update(CmisObject.__indexation_args__) index_to = () index_to += CmisObject.__indexation_args__.setdefault("index_to", ()) index_to += PathAndSecurityIndexable.__indexation_args__.setdefault("index_to", ()) __indexation_args__["index_to"] = index_to del index_to _indexable_roles_and_users = PathAndSecurityIndexable._indexable_roles_and_users parent = relationship( "Folder", primaryjoin=(lambda: foreign(CmisObject._parent_id) == remote(Folder.id)), backref=backref( "subfolders", lazy="joined", order_by="Folder.title", cascade="all, delete-orphan", ), ) @property def icon(self): return icon_url("folder.png") @property def children(self): return self.subfolders + self.documents @property def document_count(self): count = len(self.documents) for f in self.subfolders: count += f.document_count return count @property def depth(self): # type: () -> int if self.parent is None: return 0 return self.parent.depth + 1 def create_subfolder(self, title): # type: (str) -> Folder subfolder = Folder(title=title, parent=self) assert subfolder in self.children return subfolder def create_document(self, title): # type: (str) -> Document doc = Document(title=title, parent=self) assert doc.parent == self assert doc in self.children return doc def get_object_by_path(self, path): # type: (str) -> Union[Document, Folder, None] assert path.startswith("/") assert "//" not in path if path == "/": return self path_segments = path[1:].split("/") obj = self try: for name in path_segments[:]: obj = first(x for x in obj.children if x.title == name) return obj except IndexError: return None def __repr__(self): return "<{}.{} id={!r} name={!r} path={!r} at 0x{:x}>".format( self.__class__.__module__, self.__class__.__name__, self.id, self.title, self.path, id(self), ) # # Security related methods # @property def filtered_children(self): return security.filter_with_permission( current_user, "read", self.children, inherit=True ) @property def filtered_subfolders(self): return security.filter_with_permission( current_user, "read", self.subfolders, inherit=True ) def get_local_roles_assignments(self): local_roles_assignments = security.get_role_assignements(self) # local_roles_assignments = sorted(local_roles_assignments, # key=lambda u: (u[0].last_name.lower(), # u[0].first_name.lower())) return local_roles_assignments def get_inherited_roles_assignments(self): if self.parent.is_root_folder: return [] roles = self.parent.get_local_roles_assignments() inherited_roles = ( self.parent.get_inherited_roles_assignments() if self.parent.inherit_security else [] ) assignments = set(itertools.chain(roles, inherited_roles)) def key(x): principal = x[0] if isinstance(principal, User): # Defensive programming here, this shouldn't happen actually last_name = principal.last_name or "" first_name = principal.first_name or "" return last_name.lower(), first_name.lower() elif isinstance(principal, Group): return principal.name else: raise Exception("Bad class here: %s" % type(principal)) return sorted(assignments, key=key) def members(self): local_roles = self.get_local_roles_assignments() inherited_roles = ( self.get_inherited_roles_assignments() if self.inherit_security else [] ) def _iter_users(roles): for principal, user in roles: if isinstance(principal, User): yield principal else: for user in itertools.chain(principal.members, principal.admins): yield user members = set(_iter_users(itertools.chain(local_roles, inherited_roles))) members = sorted(members, key=lambda u: (u.last_name, u.first_name)) return members class BaseContent(CmisObject): """A base class for cmisobject with an attached file.""" __tablename__ = None # type: str _content_id = Column(Integer, db.ForeignKey(Blob.id)) content_blob = relationship(Blob, cascade="all, delete", foreign_keys=[_content_id]) #: md5 digest (BTW: not sure they should be part of the public API). content_digest = Column(Text) #: size (in bytes) of the content blob. content_length = Column( Integer, default=0, nullable=False, server_default=sa.text("0"), info={ "searchable": True, "index_to": (("content_length", wf.NUMERIC(stored=True)),), }, ) #: MIME type of the content stream. # TODO: normalize mime type? content_type = Column( Text, default="application/octet-stream", info={"searchable": True, "index_to": (("content_type", wf.ID(stored=True)),)}, ) @property def content(self): # type: () -> bytes return self.content_blob.value @content.setter def content(self, value): # type: (bytes) -> None assert isinstance(value, bytes) self.content_blob = Blob() self.content_blob.value = value self.content_length = len(value) def set_content(self, content, content_type=None): # type: (bytes, Any) -> None new_digest = md5(content) if new_digest == self.content_digest: return self.content_digest = new_digest self.content = content content_type = self.find_content_type(content_type) if content_type: self.content_type = content_type def find_content_type(self, content_type=""): """Find possibly more appropriate content_type for this instance. If `content_type` is a binary one, try to find a better one based on content name so that 'xxx.pdf' is not flagged as binary/octet-stream for example """ if content_type not in ( None, "", "application/octet-stream", "binary/octet-stream", "application/binary", "multipart/octet-stream", ): return content_type # missing or generic content type: try to find something more useful to be # able to do preview/indexing/... if self.title: guessed_content_type = mimetypes.guess_type(self.title, strict=False)[0] if ( guessed_content_type and guessed_content_type != "application/vnd.ms-office.activeX" ): # mimetypes got an update: "random.bin" would be guessed as # 'application/vnd.ms-office.activeX'... not so useful in a document # repository content_type = guessed_content_type return content_type @property def icon(self): return icon_for(self.content_type) class Document(BaseContent, PathAndSecurityIndexable): """A document, in the CMIS sense.""" __tablename__ = None # type: str __indexable__ = True __indexation_args__ = {} __indexation_args__.update(BaseContent.__indexation_args__) index_to = () index_to += BaseContent.__indexation_args__.setdefault("index_to", ()) index_to += PathAndSecurityIndexable.__indexation_args__.setdefault("index_to", ()) index_to += (("text", ("text",)),) __indexation_args__["index_to"] = index_to del index_to _indexable_roles_and_users = PathAndSecurityIndexable._indexable_roles_and_users parent = relationship( "Folder", primaryjoin=(foreign(CmisObject._parent_id) == remote(Folder.id)), backref=backref( "documents", lazy="joined", order_by="Document.title", cascade="all, delete-orphan", ), ) PREVIEW_SIZE = 700 @property def preview_size(self): # type: () -> int return self.PREVIEW_SIZE def has_preview(self, size=None, index=0): # type: (Optional[int], int) -> bool if size is None: size = self.PREVIEW_SIZE return converter.has_image(self.content_digest, self.content_type, index, size) @property def digest(self): # type: () -> str """Alias for content_digest.""" return self.content_digest _text_id = Column(Integer, db.ForeignKey(Blob.id), info=NOT_AUDITABLE) text_blob = relationship(Blob, cascade="all, delete", foreign_keys=[_text_id]) _pdf_id = Column(Integer, db.ForeignKey(Blob.id), info=NOT_AUDITABLE) pdf_blob = relationship(Blob, cascade="all, delete", foreign_keys=[_pdf_id]) _preview_id = Column(Integer, db.ForeignKey(Blob.id), info=NOT_AUDITABLE) preview_blob = relationship(Blob, cascade="all, delete", foreign_keys=[_preview_id]) language = Column( Text, info={"searchable": True, "index_to": [("language", wf.ID(stored=True))]} ) size = Column(Integer) page_num = Column(Integer, default=1) # FIXME: use Entity.meta instead #: Stores extra metadata as a JSON column extra_metadata_json = Column(UnicodeText, info={"auditable": False}) sbe_type = "cmis:document" # antivirus status def ensure_antivirus_scheduled(self): if not self.antivirus_required: return True if current_app.config.get("CELERY_ALWAYS_EAGER", False): async_conversion(self) return True task_id = self.content_blob.meta.get("antivirus_task_id") if task_id is not None: res = tasks.process_document.AsyncResult(task_id) if not res.failed(): # success, or pending or running return True # schedule a new task self.content_blob.meta["antivirus_task_id"] = str(uuid.uuid4()) async_conversion(self) @property def antivirus_scanned(self): """True if antivirus task was run, even if antivirus didn't return a result.""" return self.content_blob and "antivirus" in self.content_blob.meta @property def antivirus_status(self): """ True: antivirus has scanned file: no virus False: antivirus has scanned file: virus detected None: antivirus task was run, but antivirus didn't return a result """ return self.content_blob and self.content_blob.meta.get("antivirus") @property def antivirus_required(self): """True if antivirus doesn't need to be run.""" required = current_app.config["ANTIVIRUS_CHECK_REQUIRED"] return required and ( not self.antivirus_scanned or self.antivirus_status is None ) @property def antivirus_ok(self): """True if user can safely access document content.""" required = current_app.config["ANTIVIRUS_CHECK_REQUIRED"] if required: return self.antivirus_status is True return self.antivirus_status is not False # R/W properties @BaseContent.content.setter def content(self, value): BaseContent.content.fset(self, value) self.content_blob.meta["antivirus_task_id"] = str(uuid.uuid4()) self.pdf_blob = None self.text_blob = None def set_content(self, content, content_type=None): # type: (bytes, Any) -> None super(Document, self).set_content(content, content_type) async_conversion(self) @property def pdf(self): if self.pdf_blob: assert isinstance(self.pdf_blob.value, bytes) return self.pdf_blob and self.pdf_blob.value @pdf.setter def pdf(self, value): assert isinstance(value, bytes) self.pdf_blob = Blob() self.pdf_blob.value = value # `text` is an Unicode value. @property def text(self): return self.text_blob.value.decode("utf8") if self.text_blob is not None else "" @text.setter def text(self, value): assert isinstance(value, text_type) self.text_blob = Blob() self.text_blob.value = value.encode("utf8") @property def extra_metadata(self): if not hasattr(self, "_extra_metadata"): if self._extra_metadata is not None: self._extra_metadata = json.loads(self.extra_metadata_json) else: self._extra_metadata = None return self._extra_metadata @extra_metadata.setter def extra_metadata(self, extra_metadata): self._extra_metadata = extra_metadata self.extra_metadata_json = text_type(json.dumps(extra_metadata)) # TODO: or use SQLAlchemy alias? @property def file_name(self): return self.title def __repr__(self): return "".format( self.id, self.title, self.path, self.content_length, id(self) ) # locking management; used for checkin/checkout - this could be generalized to # any entity @property def lock(self): # type: () -> Optional[Lock] """ :returns: either `None` if no lock or current lock is expired; either the current valid :class:`Lock` instance. """ lock = self.meta.setdefault("abilian.sbe.documents", {}).get("lock") if lock: lock = Lock(**lock) if lock.expired: lock = None return lock @lock.setter def lock(self, user): """Allow to do `document.lock = user` to set a lock for user. If user is None, the lock is released. """ if user is None: del self.lock return self.set_lock(user=user) @lock.deleter def lock(self): """Remove lock, if any. `del document.lock` can be safely done even if no lock is set. """ meta = self.meta.setdefault("abilian.sbe.documents", {}) if "lock" in meta: del meta["lock"] self.meta.changed() def set_lock(self, user=None): if user is None: user = current_user lock = self.lock if lock and not lock.is_owner(user=user): raise RuntimeError("This document is already locked by another user") meta = self.meta.setdefault("abilian.sbe.documents", {}) lock = Lock.new() meta["lock"] = lock.as_dict() self.meta.changed() def icon_for(content_type): for extension, mime_type in mimetypes.types_map.items(): if mime_type == content_type: extension = extension[1:] icon = "%s.png" % extension if icon_exists(icon): return icon_url(icon) return icon_url("bin.png") # Async conversion _async_data = threading.local() def _get_documents_queue(): if not hasattr(_async_data, "documents"): _async_data.documents = [] return _async_data.documents def async_conversion(document): _get_documents_queue().append( (document, document.content_blob.meta.get("antivirus_task_id")) ) def _trigger_conversion_tasks(session): if ( # this commit is not from the application session session is not db.session() # inside a sub-transaction: not yet written in DB or session.transaction.nested ): return document_queue = _get_documents_queue() while document_queue: doc, task_id = document_queue.pop() if doc.id: tasks.process_document.apply_async((doc.id,), task_id=task_id) def setup_listener(): mark_attr = "__abilian_sa_listening" if getattr(_trigger_conversion_tasks, mark_attr, False): return listen(Session, "after_commit", _trigger_conversion_tasks) setattr(_trigger_conversion_tasks, mark_attr, True) PK!r ==(abilian/sbe/apps/documents/repository.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import sqlalchemy as sa from abilian.services.security import READ, Permission, security from .models import CmisObject, Document, Folder class SecurityException(Exception): pass class Repository(object): """A simple document repository, implementing the basic functionalities of the CMIS model.""" def __init__(self, app=None): if app is not None: self.init_app(app) def init_app(self, app): self.app = app app.extensions["content_repository"] = self @property def root_folder(self): folder = Folder.query.filter(Folder.parent == None).first() if folder: return folder # Should only happen during tests folder = Folder(title="root") return folder def get_object(self, id=None, path=None): """Get the CMIS object (document or folder) with either the given `id` or the given `path`. Returns None if the object doesn't exist. """ if id: return self.get_object_by_id(id) else: return self.get_object_by_path(path) # # Id based navigation # def get_object_by_id(self, id): """Get the CMIS object (document or folder) with the given `id`. Returns None if the object doesn't exist. """ obj = CmisObject.query.get(id) if obj is not None and not isinstance(obj, CmisObject): return None return obj def get_folder_by_id(self, id): """Get the folder with the given `id`. Returns None if the folder doesn't exist. """ return Folder.query.get(id) def get_document_by_id(self, id): """Get the document with the given `id`. Returns None if the document doesn't exist. """ document = Document.query.get(id) return document # # Path based navigation # def get_object_by_path(self, path): """Gets the CMIS object (document or folder) with the given `path`. Returns None if the object doesn't exist. """ return self.root_folder.get_object_by_path(path) def get_folder_by_path(self, path): """Gets the folder with the given `path`. Returns None if the folder doesn't exist. """ obj = self.root_folder.get_object_by_path(path) if obj is None or not obj.is_folder: return None else: return obj def get_document_by_path(self, path): """Gets the document with the given `path`. Returns None if the document doesn't exist. """ obj = self.root_folder.get_object_by_path(path) if obj is None or not obj.is_document: return None else: return obj # # COPY / MOVE support # def copy_object(self, obj, dest_folder, dest_title=None): new_obj = obj.clone(title=dest_title, parent=dest_folder) if obj.is_folder: for child in obj.children: self.copy_object(child, new_obj) return new_obj def move_object(self, obj, dest_folder, dest_title=None): obj.parent = dest_folder if dest_title: obj.title = dest_title def rename_object(self, obj, title): obj.title = title def delete_object(self, obj): if obj.is_root_folder: raise Exception("Can't delete root folder.") session = sa.orm.object_session(obj) setattr(obj, "__path_before_delete", obj.path) # for audit log. parent = obj.parent collection = parent.subfolders if obj.is_folder else parent.documents session.delete(obj) collection.remove(obj) # # Locking (TODO) # def is_locked(self, obj): return False def can_unlock(self, obj): return True def lock(self, obj): return "???" def unlock(self, obj): pass # # Security / access rights # def has_permission(self, user, permission, obj): assert isinstance(permission, Permission) return security.has_permission(user, permission, obj, inherit=True) def has_access(self, user, obj): """Checks that user has actual right to reach this object, 'read' permission on each of object's parents.""" current = obj while current.parent is not None: if not self.has_permission(user, READ, current): return False current = current.parent return True repository = Repository() PK!y $abilian/sbe/apps/documents/search.py# coding=utf-8 """Indexing related utilities for Folder, Documents.""" from __future__ import absolute_import, print_function, unicode_literals import sqlalchemy as sa from abilian.core.entities import Entity from abilian.core.extensions import db from abilian.services import get_service from .models import CmisObject def reindex_tree(obj): """Schedule reindexing `obj` and all of its descendants. Generally needed to update indexed security. """ assert isinstance(obj, CmisObject) index_service = get_service("indexing") if not index_service.running: return descendants = ( sa.select([CmisObject.id, CmisObject._parent_id]) .where(CmisObject._parent_id == sa.bindparam("ancestor_id")) .cte(name="descendants", recursive=True) ) da = descendants.alias() CA = sa.orm.aliased(CmisObject) d_ids = sa.select([CA.id, CA._parent_id]) descendants = descendants.union_all(d_ids.where(CA._parent_id == da.c.id)) session = sa.orm.object_session(obj) or db.session() # including ancestor_id in entity_ids_q will garantee at least 1 value for the # "IN" predicate; otherwise when using sqlite (as during tests...) # 'ResourceClosedError' will be raised. # # as an added bonus, "obj" will also be in query results, thus will be added # in "to_update" without needing to do it apart. entity_ids_q = sa.union( sa.select([descendants.c.id]), sa.select([sa.bindparam("ancestor_id")]) ) query = ( session.query(Entity) .filter(Entity.id.in_(entity_ids_q)) .options(sa.orm.noload("*")) .params(ancestor_id=obj.id) ) to_update = index_service.app_state.to_update key = "changed" for item in query.yield_per(1000): to_update.append((key, item)) PK![%abilian/sbe/apps/documents/signals.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from abilian.services.security import Manager, Reader, Writer, security from abilian.sbe.apps.communities.models import VALID_ROLES from abilian.sbe.apps.communities.signals import membership_removed, \ membership_set from .search import reindex_tree @membership_set.connect def new_community_member(community, membership, is_new, **kwargs): if not community.folder: return role = membership.role user = membership.user local_role = Writer if community.type == "participative" else Reader if role == Manager: local_role = Manager current_roles = set(security.get_roles(user, community.folder, no_group_roles=True)) current_roles &= VALID_ROLES # ensure we don't remove roles not managed # by us for role_to_ungrant in current_roles - {local_role}: security.ungrant_role(user, role_to_ungrant, community.folder) if local_role not in current_roles: security.grant_role(user, local_role, community.folder) reindex_tree(community.folder) @membership_removed.connect def remove_community_member(community, membership, **kwargs): if not community.folder: return user = membership.user roles = set(security.get_roles(user, community.folder, no_group_roles=True)) roles &= VALID_ROLES # ensure we don't remove roles not managed by us for role in roles: security.ungrant_role(user, role, community.folder) reindex_tree(community.folder) PK!{xج#abilian/sbe/apps/documents/tasks.py# coding=utf-8 """Celery tasks related to document transformation and preview.""" from __future__ import absolute_import, print_function, unicode_literals import logging from contextlib import contextmanager from abilian.core.extensions import db from abilian.services import converter, get_service from abilian.services.conversion import ConversionError, HandlerNotFound from celery import shared_task logger = logging.getLogger(__package__) @contextmanager def get_document(document_id, session=None): """Context manager that yields (session, document).""" from .models import Document doc_session = session if session is None: doc_session = db.create_scoped_session() with doc_session.begin_nested(): query = doc_session.query(Document) document = query.get(document_id) yield (doc_session, document) # cleanup if session is None: doc_session.commit() doc_session.close() @shared_task def process_document(document_id): """Run document processing chain.""" with get_document(document_id) as (session, document): if document is None: return # True = Ok, None means no check performed (no antivirus present) is_clean = _run_antivirus(document) if is_clean is False: return preview_document.delay(document_id) convert_document_content.delay(document_id) def _run_antivirus(document): antivirus = get_service("antivirus") if antivirus and antivirus.running: is_clean = antivirus.scan(document.content_blob) if "antivirus_task" in document.content_blob.meta: del document.content_blob.meta["antivirus_task"] return is_clean return None @shared_task def antivirus_scan(document_id): """Return antivirus.scan() result.""" with get_document(document_id) as (session, document): if document is None: return return _run_antivirus(document) @shared_task def preview_document(document_id): """Compute the document preview images with its default preview size.""" with get_document(document_id) as (session, document): if document is None: # deleted after task queued, but before task run return try: converter.to_image( document.content_digest, document.content, document.content_type, 0, document.preview_size, ) except ConversionError as e: logger.info( "Preview failed: %s", str(e), exc_info=True, extra={"stack": True} ) @shared_task def convert_document_content(document_id): """Convert document content.""" with get_document(document_id) as (session, doc): if doc is None: # deleted after task queued, but before task run return convert_to_pdf(doc) convert_to_text(doc) extract_metadata(doc) def convert_to_pdf(doc): error_kwargs = {"exc_info": True, "extra": {"stack": True}} if doc.content_type == "application/pdf": doc.pdf = doc.content else: try: doc.pdf = converter.to_pdf( doc.content_digest, doc.content, doc.content_type ) except HandlerNotFound: doc.pdf = b"" except ConversionError as e: doc.pdf = b"" logger.info( "Conversion to PDF failed for document %s: %s", doc.name, e, **error_kwargs ) def convert_to_text(doc): error_kwargs = {"exc_info": True, "extra": {"stack": True}} try: doc.text = converter.to_text(doc.content_digest, doc.content, doc.content_type) except ConversionError as e: doc.text = "" logger.info( "Conversion to text failed for document %s: %s", doc.name, e, **error_kwargs ) def extract_metadata(doc): error_kwargs = {"exc_info": True, "extra": {"stack": True}} doc.extra_metadata = {} try: doc.extra_metadata = converter.get_metadata( doc.content_digest, doc.content, doc.content_type ) except ConversionError as e: logger.warning( "Metadata extraction failed on document %s: %s", doc.name, e, **error_kwargs ) except UnicodeDecodeError as e: logger.error("Unicode issue on document %s: %s", doc.name, e, **error_kwargs) except Exception as e: logger.error("Other issue on document %s: %s", doc.name, e, **error_kwargs) if doc.text: import langid doc.language = langid.classify(doc.text)[0] doc.page_num = doc.extra_metadata.get("PDF:Pages", 1) PK!f{{{>abilian/sbe/apps/documents/templates/cmis/allowableactions.xml {%- from "cmis/macros.xml" import allowable_actions -%} {{ allowable_actions({}) }} PK!*\116abilian/sbe/apps/documents/templates/cmis/children.xml {%- from "cmis/macros.xml" import links -%} System http://chemistry.apache.org/d29ya3NwYWNlOi8vU3BhY2VzU3RvcmUvM2RmYmJhNzMtN2M1NC00YWQ4LTljMWMtZTNjZmUyZWIyY2Qx Company Home 2012-09-01T21:51:56Z 2012-09-01T21:51:56Z {{ folder.children|length }} Folder collection application/cmisatom+xml {% for child in folder.children %} {{ to_xml(child, no_xml_header=True) | safe }} {% endfor %} {{ links(folder, ROOT) }} PK!T6abilian/sbe/apps/documents/templates/cmis/document.xml{%- from "cmis/macros.xml" import links, allowable_actions -%} admin http://chemistry.apache.org/d29ya3NwYWNlOi8vU3BhY2VzU3RvcmUvNzkyODJmNmEtMjJlOC00OWIxLTk2ZjAtZDgzZDY1Nzg5M2JlOzEuMA== 2012-09-01T21:53:18Z {{ document.title }} 2012-09-01T21:53:18Z 2012-09-01T21:53:18Z true {{ document.content_length }} {{ document.sbe_type }} {{ document.name }} {{ document.content_type }} TODO 2012-09-01T23:53:18.300+02:00 1.0 true false admin admin Initial Version {{document.id}} true false {{ document.sbe_type }} 2012-09-01T23:53:18.300+02:00 {{ document.file_name }} {% if options.includeAllowableActions %} {{ allowable_actions(object) }} {% endif %} {{ links(document, ROOT) }} PK! "gg2abilian/sbe/apps/documents/templates/cmis/feed.xml {%- from "cmis/macros.xml" import links -%} System http://chemistry.apache.org/d29ya3NwYWNlOi8vU3BhY2VzU3RvcmUvM2RmYmJhNzMtN2M1NC00YWQ4LTljMWMtZTNjZmUyZWIyY2Qx {{ object.title }} 2012-09-01T21:51:56Z 2012-09-01T21:51:56Z {{ collection|length }} {% for entity in collection %} {{ to_xml(entity, no_xml_header=True) | safe}} {% endfor %} {{ links(object, ROOT) }} PK!tcO))4abilian/sbe/apps/documents/templates/cmis/folder.xml{%- from "cmis/macros.xml" import links, allowable_actions -%} System http://chemistry.apache.org/d29ya3NwYWNlOi8vU3BhY2VzU3RvcmUvM2RmYmJhNzMtN2M1NC00YWQ4LTljMWMtZTNjZmUyZWIyY2Qx 2012-09-01T21:33:27Z {{ folder.title }} 2012-09-01T21:51:56Z 2012-09-01T21:51:56Z {{ folder.sbe_type }} {{ folder.path }} {{ folder.name }} 2012-09-01T23:33:27.555+02:00 System System {{ folder.id }} {{ folder.sbe_type }} 2012-09-01T23:51:56.385+02:00 {% if options.includeAllowableActions %} {{ allowable_actions(folder) }} {% endif %} {{ links(folder, ROOT) }} PK!bQQ4abilian/sbe/apps/documents/templates/cmis/macros.xml {% macro links(object, ROOT) %} {%- if object.sbe_type == 'cmis:document' -%} {% elif object.sbe_type == 'cmis:folder' %} {%- else -%} {%- endif -%} {% endmacro %} {% macro allowable_actions(object) %} true true false true false true false false true true false false false true false true true true false false false false false false false false false false false {% endmacro %} PK!#ⶨ**5abilian/sbe/apps/documents/templates/cmis/service.xml default root Root Collection application/atom+xml;type=entry application/cmisatom+xml types Types Collection query Query Collection application/cmisquery+xml checkedout Checked Out Collection application/cmisatom+xml unfiled Unfiled Collection application/cmisatom+xml default Abilian micro content repository Abilian micro content repository Abilian Abilian micro content repository 0.1 {{ root_folder.id }} 0 manage false properties anytime true true false false false bothcombined none false false none basic objectonly cmis:read Read cmis:write Write cmis:all All canGetDescendents.Folder cmis:read canGetChildren.Folder cmis:read canGetParents.Folder cmis:read canGetFolderParent.Object cmis:read canCreateDocument.Folder cmis:write canCreateFolder.Folder cmis:write canCreateRelationship.Source cmis:read canCreateRelationship.Target cmis:read canGetProperties.Object cmis:read canViewContent.Object cmis:read canUpdateProperties.Object cmis:write canMove.Object cmis:write canMove.Target cmis:write canMove.Source cmis:write canDelete.Object cmis:write canDeleteTree.Folder cmis:write canSetContent.Document cmis:write canDeleteContent.Document cmis:write canAddToFolder.Object cmis:write canAddToFolder.Folder cmis:write canRemoveFromFolder.Object cmis:write canRemoveFromFolder.Folder cmis:write canCheckout.Document cmis:write canCancelCheckout.Document cmis:write canCheckin.Document cmis:write canGetAllVersions.VersionSeries cmis:read canGetObjectRelationships.Object cmis:read canAddPolicy.Object cmis:write canAddPolicy.Policy cmis:write canRemovePolicy.Object cmis:write canRemovePolicy.Policy cmis:write canGetAppliedPolicies.Object cmis:read canGetACL.Object cmis:read canApplyACL.Object cmis:all 1.1 true anonymous anyone {{ROOT}}/entry?id={id}&filter={filter}&includeAllowableActions={includeAllowableActions}&includeACL={includeACL}&includePolicyIds={includePolicyIds}&includeRelationships={includeRelationships}&renditionFilter={renditionFilter} objectbyid application/atom+xml;type=entry {{ROOT}}/entry?path={path}&filter={filter}&includeAllowableActions={includeAllowableActions}&includeACL={includeACL}&includePolicyIds={includePolicyIds}&includeRelationships={includeRelationships}&renditionFilter={renditionFilter} objectbypath application/atom+xml;type=entry {{ROOT}}/type?id={id} typebyid application/atom+xml;type=entry {{ROOT}}/query?q={q}&searchAllVersions={searchAllVersions}&includeAllowableActions={includeAllowableActions}&includeRelationships={includeRelationships}&maxItems={maxItems}&skipCount={skipCount} query application/atom+xml;type=feed PK!JJ;abilian/sbe/apps/documents/templates/cmis/type-document.xml unknown http://chemistry.apache.org/Y21pczpkb2N1bWVudA== Document 2012-09-01T22:04:29Z 2012-09-01T22:04:29Z cmis:document document http://www.alfresco.org/model/cmis/1.0/cs01 Document cmis:document Document Type cmis:document true true true true true false true cmis:objectTypeId objectTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Object Type Id cmis:objectTypeId Id of the object’s type id single oncreate false true true true cmis:isVersionSeriesCheckedOut isVersionSeriesCheckedOut http://www.alfresco.org/model/cmis/1.0/cs01 Is Version Series Checked Out cmis:isVersionSeriesCheckedOut Is the version series checked out? boolean single readonly false false false false cmis:isImmutable isImmutable http://www.alfresco.org/model/cmis/1.0/cs01 Is Immutable cmis:isImmutable Is the document immutable? boolean single readonly false false false false cmis:contentStreamLength contentStreamLength http://www.alfresco.org/model/cmis/1.0/cs01 Content Stream Length cmis:contentStreamLength The length of the content stream integer single readonly false false true true cmis:contentStreamFileName contentStreamFileName http://www.alfresco.org/model/cmis/1.0/cs01 Content Stream Filename cmis:contentStreamFileName The content stream filename string single readonly false false true false cmis:versionSeriesCheckedOutId versionSeriesCheckedOutId http://www.alfresco.org/model/cmis/1.0/cs01 Version Series Checked Out Id cmis:versionSeriesCheckedOutId The checked out version series id id single readonly false false false false cmis:versionSeriesCheckedOutBy versionSeriesCheckedOutBy http://www.alfresco.org/model/cmis/1.0/cs01 Version Series Checked Out By cmis:versionSeriesCheckedOutBy The authority who checked out this document version series string single readonly false false false false cmis:creationDate creationDate http://www.alfresco.org/model/cmis/1.0/cs01 Creation Date cmis:creationDate The object creation date datetime single readonly false false true true cmis:checkinComment checkinComment http://www.alfresco.org/model/cmis/1.0/cs01 Checkin Comment cmis:checkinComment The checkin comment string single readonly false false false false cmis:objectId objectId http://www.alfresco.org/model/cmis/1.0/cs01 Object Id cmis:objectId The unique object id (a node ref) id single readonly false false true true cmis:isLatestVersion isLatestVersion http://www.alfresco.org/model/cmis/1.0/cs01 Is Latest Version cmis:isLatestVersion Is this the latest version of the document? boolean single readonly false false false false cmis:versionSeriesId versionSeriesId http://www.alfresco.org/model/cmis/1.0/cs01 Version series id cmis:versionSeriesId The version series id id single readonly false false false false cmis:baseTypeId baseTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Base Type Id cmis:baseTypeId Id of the base object type for the object id single readonly false false true false cmis:lastModifiedBy lastModifiedBy http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified By cmis:lastModifiedBy The authority who last modified this object string single readonly false false true true cmis:isMajorVersion isMajorVersion http://www.alfresco.org/model/cmis/1.0/cs01 Is Major Version cmis:isMajorVersion Is this a major version of the document? boolean single readonly false false false false cmis:changeToken changeToken http://www.alfresco.org/model/cmis/1.0/cs01 Change token cmis:changeToken Change Token string single readonly false false false false cmis:name name http://www.alfresco.org/model/cmis/1.0/cs01 Name cmis:name Name string single readwrite false false true true cmis:lastModificationDate lastModificationDate http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified Date cmis:lastModificationDate The date this object was last modified datetime single readonly false false true true cmis:contentStreamMimeType contentStreamMimeType http://www.alfresco.org/model/cmis/1.0/cs01 Content Stream MIME Type cmis:contentStreamMimeType The content stream MIME type string single readonly false false true true cmis:isLatestMajorVersion isLatestMajorVersion http://www.alfresco.org/model/cmis/1.0/cs01 Is Latest Major Version cmis:isLatestMajorVersion Is this the latest major version of the document? boolean single readonly false false false false cmis:createdBy createdBy http://www.alfresco.org/model/cmis/1.0/cs01 Created by cmis:createdBy The authority who created this object string single readonly false false true true alfcmis:nodeRef nodeRef http://www.alfresco.org/model/cmis/1.0/alfcmis Alfresco Node Ref alfcmis:nodeRef Alfresco Node Ref id single readonly false false false false cmis:versionLabel versionLabel http://www.alfresco.org/model/cmis/1.0/cs01 Version Label cmis:versionLabel The version label string single readonly false false false false true allowed PK!6~,~,9abilian/sbe/apps/documents/templates/cmis/type-folder.xml unknown http://chemistry.apache.org/Y21pczpmb2xkZXI= Folder 2012-09-01T22:04:38Z 2012-09-01T22:04:38Z cmis:folder folder http://www.alfresco.org/model/cmis/1.0/cs01 Folder cmis:folder Folder Type cmis:folder true true true true true false true cmis:objectTypeId objectTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Object Type Id cmis:objectTypeId Id of the object’s type id single oncreate false true true true cmis:creationDate creationDate http://www.alfresco.org/model/cmis/1.0/cs01 Creation Date cmis:creationDate The object creation date datetime single readonly false false true true cmis:objectId objectId http://www.alfresco.org/model/cmis/1.0/cs01 Object Id cmis:objectId The unique object id (a node ref) id single readonly false false true true cmis:allowedChildObjectTypeIds allowedChildObjectTypeIds http://www.alfresco.org/model/cmis/1.0/cs01 Allowed Child Object Types Ids cmis:allowedChildObjectTypeIds The allowed child object type ids id multi readonly false false false false cmis:baseTypeId baseTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Base Type Id cmis:baseTypeId Id of the base object type for the object id single readonly false false true false cmis:lastModifiedBy lastModifiedBy http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified By cmis:lastModifiedBy The authority who last modified this object string single readonly false false true true cmis:changeToken changeToken http://www.alfresco.org/model/cmis/1.0/cs01 Change token cmis:changeToken Change Token string single readonly false false false false cmis:name name http://www.alfresco.org/model/cmis/1.0/cs01 Name cmis:name Name string single readwrite false false true true cmis:lastModificationDate lastModificationDate http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified Date cmis:lastModificationDate The date this object was last modified datetime single readonly false false true true cmis:path path http://www.alfresco.org/model/cmis/1.0/cs01 Path cmis:path The fully qualified path to this folder/description string single readonly false false false false cmis:createdBy createdBy http://www.alfresco.org/model/cmis/1.0/cs01 Created by cmis:createdBy The authority who created this object string single readonly false false true true alfcmis:nodeRef nodeRef http://www.alfresco.org/model/cmis/1.0/alfcmis Alfresco Node Ref alfcmis:nodeRef Alfresco Node Ref id single readonly false false false false cmis:parentId parentId http://www.alfresco.org/model/cmis/1.0/cs01 Parent Id cmis:parentId The parent id of the folder id single readonly false false true true PK!YU))9abilian/sbe/apps/documents/templates/cmis/type-policy.xml unknown http://chemistry.apache.org/Y21pczpwb2xpY3k= Policy 2012-09-03T15:00:31Z 2012-09-03T15:00:31Z cmis:policy policy http://www.alfresco.org/model/cmis/1.0/cs01 Policy cmis:policy Policy Type cmis:policy false false true true true false false cmis:objectTypeId objectTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Object Type Id cmis:objectTypeId Id of the object’s type id single oncreate false true true true cmis:baseTypeId baseTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Base Type Id cmis:baseTypeId Id of the base object type for the object id single readonly false false true false cmis:policyText policyText http://www.alfresco.org/model/cmis/1.0/cs01 Policy Text cmis:policyText The policy text string single readonly false true false false cmis:lastModifiedBy lastModifiedBy http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified By cmis:lastModifiedBy The authority who last modified this object string single readonly false false true true cmis:changeToken changeToken http://www.alfresco.org/model/cmis/1.0/cs01 Change token cmis:changeToken Change Token string single readonly false false false false cmis:creationDate creationDate http://www.alfresco.org/model/cmis/1.0/cs01 Creation Date cmis:creationDate The object creation date datetime single readonly false false true true cmis:lastModificationDate lastModificationDate http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified Date cmis:lastModificationDate The date this object was last modified datetime single readonly false false true true cmis:name name http://www.alfresco.org/model/cmis/1.0/cs01 Name cmis:name Name string single readwrite false false true true alfcmis:nodeRef nodeRef http://www.alfresco.org/model/cmis/1.0/alfcmis Alfresco Node Ref alfcmis:nodeRef Alfresco Node Ref id single readonly false false false false cmis:createdBy createdBy http://www.alfresco.org/model/cmis/1.0/cs01 Created by cmis:createdBy The authority who created this object string single readonly false false true true cmis:objectId objectId http://www.alfresco.org/model/cmis/1.0/cs01 Object Id cmis:objectId The unique object id (a node ref) id single readonly false false true true PK!}>--?abilian/sbe/apps/documents/templates/cmis/type-relationship.xml unknown http://chemistry.apache.org/Y21pczpyZWxhdGlvbnNoaXA= Relationship 2012-09-03T14:54:50Z 2012-09-03T14:54:50Z cmis:relationship relationship http://www.alfresco.org/model/cmis/1.0/cs01 Relationship cmis:relationship Relationship Type cmis:relationship false false false false true false false cmis:targetId targetId http://www.alfresco.org/model/cmis/1.0/cs01 Target Id cmis:targetId The target id for the relationship id single oncreate false true false false cmis:objectTypeId objectTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Object Type Id cmis:objectTypeId Id of the object’s type id single oncreate false true true true cmis:sourceId sourceId http://www.alfresco.org/model/cmis/1.0/cs01 Source Id cmis:sourceId The source id for the relationship id single oncreate false true false false cmis:baseTypeId baseTypeId http://www.alfresco.org/model/cmis/1.0/cs01 Base Type Id cmis:baseTypeId Id of the base object type for the object id single readonly false false true false cmis:lastModifiedBy lastModifiedBy http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified By cmis:lastModifiedBy The authority who last modified this object string single readonly false false true true cmis:changeToken changeToken http://www.alfresco.org/model/cmis/1.0/cs01 Change token cmis:changeToken Change Token string single readonly false false false false cmis:creationDate creationDate http://www.alfresco.org/model/cmis/1.0/cs01 Creation Date cmis:creationDate The object creation date datetime single readonly false false true true cmis:lastModificationDate lastModificationDate http://www.alfresco.org/model/cmis/1.0/cs01 Last Modified Date cmis:lastModificationDate The date this object was last modified datetime single readonly false false true true cmis:name name http://www.alfresco.org/model/cmis/1.0/cs01 Name cmis:name Name string single readwrite false false true true alfcmis:nodeRef nodeRef http://www.alfresco.org/model/cmis/1.0/alfcmis Alfresco Node Ref alfcmis:nodeRef Alfresco Node Ref id single readonly false false false false cmis:createdBy createdBy http://www.alfresco.org/model/cmis/1.0/cs01 Created by cmis:createdBy The authority who created this object string single readonly false false true true cmis:objectId objectId http://www.alfresco.org/model/cmis/1.0/cs01 Object Id cmis:objectId The unique object id (a node ref) id single readonly false false true true PK!@#@#3abilian/sbe/apps/documents/templates/cmis/types.xml unknown http://chemistry.apache.org/no-id Type Children 2012-09-01T22:05:07Z 2012-09-01T22:05:07Z 4 Types Collection unknown http://chemistry.apache.org/Y21pczpmb2xkZXI= Folder 2012-09-01T22:05:07Z 2012-09-01T22:05:07Z cmis:folder folder http://www.alfresco.org/model/cmis/1.0/cs01 Folder cmis:folder Folder Type cmis:folder true true true true true false true unknown http://chemistry.apache.org/Y21pczpkb2N1bWVudA== Document 2012-09-01T22:05:07Z 2012-09-01T22:05:07Z cmis:document document http://www.alfresco.org/model/cmis/1.0/cs01 Document cmis:document Document Type cmis:document true true true true true false true true allowed PK!ݜBB;abilian/sbe/apps/documents/templates/documents/_macros.html{% macro m_breadcrumbs2(breadcrumbs) %} {% for obj in breadcrumbs[0:-1] %} {{ obj.label }} / {% endfor %} {{ breadcrumbs[-1].label }} {% endmacro %} {% macro m_docs_table(objects, edit=True) %}
{%- if objects %}
{{ csrf.field() }}
{{ csrf.field() }} {%- if edit %} {% endif %}   {% if edit %} {%- for action in actions.for_category('documents:folder-listing') %}
{{ action.render() }}
{%- endfor %}
{% endif %}

{%- for obj in objects %} {% set owner = obj.owner %} {%- endfor %}
type title for sorting {{ _("Title") }} for size {{ _("Size") }} Last Name First Name {{ _("Owner") }} date {{ _("Age") }}
{{ obj.object_type }} {{ obj.title }} {% if obj.object_type == 'abilian.sbe.apps.documents.models.Folder' %}
{{ obj.filtered_children|length }}
{% else %} {% set icon_src = url_for(".document_preview_image", community_id=g.community.slug, doc_id=obj.id, size=obj.preview_size) %} {% endif %} {%- if obj.object_type == 'folder' %} {{ obj.filtered_children|length }} {%- endif %} {{ obj.title|truncate(32, False, '...', 0) }}
{%- if obj.is_document %} {{ obj.content_length }} {% else %} 0 {%- endif %} {%- if obj.is_document %} {{ obj.content_length|filesize }} {%- endif %} {{ owner.last_name }} {{ owner.first_name }} {{ owner.name }} {{ obj.created_at.isoformat() }} {{ obj.created_at|age(add_direction=False) }}
type title for sorting {{ _("Title") }} for size {{ _("Size") }} Last Name First Name {{ _("Owner") }} date {{ _("Age") }}
{{ csrf.field() }}
{%- deferJS %} {%- enddeferJS %} {%- else %}

{{ _("This folder is currently empty. Why don't you upload some content?") }}

{%- endif %}
{% if objects %} {% set obj = objects[0] %} {% include "documents/_modals_document_edit.html" %} {% include "documents/_modals_document_send_by_email.html" %} {% include "documents/_modals_document_upload_new_version.html" %} {% include "documents/_modals_document_delete.html" %} {% include "documents/_modals_folder_delete.html" %} {% include "documents/_modals_folder_edit.html" %} {% endif %} {% endmacro %} PK!t*p  Aabilian/sbe/apps/documents/templates/documents/_macros_audit.html{% macro _key_fmt(key) %}{{ key }}{% endmacro %} {% macro _value_fmt(value) %}{{ value }}{% endmacro %} {% macro m_audit_field_changes(changes) %} {%- for key in changes.columns|sort %} {% set values = changes.columns[key] %}

{%- if values.columns|default(None) != None %} {{ _('{key} changed:').format(key=_key_fmt(key)) }} {{ m_audit_field_changes(values) }} {%- else %} {%- set old_value, new_value = values %} {%- if old_value and old_value not in (NEVER_SET, NO_VALUE) %} {{ _('{key} changed from ').format(key=_key_fmt(key)) }} {{ _value_fmt(old_value)|truncate(32, False, '...', 0) }} to {{ _value_fmt(new_value)|truncate(32, False, '...', 0) }} {%- else %} {{ _('{key} set to {new_value}').format(key=_key_fmt(key), new_value=_value_fmt(new_value)) }} {%- endif %} {%- endif %}

{%- endfor %} {%- for key, (appended, removed) in changes.collections.items() %}

{{ key }}: {%- if appended %} Added {%- for label in appended %} {{ label }}{%- endfor %} {%- endif %} {%- if removed %} Removed {%- for label in removed %} {{ label }}{%- endfor %} {%- endif %}

{%- endfor %} {% endmacro %} {% macro m_audit_entry(entry) %} {#- FIXME: use macro for user avatar #} {{ entry.user.name }} {%- if entry.related or entry.op == 1 %} {{ entry.happened_at.strftime('%Y-%m-%d %H:%M') }}
{{ m_audit_field_changes(entry.changes) }} {%- elif entry.type == 0 %} {{ entry.happened_at.strftime('%Y-%m-%d %H:%M') }}

created this record

{%- endif %} {% endmacro %} {% macro m_audit_log(entries) %} {%- if entries %}
{%- for entry in entries %} {{ m_audit_entry(entry) }} {%- endfor %}
{%- endif %} {% endmacro %} PK!$<@<@Habilian/sbe/apps/documents/templates/documents/_macros_gallery_view.html{% macro m_breadcrumbs2(breadcrumbs) %} {% for obj in breadcrumbs[0:-1] %} {{ obj.label }} / {% endfor %} {{ breadcrumbs[-1].label }} {% endmacro %} {% macro m_docs_table(objects, edit=True) %}
{%- if objects %}
{{ csrf.field() }}
{{ csrf.field() }} {%- if edit %} {% endif %} {% if edit %} {%- for action in actions.for_category('documents:folder-listing') %}
{{ action.render() }}
{%- endfor %}
{% endif %}
{% set cnt = [1] %} {%- for obj in objects %} {% if cnt[0]%5 == 0 %} {% if cnt.append(cnt.pop() + 2) %}{% endif %}
{% else %} {% if cnt.append(cnt.pop() + 1) %}{% endif %} {% endif %} {%- endfor %}
{{ csrf.field() }}
{{ csrf.field() }}
{%- else %}

{{ _("This folder is currently empty. Why don't you upload some content?") }}

{%- endif %} {%- deferJS %} {%- enddeferJS %}
{% if objects %} {% set obj = objects[0] %} {% include "documents/_modals_document_edit.html" %} {% include "documents/_modals_document_send_by_email.html" %} {% include "documents/_modals_document_upload_new_version.html" %} {% include "documents/_modals_document_delete.html" %} {% include "documents/_modals_folder_delete.html" %} {% include "documents/_modals_folder_edit.html" %} {% endif %} {% endmacro %} PK!ٲDabilian/sbe/apps/documents/templates/documents/_modals_document.html{% include "documents/_modals_document_upload_new_version.html" %} {% include "documents/_modals_document_send_by_email.html" %} {% include "documents/_modals_document_delete.html" %} {% include "documents/_modals_document_edit.html" %} PK!a_Kabilian/sbe/apps/documents/templates/documents/_modals_document_delete.html{% if obj %} {% set doc = obj %} {% endif %} PK!#ʿIabilian/sbe/apps/documents/templates/documents/_modals_document_edit.html{% if obj %} {% set doc = obj %} {% endif %} {%- deferJS %} {%- enddeferJS %} PK!%  Rabilian/sbe/apps/documents/templates/documents/_modals_document_send_by_email.html{% if obj %} {% set doc = obj %} {% endif %} PK!0xWabilian/sbe/apps/documents/templates/documents/_modals_document_upload_new_version.html{% if obj %} {% set doc = obj %} {% endif %} PK!f'Babilian/sbe/apps/documents/templates/documents/_modals_folder.html{# modals included in Folder templates (folder.html and permissions.html) #} {%- set folder_post_url = url_for(".folder_post", folder_id=folder.id, community_id=folder.community.slug) %} {% include "documents/_modals_folder_upload.html" %} {% include "documents/_modals_folder_move.html" %} {% include "documents/_modals_folder_edit.html" %} {% include "documents/_modals_folder_new.html" %} {% include "documents/_modals_folder_change_owner.html" %} PK!+ Oabilian/sbe/apps/documents/templates/documents/_modals_folder_change_owner.html {%- deferJS %} {%- enddeferJS %} PK!V8aaIabilian/sbe/apps/documents/templates/documents/_modals_folder_delete.html{% if obj %} {% set doc = obj %} {% endif %} PK!zzbbGabilian/sbe/apps/documents/templates/documents/_modals_folder_edit.html{% if obj %} {% set doc = obj %} {% endif %} {%- deferJS %} {%- enddeferJS %} PK!ڠ ||Gabilian/sbe/apps/documents/templates/documents/_modals_folder_move.html PK!Fabilian/sbe/apps/documents/templates/documents/_modals_folder_new.html {% deferJS %} {%- enddeferJS %} PK!Yi "Iabilian/sbe/apps/documents/templates/documents/_modals_folder_upload.html {%- deferJS %} {%- enddeferJS %} PK!te#8Aabilian/sbe/apps/documents/templates/documents/_modals_roles.html PK! }nn?abilian/sbe/apps/documents/templates/documents/descendants.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% from "macros/recent.html" import m_recent_items with context %} {% from "macros/user.html" import m_user_avatar as m_user %} {% from "documents/_macros.html" import m_docs_table, m_breadcrumbs2 with context %} {% block content %} {% call m_box_content(title=_('Folder descendants')) %}
{{ m_breadcrumbs2(breadcrumbs) }}

{%- if folder.description %}

{{ folder.description }}

 

{%- endif %} {% endcall %} {% endblock %} {% block sidebar %} {%- set content_actions = actions.for_category('documents:content') %} {%- if content_actions %} {% call m_box_menu() %} {% endcall %} {%- endif %} {% endblock %} {% block modals %} {% include "documents/_modals_folder.html" %} {% endblock %} PK!`"?2,2,<abilian/sbe/apps/documents/templates/documents/document.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu with context %} {% from "documents/_macros_audit.html" import m_audit_log %} {% from "macros/table.html" import m_table %} {%- from "community/macros.html" import show_all_viewers -%} {% from "documents/_macros.html" import m_docs_table, m_breadcrumbs2 with context %} {%- from "community/macros.html" import viewers_snapshot -%} {% macro user_markup(user) %} {{ user }} {% endmacro %} {% macro m_breadcrumbs_path(breadcrumbs) %} {% for obj in breadcrumbs[0:-1] %} {{ obj.label }} / {% endfor %} {% if doc.lock %} {% endif %} {{ doc.title }} {% endmacro %} {% block content %} {# #} {%- if doc.antivirus_required %}
{{ m_breadcrumbs2(breadcrumbs) }}

{% trans %}Waiting for virus check...{% endtrans %}

{%- elif not doc.antivirus_ok %}

{%- trans %}Virus found. Access disabled.{%- endtrans %}

{%- endif %}

{{ m_breadcrumbs_path(breadcrumbs) }}


{{ _("Back to document list") }}

{%- if doc.description %}

{{ doc.description }}

 

{%- endif %}
{%- if has_preview %} {{ _('Previous') }} {{ _('Next') }} {%- deferJS %} {%- enddeferJS %} {%- else %} {{ _("Document preview not yet available") }} {%- endif %}
{%- if doc.extra_metadata %}
{%- set sorted = doc.extra_metadata|dictsort %} {{ m_table(sorted) }}
{%- endif %} {% endblock %} {% block sidebar %}

{{ doc.title|truncate(32, False, '...', 0) }}

{{ _("Owner") }} : {{ user_markup(doc.creator) }}
{{ _('Uploaded: %(date)s (%(age)s)', date=(doc.created_at | datetimeformat('short')), age=doc.created_at | age(add_direction=False)) }} {%- if doc.updated_at != doc.created_at %}
{{ _('Modified: %(date_age)s', date_age=doc.updated_at|age(add_direction=False, date_threshold='day')) }} {%- set lock = doc.lock %} {%- if lock %}
{{ _('Locked for edition by %(user)s at %(date)s (%(age)s)', user=lock.user, date=lock.date | datetimeformat('short'), age=lock.date | age) }}
{%- endif %} {%- endif %}
{% if g.is_manager %}

{{ _('Readers') }}

{{ show_all_viewers(viewers,_("viewed by")) }}
{% endif %}

{{ _('Last Changes') }}

{{ m_audit_log(audit_entries) }}
{%- deferJS %} {%- enddeferJS %} {% endblock %} {% block modals %} {% include "documents/_modals_document.html" %} {% include "documents/_modals_folder_move.html" %} {% include "documents/_modals_document_upload_new_version.html" %} {% endblock %} PK!"] Dabilian/sbe/apps/documents/templates/documents/document_viewers.html{% extends "documents/document.html" %} {% from "macros/box.html" import m_box_content, m_box_menu with context %} {% from "macros/audit.html" import m_audit_log %} {% from "macros/table.html" import m_table %} {% from "documents/_macros.html" import m_docs_table, m_breadcrumbs2 with context %} {%- from "community/macros.html" import viewers_snapshot -%} {%- from "community/macros.html" import show_all_viewers -%} {% macro user_markup(user) %} {{ user }} {% endmacro %} {% block content %} {% call m_box_content(title=_("Document view")) %} {%- if doc.antivirus_required %}
{{ m_breadcrumbs2(breadcrumbs) }}

{% trans %}Waiting for virus check...{% endtrans %}

{%- elif not doc.antivirus_ok %}

{%- trans %}Virus found. Access disabled.{%- endtrans %}

{%- endif %}

{{ m_breadcrumbs2(breadcrumbs) }}

{{ _('Created: %(date)s (%(age)s) by %(creator)s', date=(doc.created_at | datetimeformat('short')), age=doc.created_at | age(add_direction=False), creator=user_markup(doc.creator)) }} {%- if doc.updated_at != doc.created_at %}
{{ _('Last modification: %(date_age)s', date_age=doc.updated_at|age(add_direction=False, date_threshold='day')) }} {%- set lock = doc.lock %} {%- if lock %}
{{ _('Locked for edition by %(user)s at %(date)s (%(age)s)', user=lock.user, date=lock.date | datetimeformat('short'), age=lock.date | age) }}
{%- endif %} {%- endif %}

{{ show_all_viewers(viewers,_("viewed by")) }}
{%- if doc.description %}

{{ doc.description }}

 

{%- endif %} {% endcall %} {% endblock %} PK!ͺ:abilian/sbe/apps/documents/templates/documents/folder.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% from "macros/recent.html" import m_recent_items with context %} {% from "documents/_macros.html" import m_docs_table, m_breadcrumbs2 with context %} {% block documentcontent %}
{{ m_breadcrumbs2(breadcrumbs) }}

{%- if folder.description %}

{{ folder.description }}

 

{%- endif %} {{ m_docs_table(children, True) }} {% endblock %}

{% block modals %} {% include "documents/_modals_folder.html" %}
{% endblock %} PK!dGabilian/sbe/apps/documents/templates/documents/folder_gallery_view.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu %} {% from "macros/recent.html" import m_recent_items with context %} {% from "documents/_macros_gallery_view.html" import m_docs_table, m_breadcrumbs2 with context %} {% block documentcontent %}
{{ m_breadcrumbs2(breadcrumbs) }}

{%- if folder.description %}

{{ folder.description }}

 

{%- endif %} {{ m_docs_table(children, True) }}
{% endblock %}

{% block modals %} {% include "documents/_modals_folder.html" %} {% endblock %} PK!cDabilian/sbe/apps/documents/templates/documents/mail_file_sent.fr.txt{%- trans filename=filename, message=message, sender=sender_name %} {{ sender }} vous a envoyé le fichier "{{ filename }}" depuis l'extranet. Message: "{{ message }}" {%- endtrans %} PK!_ǣAabilian/sbe/apps/documents/templates/documents/mail_file_sent.txt{%- trans filename=filename, message=message, sender=sender_name %} {{ sender }} has sent you the file "{{ filename }}" from the extranet. Message: "{{ message }}" {%- endtrans %} PK!0""?abilian/sbe/apps/documents/templates/documents/permissions.html{% extends "community/_base.html" %} {% from "macros/box.html" import m_box_content, m_box_menu with context %} {% from "macros/recent.html" import m_recent_items %} {% macro m_users_table(users_and_roles, controls) %} {% set table_id = uuid() %}
{{ csrf.field() }} {% if controls %} {% endif %} {% for user, role, has_access in users_and_roles %} {%- if controls %} {%- endif %} {% endfor %}
Last Name First Name {{ _("Name") }} {{ _("Role") }}{{ _("Action") }}
{%- if not user.can_login %} {%- elif has_access %}   {%- endif %} {{ user.last_name }} {{ user.first_name }} {%- if not user.can_login %}{%- endif %} {{ user.name }} ({{ user.email }}) {%- if not user.can_login %}{%- endif %} {{ role }}
{%- deferJS %} {%- enddeferJS %}
{% endmacro %} {% macro m_groups_table(groups_and_roles, controls) %} {% set table_id = uuid() %}
{{ csrf.field() }} {% if controls %} {% endif %} {% for group, role in groups_and_roles %} {%- if controls %} {% endif %} {% endfor %}
{{ _("Name") }} {{ _("Role") }}{{ _("Action") }}
{{ group.name }} {{ role }}
{%- deferJS %} {%- enddeferJS %}
{% endmacro %} {% block content %} {% call m_box_content("Permissions") %}

{{ _('Manage permissions on {folder}').format(folder=folder.title) }}

{{ _("Export to Excel") }}

{{ _("Inheritance") }}

{% if folder.inherit_security %} {{ _("Activated") }} {% else %} {{ _("Deactivated") }} {% endif %} {#

Current local roles for groups

{% if groups_and_local_roles %} {{ m_groups_table(groups_and_local_roles, controls=True) }} {% else %} {{ _("Nothing") }}. {% endif %}

{{ _("Current inherited roles for groups") }}

{% if folder.inherit_security %} {% if groups_and_inherited_roles %} {{ m_groups_table(groups_and_inherited_roles, controls=False) }} {% else %} {{ _("Nothing") }}. {% endif %} {% else %} {{ _("Deactivated") }} {% endif %}
#}

{{ _("Current local roles for users") }}

{% if users_and_local_roles %} {{ m_users_table(users_and_local_roles, controls=True) }} {% else %} {{ _("Nothing") }}. {% endif %}

{{ _("Current inherited roles for users") }}

{% if folder.inherit_security %} {% if users_and_inherited_roles %} {{ m_users_table(users_and_inherited_roles, controls=False) }} {% else %} {{ _("Nothing") }}. {% endif %} {% else %} {{ _("Deactivated") }} {% endif %}

{{ _("Audit Log") }}

{% for entry in audit_entries %}
{{ entry.msg }}
{% endfor %}
{% endcall %} {% endblock %} {% block sidebar %} {%- set content_actions = actions.for_category('documents:content') %} {%- if content_actions %} {% call m_box_menu() %} {% endcall %} {%- endif %} {% endblock %} {% block modals %} {% include "documents/_modals_folder.html" %} {% include "documents/_modals_roles.html" %} {% endblock %} {% block js %} {% endblock %} PK!e T T<abilian/sbe/apps/documents/templates/documents/view_pdf.html PDF.js viewer
Current View
PK!e,abilian/sbe/apps/documents/tests/__init__.py# coding=utf-8 PK! =abilian/sbe/apps/documents/tests/data/dummy_files/content.zipPK 3E existing-doc/UT MTMTux PK 3ERexisting-doc/file.txtUT DTDTux file in renamed folder PK ֌3E"existing-doc/subfolder_in_renamed/UT NTNTux PK ֌3E9Y!!)existing-doc/subfolder_in_renamed/doc.txtUT NTNTux test subfolder is in right place PK ʌ3E folder 1/UT MTMTux PK 3E嵄folder 1/doc.txtUT ,DT-DTux document from zip! PK`TE=a folder 1/dos cp437: .txtother docPK [eF\ .DS_StoreUT /T=0Tux dummy PK -\eF __MACOSX/UT 0T1Tux PK -\eFuk4__MACOSX/dummy_content.txtUT 0T0Tux this file should be ignored PKaeFfolder 1/osx: utf-8: é.txtdummy content.txtPK 3E Aexisting-doc/UTMTux PK 3ERGexisting-doc/file.txtUTDTux PK ֌3E"Aexisting-doc/subfolder_in_renamed/UTNTux PK ֌3E9Y!!) existing-doc/subfolder_in_renamed/doc.txtUTNTux PK ʌ3E Afolder 1/UTMTux PK 3E嵄folder 1/doc.txtUT,DTux PK`TE=a .folder 1/dos cp437: .txtPK [eF\ n.DS_StoreUT/Tux PK -\eF A__MACOSX/UT0Tux PK -\eFuk4__MACOSX/dummy_content.txtUT0Tux PKaeFjfolder 1/osx: utf-8: é.txtPK PK!,44=abilian/sbe/apps/documents/tests/data/dummy_files/mugshot.jpgJFIFHH XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)KmExifMM*JR(iZHH!wC     C  w!" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?yg(p?΂s=>Fzz^j0}EοZ\qJPh3^#v2@i<*v 803Hp[=H#POsSk!Sy<jnJ4&cHb3#9jggZ3h9~)zF: ԃ%rjB0܏(u Jhg'O#M<j`u>'4?jorBv-v"ӜTD]MRqN#ϽNz?{gO#ߚ0x9>2/FI;~<0<旒rp)K!&z8I(.S܌Ը%3F1韥MM &2I$'!x搶|Q"sӑM^O>xl~tTOjmU)c?b:\o^۟ןqI Äh^t37k<|?|6=n?ҏi.焙vmnFs~^#?f x}(~]i2~F ~^#?fڅx|E/H_ڑ?0:L~ʓ$k-PɁ'Kr-fbsF'*N'WYy v3`FAھZ~&o5c晿<iGDθ\(b>mbGsg: }E01чOƽk#C/ ̳҄h rǧ`qi6Ejy6ͼ?R(r&ÿxCMWIBN戁ʴDR888 uv|3[yoro)`V0y;i(;N~/ĞDeFG3*"?(ۅ8~ٻH#>J}}j647yIGԦT}h2rpk<͚gh3Ns40Ѣ~zy7tk%f7xK߳E94^0E.܌ۚČGFf{sΏO)韭!#տfMFi$!5?TO1q^;iy}f[}lDfx"jv+B>E} ƿɿ:(A}~3O09@ f5d&yLmğJ gHA񠥱2OJu#𧀠0sC)In4}( v8'um\G4 oϽFޟ[֐@+ OjRJWltG@%ң\ך|Ze9%?/N'gnc hUƥ_Z@B0F9sĎnťXjӍ/RԤy|95qmlT=/9R 84ZճrW\d|%ּG YWf73p^O灊ckT&bN$䜞0:ZͤOķ .㱛mP g*b>-4_^>l-]}kG!w%xs%6>Vy <F>q8;(Udr#l;zk'|qBX ync|NQӯw¬m{lJ{4j9aXLnzp:^ KY;S#px,&ynJycxrJ7u#9G6"$# v5},&]1H:g֞W g8n*N x#?JRqN y~\=H9-)x@$Ff8#4`n)qmim pUW*LC QPV 8$~`繧980hQnA$Ӷ(.;-6ғ9cN뜎 qw&U.'ҭuHI" W}㝠`׽yw,3UE'[Y!9n$`b0$ywt⼬?t$s)Qyt|qZzƯFKaYܳFQcA&C2\wœuG_ǫ ;OKR;W2\mX&$7U¨pO1?BVכTԗ^T"sHbG q s'n2YĊp?y ^\n{BBqp2stY(BZux>p)iYRf-ibKt$#IힽkxWu-e*mo-nP&N3*Y:'e?x;;WЯ7\&R80?|'$3ݙ,& >8(Õ!k- 1qX8Yn̟QOEuY ewanfFo,9+p^Iֿ 1+Rą"Ĭ#cbΠ' $9]7 ,-NV#\Hc'1K&N#BO|3jQzNhi0HN$iq]g!6>G>FRMUĖǿcN=i;`* Z:Ҝm'>mFpG'Լ3֗{g"HvOz(Tӎ8@ڀ2NO>i:z~#1@8CHl8ڜw,p2)H )zqJS9v0zJq19Oq4"{\V|6#x kd"v#h[s_R5Y./#E=I5pjp^k-ٮ.pU^O9UeoZ)wR6YR THn8y>wewVJGpK<I󯃿eύ^0WZY&_Nx .;>bK],[Ns!_Np+[Dl`@Nczl#W_bI MF{rO)t\`a _2H9ʼՙWYN2]O+fxkw6 tRVQ׊t ap{wr2 k谔h5z"U-'ZK yѷrT ',|=)PwPYs͚NG,cV72n;W秂svFF@g^Oj̑f] /ҸN}Ref_^|}_m'&>4N~Zͅ'T`~Vzgu/v:V!Q e*CV}OWŭ=;e )V y*A}{~6hSZM̊ !=;W>BK)7Ϝ֥?n>ᮙchQ!-F8硪5juBmmÅ,qFpqZX81_ҳ?m7q1jҎjvj=M;hTgN3Uq"<{qF:;u4`gsF`ig'8PO=q6*N= 4V}jm`N2F95v:14w1\9G<9kv_ fZ;Ov7QJ8'9x᲼[Es F3Ƕ^ R5=  5(#sZ\y<~$WOtGǶS7r8N n@ slz\I5m@2B=W|~%otN &mQV#b;_pe)EO|Qys:t~xOYЯ| c&c$ZtC̒=PB {kUT0TfkVQHs_`Ԍ<~!Nul.ӟR4ryܜU&?0$vs@y)IEƅQ>ޣ?֔sǭnԜ`zҁZr}F忽QOڊN1ǽ)0 #ԉCW,,GQI@5fؐi_RչnZO4Z_^i'=tJw'g4`nuA/'q q҃O9S{x;۵/sL9q@|%QA-Λ4P` ` }׷~Ϛ='c$!vm]ӏP^𝎯 Oa$8? 7S[m#ticPNW~7TKRRo?8J'Zx?~({ hvH)5N7uVa{x4kV񇌵=*{[JYKa>^~xN<3*v#"o=*=V?R3[ڽߑ,<8lWK>O{.W3f.?jw|W,-yJLȄvt2>]uG?u>ڸT`~&F5 0*ȃ +Gm] ՈQ HS@8vE k7$T)+ֹ|Ms S6A;40z x8  fyě:h%+Y!Lgj2ϱ'ssuDe W߆5Y\G |<3_M"H1o9_lT]X;p_\xKVtOAy5v7a$gڊGlQ.\ׯνUoZ-{0ThNxN,`9-ċ$=z=Wy 9QnΣNn,K\sv`@ ?vJ aS<(eۛ $BBMA|[>x O8 KD*C8\tMSɥ D)ʽI%u~K-n]9}kGѺi_c_?~_>#|9:{x/"\<&L6|Gp{=ڳ_'hg#sd vXj|3m=LS'uRޓ*IoIlJ# 2{֘7'7::<&*5h^ozq_4Լ? %|QVc8cB+꛻OS:[Сm"Gcj?}#H(W?!&>cqrLv_=rVN#4چնYݵ?睊=R*^xFٶnOšX㜚XB;fG_B5eNxܔβ*v}+ r2G`^#99^/cɻ[8 { N88}i;`E[Nx֛ʑ\sfn/"k@0TR'+Y#o"|Ǟk|^I9#f1N266#H%꿴4W+z\GbylRМp֗k+w:u?ƺ:f#Y4w`O99Uڗʜ1֜-֓5^nq@c:b4sBW q}q(Q^Œc8z3`,>._@q\Gl:,}yҼ, kyǸy7חPi#W}1{_ۮ Sb@̟}X<ڿ>M-4l ~_'_A\L #IRی0̲Ws+l$y1$?~0Ƒr޹_\Hk qzZqy/]rz֙q=u{ VR9qs?uCOsseI3eˎP;a ʋm-{Y"-GE| <= {9' R5|s?_GGiM]5e7+pF[zOoNKle<NxZ]_F#i>/^Oȯ3¸pX99k0鴶X> xeHe\qI48,Oj+fx%WJπ׵[g$sֻMiV^qTupV鞕cvbұ,~XMWXcΪ mQ3WqɯF%+p m1]yTR]t(5P9" F!-&Sh,6#b;U&bdں(t}j[HclM&Úۜ۹PÜlDVboUOc=)(=EQO GkdsLp?Ƶ1ßBg[qXs ڠ}F`5oCO^igF[n;O$ zMNHGdRNKn8>8zǵ|p_P vrE|Fg;6p;My7$Ȓx?v1^]J]ᏉtPMÌ8?L_2\x "׌޻f[{ %YZ,|=4WrJćnھ:/z־%ɴ v<@Q֙\'DT$q8biS"5z"kG5oU/k.I%TN!9WֵG\E|)OƄZIHˮ +εs'ۑeFY$$P07Fd-nL%@ƹ`@}#zewYOq-8Ix8}nzb|CS }=rqTQݠ%_({ WX㴱eڻ8byu4mJ[K9VH%##2lt9:7}= ɖP#Sr` H6ocW93kJu.xWf5L:OգQ}~.5gƍ Xc_k ,ws6gɵxP; @wu|҃\Zy%#:W#o}֙*^Da!GhY];$T|6(Ԇ&oY,& ҂SzhPx5-?|Ϧ/" ' 1'qjz[Yg L{:BNOZ|7$r BIflOOq ?KG~Ϻ[i-qlǹcҼ`副WRybEL#LMnxk36W$Ӯ.RJPLrDV#AA沢Ie=E|OWr'/1ӆ1cg|㶶:6'dB+1Ìg5]ytLw1_]F8GSNĺJN$]΃}M5 X:#}s^xjkr}ysѕ:x3P\cvևgH:$zv6mi5ҿ{ FRvsRv6` iv\ScL\IkUؗՑgC"g*4r8S1A)#}GE&Ed21M)ǥ7'JsGև20\`jΫW@&Nǭ$-? 3FM7:ݮ5īĚ}CG}:vBsco$ߑV>&k =JW=['8 _>~~LXh󴲤*2Hqz #MLm<vS OAP̱ǗdVV.^"{sۂ>koo/ \N+y5ipgH4:mOMG]F5+n[XDetWvY5㡯n褝'* Vtplj,faybI\pΠ5Qиv0rx7Ť#dL9i{y+û9#X{amc~n9|3o[8B/deIO6|W/oD'p\n@; o|;u??H\mNƛUp e3=A+bjԖթl]E Qrť4?4,Ę<u#ֹ/m4E1-e*ql7R)71o|twafA:=k'W}3~Ø ůwFI4d7 Bo є'mj\Ν rc˵IQh Dc*H1%,įV5aA HD= s_i_ycg]SN{J P+ 6+$`צ|S#9J#4}r74x~; \`8?Vdϊ2[?hn6g!d k 1,|8e]+>"xqFm?.K޸mm>fIHq X2GU zUFogj۶kRSNp?S}hΎTvڅޣwkaje?M|WLu=7VVe;ڋ%Պw'|۬"u9f5OT'{AߧrNN)jwjt-BdӤ%“͠>׍ߴuRhi~ H3L=8|,AݽmĖ"|5|Yw&Ѧ,~1Uc")\>\Kt496gtD5ﲮc<}kة_ ~gx; x&Kצ-ku )#Xt/K(xZ]>; 6Fi0aYP:-~RĺؼIe]?Qc7xjm~o)_Ž*Jn'VF]sB봡 2!r2~O#*N>6Ox+|eV};Po-djp0!,uC+Լ1mWFwj2vꠡ4i J`xWMtE !(Ɏ[#g</PMumyWfkgO;vuWw3W[W4¾P y\m D)鹣=+\Dc% &dy?Ǟ7_e/VKxկ H<sBGi/#Mt}'~OdQŷq';ؖbIbI|? ?ܷbe Q~`Fzv&./wu[wж 2+4eY895,*ʱ9h8![|1^Ϻ6rñ=1|=>Vmg IVHg ➇tI J[vev{hbA8<UAUTޜB$Uw4Q!2:ԟϊd3M&>Gn{Yg ֘L_~"dIj][[^ؽ 4ox]`iƅ{2X_ʺ#F2?_n ! ܝJZ&_=EhM.R% !|?ڛɒ6-LS@ň0` XҳjYKR ́ Qu[\:й?W} SU:my hCOz5AI..t;:]ވ1{cau|GwU!Qج1w |Ergp+6~#u]q<CnݘvZ!*s_^l"2FsJ[{WvlaWc8V|[V2ܪT(SZo|5jVw~ML?>KwgXK`ddCIۮ^ȏ |")<}}m;^tn՗@9 *7#^گuޥxi#uY«O*38dWGK'Vr{㯹5.Sbgܝuo>cSª =O9j|ұI1ש}N$"dUU@ WW۔?r2 )* 3UeY:q=$Օ^R7,N8=*e^F `_LƮ]l$p5 `O5u1\sT3/H* ObK m};J|0° Gl!J)`ē*SW)>U > ȹb=YeJ7jJ#t;X<2Co  ?R,< 5K0yr-N)MZ^Blr,_5y֥q>Qʜ.ؐ=e.ST ֋zZ[3|p⽌MMǹ[cʊ_J+g:kJ·WhBK\F_z$WZHF;vִ<1} !D AZZt>◻Y,%m'fr:XM/ZQo =Jh>ay㡷2HUi z!S\|7v09s}ݩuqE2/Fr_Ld%p[AՕ#N.񜞤SL(ĥYIyRB" #8G }OSZ$tI={{dI9Tu]\c&ǒW'Еv\J;@ݖl+?-lf<Tlș6w +.vߕZŷ;5^BAs8ղѲ}U(oNOSUlF9Ϩ#zАw-ӟM}qyF:RHDNzG+a88s\6c'cWQ۰t26m B ;N?LVZ\ORWpk߿eitbM6I6ër<4"^{G_N9_)+ZRJG;\~(ƻpQgQl~)|V~i?Q~tWeBWQHvLz]6K'6l 3]GnKKU'>?ۖ āӨRp8-_x6$ m$ `kυ,Omzoc<< x Mr#[k7֒a?zWHUvfVi5BGC^ެQLüDKs)8^@ͣFc?R8JIų$|bvOja/;97NJe \zmA"\RZT|㖌5euW(*I}%R)Xd#c5Mbmc^0Ph9b\n%ם]H-ͥn)u҄nEZfzZy}<`%7r}Oa_| t6F̢k\;@ |3u9|[4*ڶcx&8؞O݌5Q1=?~ \OƋ_!k._jY 3ϩ>6h+Ѡkg']Oa0x*](w\1r>5)65GiZ׌<=1)`9Cȭ=Ee"0 Z}"W۽p2'-NKyV&\g7{i'®"tO]|W xH"!vUosq$^}!-z7 [j6HX "sk?*j_hZM=$@( Ah][ƧA^E ^Akfe<`z޺9٭ojٻy;Z?xuHs3mPIUXr{_eo]kɹkhZi~ yH :~?G\Hܮ~>7êKضQ?5Av!v0:(?(}&Z 6̀E7z0[j#һcF4mMJ]uD ]ROL̟\\~1' Tv;l/]0BM[6Z^NN>3Gf(7K#隷ss{GI3꼳= {U2M$+3{UՙhRI6 gR:c<㊴]ŰWeɒKPvнoal:]XF.[>B\SgiHbz@ǮsP [MW%sS˅q>bFa0oL{ž G󫊽7dRoZlBxm+tUU6N+W9qay0ґ@ʹzQNwmz[ =: ^tTxmڇ*ܽm,>8U}'[O?o^9r~TWg;8y#=sS={odeH94-2p\Dː>5P'ǖfЮ؏ȏzOCve^9'Oֹ?a-n,΢$Grr~0i17rX13ϥy^'#ӡ%XĬyO'|}_wG509!zW'}Zq=sP|Fuxu뷷V,ddcx9oaW]yWۚ1țIel_mGTk9-bY?{:㏈<-ax*]"ڭlc)uKPF{8G2Suc %KqNWfs+ᯀ~/i/[iڤ]n~l< !?"{fV[$5k+E%]e2vTP>)(p˞'N3M:elc0xfH#}]h-{W}A4cYDsfZo2krCy qҪcnf 8wVK>oX%Sf\/sH\+<5cƺf_8kRCd#{>8 ҢMH7h+e F}wG# TCvcFӏvA6Zqd_?3,]Lf%k)/{>*OGy<'zd>JF@2˓!TcwQGsO3  }GS?ZP˖׹{Vq=]`+?|P C(#!%Jy*fW>nTxјi!*;pe˰߬`Vg?ytǷ=k VS{~NDrUI8 })PÅeP޺ y#*s֢̗G?;~UePh)ݗA!y,(ʐ߯\_pLic%ܒAϨԚ[R+Hr@O5jW4A{z6nq=oȂAK]4iݜM{u'(X guM4KX:I 7N`)17:E>CF1#U ~FܒɴZR39'=XS ^Sueh&GEs۱r_ΊH=.Խt{Ukk>o(Ѹ^֗M;IsEw3A:Jok5xOC:<TN[&C&8$ v=kmo <+#?0<]W^.[_+*X}@ݎoepf1{@ v|R8(ӛ^Q` 2vnXHGP UЬwS׀TzVu;B`&i(ݘC ܿ0OV&G{z7RJS$TI=hK!#q8fCT2jR챢EuzˮЮLclrzix5x?j1O&e}99{¯Kxַ&/ 9۸8g{=n/麨umj2ӠFp>~Q_K5|N*W&R?:OUy'"/̉w%D O!'h*9_~ho(|Ax%D¿zF`2B =+M??tk[XۋeKc'޽!5EobE9,r{W,F1)uf/%ma]6mbkx|z7y~/vHȟ^ԣ ozWw\ljKIJ|ާkZigmFpr^!8,{E|9OJ-?{^-’ B O-W?'du*ܰWc&nX|Gz?ErFn'85jt'$!T( tW#:T՟^Ԕ?56ՒwQ G.WIEGk|?լ̐k\'?ʻi6/>rFxq#¬s=njڣyAqU,Te :SRBiVQxKsZ/y KVC l?1?z ̲ƮpObz"y4v̎oNF"Syv >`+i'z (3bݓNб+(qFKr9EI:GlJruW)Z,ºiM\Rҳ:C*y8gFiZ4gڰn"'*R:Z2ڶvWf.F֕$p6F?ھյWgU_elȖIG}#~8@V»ğZqulV<5U 1V$-<;}[]*MFv9):׈cQ毨OO;o-ǰ_C$pmﵫxKL>u"FR}5xW'þb,wbY'b1=q UBQsOx]B~HvGFZ:Z[σz4]Co3 4zV {ۥMI.U=Qn\Ֆӈe}pHfniT댰995g^`%n[;D~0:wU jA-Y RD„~gcU_ǻҞ~Χ7@# d85I+R Be^~Fzsy)1&yW=ss#G+yⶥc\\I% ĩqV4X%v =sUofՁH Zt̊D!T19W ݝ,m[M2ĺze0z?zmQlQ krQU`X-?v:UmJE&ʬNNҝ5? +ξϫ/WcܟjL~Π$|zGFIBUf9cQHr8()KMNf;flBpO#]Hw|C< y8O+T m ?SsbkhG5B][K@F %Dmj|7漅q*dUNLv{kD\`8YvZ6_!$ ; P$^nG?e$+EwJkTwwiڀX##ֹU%ӯ)X =O3nBqtI纛WM&DU;-`r9݃ZSuknhIwŀsWtF[-N)c+{~5Ǥ)df qMRwQ%ycϫsk űu!ȜuOt_tx7P]g 5-AbƬ?uj.]LQG;Ai ;L].SlD`݌:_ǘTMBmhW'*be~7_ˣ'Q \6% +JCvJn,_<9ko[xş<9oOu]7A[]- {{eD|bf1Gk$6OuKyw+sLOlU@랕 pm\SjOEn?G`ow~G-=왣_4e +"Y_hImy >hݹxǀ_-g] #E0qT֩z^ҧd<lPT}Z|giaIzqkФvxk|8t|)졉tF^u7|fOxL]|=s "rA>X"Qk@a7cl pVK8d(#1ֺkO! nk9NM!L4I +oEg{ldUJ7vӥHupϩ?JnŕH^vf&g6I9;3TS7.%UlCu?"mcSpyA\շc?ֳrteCs Ah×CUIm[).H+'P<=G4ua}j2AS٫)݌;npxoy+485% ]qq.NIϭS]§Eeq?#lNjJH0ҹHVuBimJ^md -~>hp+I@?S^ZIJi g`?55#q_i!ny# J*Q}}V'Txvy 9~˘~]|=Qr;SAN>JM5ZF`iPW+/?;د|H2q3p 6_? xWL}y~kz/:qPx y?4|3gQ ܚ|><R݉^/&tf">_Ƭ5RW? Ͳ,TG5܄ς@Ec#lsij0M-&q\|OkЧԕom[_Ca3\sv?8JMb\D$ 2*I4"{=2&gฑ4AUq,ܼ d?)\]ߏr!uoaZRѠ{ؒF#bQ.csלx7qY&`˪2lNR+3"21{QXC} *ycOiLM\.WquيhnB{& 1Pq*Hkg\Hq;s둻H'i]6{+oV?9h N=OJͺxrS{ ɒ=3z6I?J6yӯ}eJ1'P<&;c҈abY*^xB aΨOr{V)+Ju]^8{kAR{G{FM (̧ʾc}X7NU잝jEbz*8GšjZN`mg)5/ҼKorF*z{k䪀[uWw~Z_CTa&ZQpb&|h.~,HV1,3'?f1:kMB DEUb7ӌ}a;c8zv+5/IC j& &奉d0ݩt3i99Cաlfegxu+}"&]3jSGMϹ~ x >|X>roqٱ GuX< H"=5nn!9P;ۧz,E$1𬃏ws)[Ɍ>Ԣ@ ?l-ŗDOpLs"X=Fc.8?@Oʻ[- ;Z'H$لAx98'⾰=E|)pJI|p=O%τQwA42gGjmt~?/+̲6.3n]NQmeZZ'ZH#޿4D~PGk[ݩt?~g᜺|~Gx,qR}]?uM3\\X6Z墒'ֺ񭵅xvye23>\3s_>|(ǂY Z[89f#hǦMbxǚM֧qhRm `+j`e*ϕU.kyGCGu `\I4ܓZlcfHj]x㆟}KK;7f$@V#ڸ<&_N^/)bZV7NprA7y'-bcЯ#tĀ ښ2Orkce:FH26<$ 己=E:xQO RIs.dnlei<ӿmue۹I=XY עE#E: ?O̶յ嬲fϲaQ_G/|?ں4h-̖Q"RLB]Ywr(LETv02 tot|⯉?=ޣA-:'Cw=VҊ>k4jBX qKKKᯊ ͚fNҦ0N>euQSS&mx5FT~n1zMw,\;#aFGZ[._(lIM{BN:\g)|yg7pkCM g> stream x[[s6~ϯ#5^>m.NڌLDdt)R!A78 (Jrؚl'36M\s9ȧ'̃B7YE.IYzoެ73 tG?u%YOl'F[1<[ LW7×ze_|o'O_'nH`vq,!쪘⼚SOT yNoWoJgA,מnJ٩_~X5-qiiԂX_JMSDAKn*NՐ3737"~z8I3baԼAl, ~Sc5=N-]7u+􎊝gW^튈gX[E!Ί.8q+ SO1rMsGMC]opfִ|~;BeΉpYv6sv!U sTa&~_w$ Fm/=oqb}$. ?u 49p S~]P)/GZ [.ݯlE+B8t~xr,P~3֒(vVƥ}I^1YCN4ϻUK~Tk8NT0Xܯu*&[2&wXy+b%TsDh׬y`@-]w[hL'5S՞yZ)U<_WtlxVOìhROΊ!VS cHeC 5ӐZyQCk.LiYKۋR^Ւ8uCl[ۃ^I(EVor?%}/ۡ^0H[v Js?k PbìtRC8fی-8oH9vզk?OM/+tɅ@?:D <]t\sX ~ݕ8X^@ۨxg<Z%'n z٪.%M ~ 0u >Dԍ5ZkYDj,u8ٵ&02|g):ik;a`6z={;h\E_mBw(K[FLJ H$,vmːHa F[Qy>:S N4H 8Jo9B'Mic9%-ė|UAkinhXLaP3pNоW3&c[!ZNw"DcaSKVo +4tzah7@f"j$9Ra - b٪7ȌY9mnsyA:] MƸ5-f/pM`:ٶcSm8P7979YE۱XIOGvy V˅ 7}3GnG%{oI֡YJ7b$cL/u"::Zh#`i ~0kCJ%4Fbݖ͐0vI(Z39evx@e!FK.xStb0IJX u+\pS[^:0 qX2,`r[+bPԍ{fU}f 2(潒 |s]Ҫ(¼*4R)9GنbG.E5|X;V!6T[l9_s1鵨MjWJ2!HWlU8.#heS[F[8pKm|eRJ\naM-#a"K`ӕyb*jZy0JFTaR؎˹}<:5݆ߖF3~-߉z?,)꿐;.7yaH*q V-qCzFb9MkSLӬV)s_]?}-~Wߏnpita- U37͋/(:ܝh1>%ѫOEWr|O y?QͬBP2,.Bo{L|"?a&]O>_*➲ I_S#QP LK sP|\yϚ:n '88/dtն լik>4WW&r{84&ȇ5jFht&'e%D;l6|%>g}QMS7cs*s!QめvSwLbشlL`2YVsm}3-l4xmPӲոܡepYH|4`g^vUk`E6h6bY`0,f@:*VMmBNJe$InTCQEgVIrH76Mm` 0 0 endstream endobj 2 0 obj << /Type /Page /Contents 3 0 R /Resources 1 0 R /MediaBox [0 0 612 792] /Parent 12 0 R >> endobj 1 0 obj << /Font << /F15 4 0 R /F8 5 0 R /F16 6 0 R /F17 7 0 R /F18 8 0 R /F19 9 0 R /F14 10 0 R /F7 11 0 R >> /ProcSet [ /PDF /Text ] >> endobj 13 0 obj [843.3] endobj 14 0 obj [500] endobj 15 0 obj [513.9 513.9 513.9 513.9 513.9 513.9 513.9 513.9 513.9 513.9 285.5 285.5 285.5 799.4 485.3 485.3 799.4 770.7 727.9 742.3 785 699.4 670.8 806.5 770.7 371 528.1 799.2 642.3 942 770.7 799.4 699.4 799.4 756.5 571 742.3 770.7 770.7 1056.2 770.7 770.7 628.1 285.5 513.9 285.5 513.9 285.5 285.5 513.9 571 456.8 571 457.2 314 513.9 571 285.5 314 542.4 285.5 856.5 571 513.9 571 542.4 402 405.4 399.7 571 542.4 742.3 542.4 542.4 456.8 513.9] endobj 16 0 obj [699.4 670.8 806.5 770.7 371 528.1 799.2 642.3 942 770.7 799.4 699.4 799.4 756.5 571 742.3 770.7 770.7 1056.2 770.7 770.7 628.1 285.5 513.9 285.5 513.9 285.5 285.5 513.9 571 456.8 571 457.2 314 513.9 571 285.5 314 542.4 285.5 856.5 571 513.9 571 542.4 402 405.4 399.7 571 542.4 742.3] endobj 17 0 obj [813.9 770.8 786.1 829.2 741.7 712.5 851.4 813.9 405.6 566.7 843.1 683.3 988.9 813.9 844.4 741.7 844.4 800 611.1 786.1 813.9 813.9 1105.5 813.9 813.9 669.4 319.4 552.8 319.4 552.8 319.4 319.4 613.3 580 591.1 624.4 557.8 535.6 641.1 613.3 302.2 424.4 635.6 513.3 746.7 613.3 635.6 557.8 635.6 602.2 457.8 591.1 613.3 613.3 835.6 613.3] endobj 18 0 obj [319.4 383.3 319.4 575 575 575 575 575 575 575 575 575 575 575 319.4 319.4 350 894.4 543.1 543.1 894.4 869.4 818.1 830.6 881.9 755.6 723.6 904.2 900 436.1 594.4 901.4 691.7 1091.7 900 863.9 786.1 863.9 862.5 638.9 800 884.7 869.4 1188.9 869.4 869.4 702.8 319.4 602.8 319.4 575 319.4 319.4 559 638.9 511.1 638.9 527.1 351.4 575 638.9 319.4 351.4 606.9 319.4 958.3 638.9 575 638.9 606.9 473.6 453.6 447.2 638.9 606.9 830.6 606.9 606.9 511.1 575] endobj 19 0 obj [388.9 388.9 500 777.8 277.8 333.3 277.8 500 500 500 500 500 500 500 500 500 500 500 277.8 277.8 277.8 777.8 472.2 472.2 777.8 750 708.3 722.2 763.9 680.6 652.8 784.7 750 361.1 513.9 777.8 625 916.7 750 777.8 680.6 777.8 736.1 555.6 722.2 750 750 1027.8 750 750 611.1 277.8 500 277.8 500 277.8 277.8 500 555.6 444.4 555.6 444.4 305.6 500 555.6 277.8 305.6 527.8 277.8 833.3 555.6 500 555.6 527.8 391.7 394.4 388.9 555.6 527.8 722.2 527.8 527.8] endobj 20 0 obj [693.4 707.2 747.8 666.2 639 768.3 734 353.2 503 761.2 611.8 897.2 734 761.6 666.2 761.6 720.6 544 707.2 734 734 1006 734 734 598.4 272 489.6 272 489.6 272 272 489.6 544 435.2 544 435.2 299.2 489.6 544 272 299.2 516.8 272 816 544 489.6 544 516.8 380.8] endobj 21 0 obj << /Length1 1249 /Length2 7237 /Length3 0 /Length 7981 /Filter /FlateDecode >> stream xڭe\ݺA:$TɡSnf`fC AJ:TE:n)_={=^kay& BXp3.Q!7[Il rH(GDB=܂LY3 `eI ;C-5KamAXCHO.@.m  HIn rue M6$w6@uJ a?+:8[wRn 8xo; 3?[ 2'p/eZnpqQxAw Tp0T2PR3{QGz:37qxLQQ?<mKggKO|A1 @Pl(W_$&!P7S7:~S5/@&3߄P7 hj%"JFuY]([ctUQC\E(Q?QWQ?rQB. *Dpgt=@WF3^t" G瀊lA3`f a- M -)bGU>B4_ʼn}uiCMMZ[Jf!ɻM7tJÓt5 GAMg{cʼnF]ows4Ytp>U2THw GMT?l$1sa8[1whv}p.;2҅7$}J@߆jg"kY[eٵ5_Հ  /iJ)-c_M ?ZKK9M D~}o?~` =.VQHLb[1Fd^sCl-bַ 'gh%v^尹u^'ukSHV,fMR:{tz4@ا Yǚ+m<ʞbGchewo #^J_ty'tiy剭s7[WyU+ŔpE `@nS>Cxcq' F6aGR!,HkH=,oob#˥Dmb7+fN4˄/or[Ml/6VȋeN!#5'S%yRӉS6\ͯZ0I*KEv_MQ%>9 kEnI]#>eT?8l57IDs;A}7YxτKha 4KT*}m!ցn,133;7\cOfKt51~y{eϑSڌbvdME+zA8ruo]HxH_8[o}¦5z78?q!\]'8%,[`wh^14 !\@)-wgW`o$uf&0(1d}h- qA~4(mF|i@bkv/CAϹmKhn9UeŹJm׷#EfcMr d2<]_hnlqz48Zm27tZtV7K 1.XF:LSC\_j=u[3,^zYg:[_ʏƠX j&&kf>-z."(},Y4^f)`VU8doy`k^h`5qЬbĊ:27ӵ8︌X 4BԐ #:{zV 䤓 n4rǑ;кPFr{ \^=:UW޶7L|5gmr/dغT9BE2;=W4jIBd?i"4U:>9%2-~W1(/1|],\Jndz{`%q^H ATے3h>Ggݷ؎#NbR-;*)C_3$L<'+*RiÖ20˼A/bjY1bW`a$|Zc-p<ĆL'?Z FEm8 -} %IZ*9AnYw2j_NiE_&$ e tҎk:0ҝME/4HsGJ)~|D1sw=N𫑚5NKn^hlF?d1b+]_T5i+vL}Y6`&pC;s #㤏ПPqx'5o}i H rҍ T̚t+/AKDWb4g4`:}x$X8XV%fHc6G$r1#(yj*[s5pRs އ@9;>"/tu( å/q5-[̉F`x5yK;bKzw9˂ʏ/7p (iioʗ\~aS懻i%u걶}\]r+-5GL甜vvv]<.~ Q w)E9뽧MT'=oB|/[l"ݥ6 ֵo-;co2RHsV :iDAt#BlP )Q}Z—wDsRv"dGexj y}U'xv7zK܍>6v>5re> 4}#NCv0djU>prŽMImʕ۵kЍ&ґ,Ґqaez%2l9թlPS)GQx:osPоԀDx–13v"`6@08ŵ~8ǯ7@lH6)cI6wn?`5ʲ{ ,kU H 2pe}60<0m,}nմJďmF{)dAl'>viԒ ;HM#cm\%R8<2YWEY]Sa5MD8a\r`m }윋29C+\6Ni3ל+BRJ%϶2*Zu =u*R|̳RlTLD~ )&Osؒ# <0 VpAIXՉfajQxmoxoحM](ɴe\~~靯%j E:Pj2kYOH2 =YcĊZ+PQNJ)]\dbeưQǝr?Sh\'}i)h8R(a]Y ^ HvS*H&I7ϹVrӌ}Bf7"+hf%ZCeD~>>=ڑMo6,ozl[NN_mfPiB#(/+\YHl`q#A iR+ P2WON^m StyiprGff믟]D6fI=(2+gu5=g-3"F62=b3v;! +ե8%Jr?ts̎B(FRHP)Qoʛwճ!]ۜk@GUOgO% J2݁6kJ4]%e5'! D7cä́]mXʠ.d~?˜E*yIB)5qlaC7F浮uÂUjCƖXӪɍ-xOI>fп~k;s.ZMd5ιMҲi:18 {`qn%NXp8v(!з?{ޖDIc$~W_wӨUAqO,:Aԋޮ&ɮt/-ب=%-mە儰jᘦ7*a6]J0~3} We'(1XWp0?;nB^=T~+-@E9 ,G/ dhՇ* .tw=:^U>˛~a)%!] '=b݋ص 59"ZIC_⊟$۽`q-IԽpȌThuyJ.Z^p.SUl5W*z.l^tL:u%e\\ᡫBm1Kl^䞧X%x }mX;8Z1Mj N+gn&ȑv?2{,LPh`5(h~(96 Ivt",wLh_?H)\(ar;\g /#y* WehV\/Ѹ 7)O4a?_{DiM:/lX'@g?b2]o(59Wm/-Î)ZCjs GF*]ˉ`k3cׄg1hOD/)8^W>`܂Kx_vDm^Xf>ǔJ' mO&]γ_kz%WK*DB?v 46vnH*Ѹ%y>orDٛ7Ma])k DpaҎ+KlV 6yGMNWubkd{h4s10U 6͛=)T[j+ܯ8WI];2LoR].J"m{eQLrWg2 2MѶ;g.Y7ߨGG6Vg*?\F L q|gQb%vu3z,0+7h뫴I\ "R`3O`U[ o5@%ѝ.zhzL8YYն딠,'0UACwԮֆ߆LV"h<ѥwM@%c;JLC ouאn/~tބe`$ء{bdC{@]BVЌD"D $mnt=rCHJ 2k*$J| svY6f|k [qS4_7ʴԇEҚ6);B6|:35ͿB&/$ ~IZio>~!&,Mbgϝku4PtR- tJ>$OGce|'ڜ]m*r}QE @pap> endobj 23 0 obj << /Length1 1036 /Length2 4285 /Length3 0 /Length 4934 /Filter /FlateDecode >> stream xڭgXY)҂4&AB("R"=!0@,I X(JSt^(Rg|?^{;;}w+QvE!0Zxd3"m4?-U-54%韷FL0iBDI'ibqH  =<}C" `( ʇd,yJp+l E @HC׀`O?DsA!B/Q I ο /() 3AY $z$]_I IO]|IZR20Op  ?yx`f' yP>(GQ)9YJ̓PQCPޘVb5E1G0;cJ.o閁NY.j@?1&vܸkC!=Ә+uB3ލTzkPs EG~| r D -襡Wg7o^>pg=z?52@P֧&(A|z ox"cożMCQ\Ũǩ#ZmMQoM(m~U}4V|Ar& 7Wdc4;Jg1'R~TrLS^ߌc~n漯&gEwA6?Jh/o3`#g, Ac惡H#}Pb]:}!:KߜA)y;At̒yWC_UnrGt +s6A@`!||.Q'z~͸'Y|Z9R %K::޺li`܂R䜚[N 6Mi3oj9ZhBϯġ|Oh|聎˚S<6oNVue4N^j*-K*p1T=^_EG {΍L"Qevv9=`[٥soeH{հ'Ɏ]X=k ﴙ1C"ILg7Cg ޔS`-4mOUlha GZze%LަvoM>#edcL1 '߿5^0k1Im UXe.;`xONF_$hQS/}}Xh]D+)i5TDe+|NtasK$rԭ_D;)uh$_4y8z6 }=VZsy~ԩ _Pqol-Qa}t@\]4eE ".mݜ % eAY3,'$񤨢XR˂<sMc+}߲hT l`5+J&^Qɝ;#~3WiBȘVQV,*h\ҢFSHnosmL[_F鋧iқ;vx+1ul;Nw#x$%rj]<2kqzjmbS0oD`jҨO"Չ%mH\# P W(6m,)a,[^u2ކ҆5wW룸MNo4Uω,g :,=万1` 2X:k ײ'2w4ez6JƅG̚CT]# rȪJu W0VٖY~x+@{TbJM'ʦ1XeU=GHpe%ILKuE-M#taA l݀5jמj+cnT118 Zh5 gs4${RRla':Mu!H;UȰ¢/3N.?SV\j7ZӅupo΂楛z>X֑zަB5Fm636~*X2z;Mߝ?~RtV|Y0yȀk*dB\wȪ}T;#.cu AF. z 'U\Lsjc`9!Cg:%a(]ܘ:C.͗ 4}{\(ۀ5`;m˼g` 7Y$#;{t|6GA΂U?P`BWۯx.Tx U:6t'{MNp/+rN?/[e{1ny }N㦁EK_+dpotle.߬[.['7N~yVõa m5*"~z6Jդ5- }t&<>uVap }bfbH|.vy!zX|q>0blr[kF}qiL RI.R V@6-`%~U~>qkz9y d62_OxNaw @ȶ]1*#pJ+1xOUVFGݏw錻AMTvRsKOt|V-K4e6MunNwj(D.CGE7"Txح.JJ*>VA ڳW 2ɟ`>6ovIy5\ܒA"=!}35.4y?R ruw4Pț>9/, D%オ>Ԕ zv >k"&]CmjA(+߫QBlT Nٽ}c( "UtZm1ݽg؝pG:U7uꩭpq]#UBƢ} ˵VYeLveWemDhLߑxxaw4yNJ{5|c GsN:j\FeXڀKJ&G(#1g.ղPп}g3C, !.}M1]EdU!\)yҧR͡.U iĪ22EUBhX#4hw9ЬeixAc$Ot7`DR(` n;9t,N:m\j2dn3}9A*ߗ GHy K+7@OVXC- 6YmlʗcW3n\؛Ԛt: Jdͦ|BEhV/%CN#[ #nT?`]RxO}N^I.zsLc?s[hf?'g[X]sχТ\MQ4`26/ Q39JNRfh1RcU&o4 h~/%19mg#gzrE-3*'Y\OUkbf.z3a1!mC |w&xV:`mU/r|kwh;Ɓ#GvNa⛹S'M_+j\' >b] -UL|tݺ7{l5v/k]O`x@#ӹ ny!5)7Ţ*8B۠i)BբM"M)(_lnTbt z)pAUrC&L~\;b?NSݎ 'c[Bs[N!ps5qJӗ/hG|f,ڱ0- vSK2B&ST" =9oLMД/<7!oOK2tՍ/w\wPт^R~V[4%'_l$zqv3kfڛڬx v.0 +޵PN׾8S!t3l|f9ٚpVfƺb1yì᪝ 4-05`vp"9$ΎAogZUo&Sqq)#)iw]!ղūBه endstream endobj 24 0 obj << /Type /FontDescriptor /FontName /AQWVDE+CMCSC10 /Flags 4 /FontBBox [14 -250 1077 750] /Ascent 514 /CapHeight 683 /Descent 0 /ItalicAngle 0 /StemV 72 /XHeight 431 /CharSet (/A/E/P/S/a/c/d/e/i/j/k/l/n/o/p/r/s/t/u/v/x) /FontFile 23 0 R >> endobj 25 0 obj << /Length1 1698 /Length2 12143 /Length3 0 /Length 13088 /Filter /FlateDecode >> stream xڭcxcmhl;YQcfcv1hgֺu9a+ %lyrLf&&8rrQG3<||yxYv*Q86@GK#[#5@(P:] pSKg1Im^6u+C_2"Ml=@38Fyj-?dwr kky#3ldcil]9;SjMhjbF֖&¶@ӿ,$,݁&3#k'ց-cr,/I=MY?Yq38Zt>'%nkbgjik`a9:y}bx1,mM`F[;L|fvpl(;QQqEQqCLF? `C,F? `裺.>>)zJ裞>Gu?Q]}T}cF?a3Cs1}3!/~l!Ov8|cfW?f;ǿ>\ }laoc/`~g~g?N5TehO`ePohk 436!23L]~I14Ǚ5w??s6r+Ň??:[8ڞ ~L/dv ?{Q twsgϹpsp/OǏ:z,?.= h`glZ+^0]I*bUg>h)ԺxTƁf^"ir Sd҄E״or{$NۛoOAf7ʒ /NT׈Azu\fspjJdXS+9$%@OZЮ^u=&ƌC5ĪM| xZZ\>0R# 0WqC(~A7Ğsk$~%(5`5(^*bL(||;"vmoMx;Aog)N ڴ#|>.yMTߋٝM`2:{GQ?Ji >-!o~Sp: {cgPΝgt6A=s2V?d'R ܮhI(jȹdQ(Sʝ@]mW+PT)ѡ{ m9]ZpvмyqL̃OuW hꄷ o#0>ҥN E|u;"' .sx\&l[A|#_5Kd QME _H޹:u&ВuI' HdWbwtc1QwgS`QOhZ2Y2jfz4'igL>硝6ͮ(^Ȅ \PZOG,Au: "o,`*CQG1G0eo'ǭe)?B+h e[A6<d1ZDSFLXșO5^$^h`-s=%PxFg4= :!eA$#[ Wtxt<^bTF$-lԾgA؇wm2ʇbIQ0 ؉ߴh]u& M`{̹4ydBv nXJ^n0pjA"~ӬG^eH`瞞]+;B> FcbuZҁtGT59 }}x-#Kng5}ȟt) OeP%Q[Qc4,t\|#Fd^;%{-' -/Q֧C_8Cbq{Ƨ-remϩ0^FF%CE`uz׸yG aނB~Y֎rtz1$XxVC' 9 tKwUb?6A<^sz%gF^N^H! Sm)i~ xwZ@x'ˆ -/[5#e@. !m rص%YMoKk ]3 l-%8wc'J/hQšAcMO2m|ؑ]G s"65qґRVPzbQPEFOVd cJ>H2pw2k7v%$#J2(o+1*jv vN{hL󱹾4뗑 l-f/}y2̖v78Uz߆CIx-LU,>=R A MBoS O$F̊~qAk_Lmi,g!\o/^N303$3x S(pItD)݋\O^# ď:%GIzyl_tW8UU˘nљ{h1OQRؼXZ1( uBf=E@pjm1 (3?lAB(Tz7 D(03c[iq[PGO(p+)9tܟh[Ŵ@#p2Fل@2F`Sںᐕ`'*BJ xJ(S%5}px}N]Pyu0-$J uʣ$RԴS &)gm 5ymkK;L<d4XxuF!OkP vő'Eqzz)ׯL7? 0hOmPPB.~*JKzaiD6k%.O=iŪydϙ>[reV\4~:856*Ap$+푗-V= [9 BVu' (u<H{3u-oHʳN AkW R?TByeBu<0+st$9[[ϕZfzD%~]u0fҽ$^ tWfF_YsDR}"*^0?!7Ɗ-<:ƻu~8n!~i#:\x[#C>h\"%¯JU%.p2@B$uzm'8Dk˩5V:b$D&XUS[ҝSc~j?~i=╔*+< iR:iC3ǖ['\*BKnI^y۷(+JW\>\iq[&#)_B  C7 2.mD';.lXq[UX{k14ˀ'k $cyϢZw&#ki ħ#i?0hP4Ȟ7@ t;V"6iT~wsiJi6X~/2usy%ڂ ]MU,-p$LWzY); ?gCޛ ^l3heoOpHRn<6Hbl;V [N Pe3)HQ˶DG `A%O4`(bH@mt&: !c^_T~Yw#1ͥ am>@I=X]Ӂ?b( UOSKеɒH' &s˵՞*'8cD^]㡙iw5l@Y؂ckOaNsB`㟱YtאK:5z!WYIZMiì)χAӦanPܼ&՘Yic?3l:}ɝ@s t&2 tO1kUY5:Sy /)j~O_kZS2w%ʀ\VIlppo'!pͫG]"=3֪N Hv>RZ͢u>Z}&f=Y:C. ML@p @J QH0ZAkwrR+K c uJs{+BAl<Ɉ~;9FeEd܌ EdXiI`SJJe2arA-AviJ* U5↛,\[̷XE\ F 8~sbOFםٜllAdF%(* Aq3,{gँvWѼYiR;n+Lz|w)4؆!č(ȟBA#P>}36Fdsg k=c/&-=>6(*բpz6YuVl Vt $,%^!YZrce%ÌuoƵ*kZDcH<",|4k:m8LnB.~xҍs&L(i-( ެ(Uk.|_TyPAzI6j0Չ4 =DS5(+X?$idcJ%9=J!~lD]Ʊ13ssOR26Z531s~k1H۵P:L*E;#R߶&":[ɉ j0Ax*(k\^!`p>5S9/]`hq`A^7x)fIjw1ChYYN Rǯ>zws7 q^njZ \JH@,)@;s }zO gJWݫ_p1TZ3D&orl3^8|)eӀ]|6@B6Dx[ 3꠲%^4[}\ayg1`^^Xfm x?17Lܳʂ"L,WKzUlh nT0 /X"ۭ0lb&1XjE쯒O$/2yMZU,|?`%$%1'iu*"b>G+七p6F)S#w*W9L4`(D> Uk>qX1W;b"vɞ:"bxldRd.wNCT2=>ڵ g?h4o'DBSoDG%*g3)с^/;qn*X&)jBp oٴݥNxKM8@$}U҄;]=tܣҋ3,hʪ&vRt+_{`72Ό&o/2ˆd!åOxyāHCl"MtWSAÿEou_bɻӅkbo9%%QXD|h5;ܢµY"̩X6TsM](rw0ߵǹpQ5| gUu. E34D!@*%pb*)싹!iWZG6]苲/h (^Ω%re慠w=q2lg<8e.S&C**{}?Hfp;|\:2yqtn/;@)? \se7h2G~=䘹/vү9Y\q!poO5dfLynUԶ<( |LEl(}Uw71pKVwQLo#T\aw(R]Ab,[5;eX%+}FeSԍ&Έ[؋& u ` []?fLM_;P%+99J'H3" j͖6+ CuEX bwFBz/j|XBQ^{0Ye Vu1^Ĭ)+e;}o|13t͆0w4eZC޽Dh0儯fc69s ˬ:IG=Cإu#>wj3B/B&q&vQy_PP=GJwFAc88gm U2r>M(T+zh5 !_J20$aƄpz2x4a0K\l֥n*aXl]~ PK(f\Y|mA}!Q,oASXLu}.\Փl|NfO|zc)#_CB. `B#/2je7HoJEαscxsk+4쿏p#,@\ɀ& ܙ+Xby\-@ѱAs=:?\/`=hV2'5)n61xZ^$Or.>W8iˤ1%4~v{oSݞl Ɲ0Y*JSPJNԉwUgI%0Uq? Jշ@7لAJ;ǶQjJI,;v8s.pAS2#/g"Y|lE Q)E|StWnUJ:Ӡ.bw/jΛw ºBHBz4{ r/IVRm#pnqGnS7|p6J| dŨP :Ur(&34c985)mN}Gdn>AKR^ !rL1xfǮj[ֽpgQ\/'aœ mz1SX AOp]'w< iL7+\B[(*K7`֌ !U8w]A >oep<|}' >Xݝm?!_ʞ!@ˋU\ 7/}wCķDl 30REUnMJ[@J%y.>x)( S2w|cN5'&.w4aI>&x0Fe S!L~\<~ҁUuQ9=e3Y1W '} NhëbIsXtjސ] QFnVI:tE#YEK~募~Ğ~O 15~ v@" 9|[Os_׊Ҽ݇]6,R'0(6 v\;*|k,΋o $)@gbxfNzSm4JỰ\b6Vw7?w5EPC^1GS dGC!sl`ŌUq,}{LicOj~ihx- |Z5e (ΣHHZT[b5;ehk"Ykubd7 d y,0u~æ ˠ/H[LDRX/۾h(raXKYa4~n0x~t:cAo' c v|LG*#,\81-ADԈLD>L5`̄h~[KBOs1e-y,&q̺ܢk{ʂ8<bXǿ-fR0ʢSǛ{h|k(dD!&fج U|P!3v g,b;-]Z̒,WnrpJ}#`)}bwSex+>RLr %W `}));;EjPbypz}y}1g5f4Cx-ٹV2ַل_p{t~_K=,1AÀQyC@٫*u۹BȞEßfj"7%$Ա%)br/'r:NVQ/ R,zSV)>>:i:oJ.lbqOn-@`-"W:+DBj$\7+l $xynʱ4Yv=3SHϑ.YJPm*]S6I^p{"l3.RB_-ï>|X5)ri6lY[VFg]6Z2qrv|CkVp"بJׂgoeBɾ'+)9<3g&cqQ0D,)1r;YǞb΢L|n V\*|5"ЩRvZˡcydK\`tJ}S_a[wa!z̀{nyN2 94%S L1XnD!T a@Yށ>yQsbSM9R1zoǍI#0qdt7c;?^kfJom".=|i>5vaz%D P goę+5Wig DJUwR*e0XZMRV] C<{X/IX,:X$ !ܽa=5%#RhJt0L#j|cSg-]})w+GSA$s&\s/|ZHR** kF*% ?/ES%w΃Mv8y,}RK\qxmܬJgPtͫd=6$%1L:y 6UgԻdi;Yk  R~BS;-#)Au9gƵō3d 8C" q؁#,O=GK}CݭEm; J|fټƵkToJ&Wa@< >.U{#%2xVQS A9 dQD#{#`D)Ȫ!M\nU9ޚ.~ėͱ? o{8;d=\6E92'>Iᨰ]lBUQ S -XfFЁ}};B- $1`Ҕ}T[RBHt3˲1ii'#+2nA34bG/z*Vox endstream endobj 26 0 obj << /Type /FontDescriptor /FontName /URNNZX+CMR10 /Flags 4 /FontBBox [-251 -250 1009 969] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle 0 /StemV 69 /XHeight 431 /CharSet (/A/B/C/D/E/F/G/H/I/J/L/M/N/P/Q/R/S/T/U/V/W/X/a/b/c/comma/d/e/eight/f/five/four/g/h/hyphen/i/j/l/m/n/nine/o/one/p/parenleft/parenright/period/plus/q/r/s/seven/six/slash/t/three/two/u/v/w/x/y/zero) /FontFile 25 0 R >> endobj 27 0 obj << /Length1 793 /Length2 1675 /Length3 0 /Length 2226 /Filter /FlateDecode >> stream xڭRy*2N}}aZE @P *̨0M.z \+1$Y >O8p&?’@LM̿pFԓrNcjO S7o[vD$}H=~wTqܩUi=ϴLeU,\^_Ӏ .o7pJ%]%)X1!/><#~G}=:Hf'yn̶>PWB&buwsV.!{8Piw/?cGhEFwdڅhM3Fd<*Qt{H 6{8PiƑ!Ax`P9vX92~PB Uq\YhWtZIZE ۋS$ ?;Gw.ڡoxrDzX1,}MFj!Qݎa#xOHgwOns:^&N Bii}?f, A7Ȋl`./GST@n{Y0ksz)wTֈv7UF]C/9deU[Ձ/Pj‰bY*glM^{b1bdW.т$1W{SoKs=^˖={쳱[ݶbIOihA>r߼vNS¸ +}|'u7(y(x|uV)NrRLds;[e>Ybucmׯ$R~/PC;)=lxYs%M\ 1?{Y`zvZI?<+5Y~_=3^ylB)k>eU~{sy*{ 60g͜_R<҇<[ )m 2 HрEANQdÁ`ydWaXyq˘Nn{x.Ga^C|QJ3jmotEYwnPqI—6luSh&ܯWsNdklrt%u<\j&?aD/ȷ;cme][3j|ng-&Lc:*}/,>WA[gNl1c;5v\ 8.tPqbL|dSkoZ@ŧLdB]=N-5+--0C7wB4f;T۵ךx_uWkUMNF$zh%ޠ]EM%+k-B.ۻ aWSxV3O@NI3:TBHK^`[ endstream endobj 28 0 obj << /Type /FontDescriptor /FontName /XITOCJ+CMR12 /Flags 4 /FontBBox [-34 -251 988 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle 0 /StemV 65 /XHeight 431 /CharSet (/B/F/a/o/r) /FontFile 27 0 R >> endobj 29 0 obj << /Length1 733 /Length2 1087 /Length3 0 /Length 1609 /Filter /FlateDecode >> stream xڭRkTG*->r&pI ڄ !d7$H`Ԝ"XE-U"<QUB`mOߞ3wܹs[nć8Cq' \b0I˗ G0GÞ|u<`3Ó"-LU" 8 $.a%" P'rBC"0L2< V&b GPT6Va0hI: ,B*0.p.ɂIy"b9"cr `%:~k-|:ꏋe`|BTB\b (4۔gAOxZǩߜBo*Nլj"%QL"z<*!* %ƁJXK"您8`+ (k!:3P '"T Ŕtgdx{cNl.pbsM f꿈R p9 k` ML4a*3Ssju5y{J@C_|k 5eV̶mH9xcb09v0lE@acO8}W+Gk>2zqhgãLO5ֹqE]5؅ZLrMڟ;cgږ9u=ϰ3^)7EݕqYӳju8v"?zgXdwU6 w(e}*\bkWڣ\SoK׾z[Vw/#}-)V^Ō]RԺh.- x?tx=l.Ԥt+/>U~Q]j=RY!aODKǵvڈm/hdyߘbtXX0,bEA#m_, G;-9gD8dSsG&G[w|ſ1.Pd!~"(?Mi/YDٹ|*Dn4n(~"^tz>9X|60ap4`!-[V9x[y/.o5 q0^ 핃 7u1]?;~J)g,*:u*zH奓++th9/W!ul endstream endobj 30 0 obj << /Type /FontDescriptor /FontName /CKDUVJ+CMR7 /Flags 4 /FontBBox [-27 -250 1122 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle 0 /StemV 79 /XHeight 431 /CharSet (/A) /FontFile 29 0 R >> endobj 31 0 obj << /Length1 1027 /Length2 4308 /Length3 0 /Length 4962 /Filter /FlateDecode >> stream xڭy׹KLLsFi18TP7Pq:'qX 8%@䠀+ 2rr:+vu#B¿dUOcC8 I@1FU ZP>( )N H48\XZ/?X W C2L $H Q.`#Կŵ|1#/yҌ+ Dc7%!c]j˚! wVǠXW BhPH41>qoֲ0_}S&p4` I;8%9&C -xx -^Hh,H~X $piMo &$6$w;C$Md @(  I=A(Iq "$)2vYM ,&)IHC~GB/~?!mIw @!h?  /4s)E\k@I}o*4EE >1!Z7/9-_fŷbǪ4qTg}-+OL4̿PO𒽱{&=RFZ+ c}JPлT?ӓa4uW7q4cs g<9V§L'UW3HOZEK&؈[S+hf?<;]*$ٳݨ[KM;ǽ%_z܇"^z}Iu [mBe g&Bv`y^mb7eǽ CW` ^gގlV^LE}e(2pӻ>fO1*R@CbQ:AArSj-04#hy%'Y u( @PNef oNO}ivGpLQt& j֯[e:3`9'+ bXCgiѳT,DsqYXYvDiIՎwReq.OtYDK}&w:Tw?q&מ(/>|Gܵ1e jrjpL dDާnvg8$ QtI 5=J1pU03cID;zil":1Ngi}VC(*ZpxQMTY$ᚗ@,߾б'6Xoy1gJٞ~̭;>jtVJYH ov@3UzdvW+O3mSyfhM"gv8]-8F4u 2U7Tq:+>Uj> l3PidaOWki긴;?ֺx4{U~s\䎄}s۷oV$ۼ≼0\1ۛ(Zy&3ͭb|˂kf7:㬙n$쥠U:k@/s$D0>i<+u{rjg:[EԮ} `ՐWD, pt5-|7L28]D2,=)9[:#oʷ;{c1ޚBn e'fK%W3mڼ:`F/Du*iH >m[xoiAWYH(6(OfrHWLH_kZח?Wh+MM$vF 2~!c2w ;a uI?dTJ}$ZE-oK=#^̵͈(Hh{R筹NG'i:敫̺$=oRήf؋iG^,1v|V5Я-ɽrק r/߻ a6-C?(f6F]],_Pw=aqi ٌ4{ek|,r  6¹NKƚ 3"f\fFW^kMFO=?ZaŽ֫caxVߙٷJrkZ憽gd4&*^{ÃmիF5Ib<ȋuӷMKj7|6=&Gϒ̲4΀Je%7l3D:Av(e'ZEݛHՁ>^[XOkVh|Ňzǹ2{$$^ {40Ӄ0bb`*h6g۲zy/Wns6m;hzx޳\@'wf8yݲU%aے-N yeD'Z̅S8 V.r/ԧ,uN4-9TL Q-BwHdi#;fOTv?|^Qc2G\KaPr /Q@TN2a-;be d<1HfB~C,_4 # Q5֍΁U7Kۺ8SUzԬ[L{a XBfc&xȆ)Sx^FV>U^~FW3*Ӛ֊;?Í*~}Z*GcC{c1no_41ocd7t٣R)C|JĎp Ivʸ9oBwGL|p6pG)V;lot’PKay.Q$w,nDȎYh?5xG˜ZeܕשOD~ dۼp蓟̵J,hmgK\ܽqޯp^CdZ{=\辊Ň'ơXΣIaLtYLrKjPt_cLRS1xY׋{ǧURc㢉<)abk9袦 I5\I5Ey]/sj+"*=]X7k{ɜY<͛j^ &*(vx-Ќq `|Z#4+*k48~?vR}z.ha(و89$6}ż>yMAk-U{M5M!DϼUyKƾv:#Ν b" Z9K_JVRx9n7ȀjiX)+ qnuٺJY++sw:9TV'}hZovuO r_0I} i|#a7ݣ&Nҝ]jtÃiأ&6Ы[\\)0^1v6|. & u`1Q8Wmϗu-xfƔWcU^](jjwEd&j²Gh=|L*JKz;݌9J;!dkBd:1#th6φOܳJ:7w+_ϧUP@p/`mkav _:n0/E"_>kn_^5Yီ,M<YjN,rOF]b~Lq`i<ڴH[o7qIw (LډC^O?<їQs*w/z_mY^ޠ-P9Vbq f/eqx]{)2( Ud]r1 Pܥ1W_]\> endobj 33 0 obj << /Length1 1076 /Length2 4946 /Length3 0 /Length 5626 /Filter /FlateDecode >> stream xڭeX{  ABB!$TF$f!f` .))).IAB)%7ᙽy~=|y~+khhHA09$Ҫ*"=i G"d0Q,"\~>,$*DtF:z6hK  C-9怩ainh#-0/ eoh ha(W pK4`#@ RDX!f\03F `$B{ "!1`%]\^.p{ @:8a(@ ՇM8۫6[J!afՀ-m+s{g_vo!%TQJSJoO@x8_1c6|||`L g%DBQ (:0$x8 s` ^I0{(?THIi HA/@j00 2EH O# 5w- c E/c#~CLobFtt#ALe0\ `0}O"ݽx? F?.( / 9EfI4= M -)vfH)l Nݚց1Kn'(V tR޽^ڠ)[T٩{[&8}Y=d/֦}e;wm+WCrv"nMN\ -L'CVe5$c?o#s}4&Gtcv3RD"TqS$. ݭivߛ2+sxTinm-5dc.ͯ׽w/A ,)[~0Zxpc&is&9cޫƼ2EGy%M8OTǺB%Mٿ+X Zf=?"d@T-U`&R++5 60-, q8&R /vmRE |,G9ᗗr@pӽU}{Cfj:Z ZI + F<5fаB]6Ag)C $mcFE|Ͽ> wk[77⺊]4z^Ҩ'3}mxn tF,Isgl;\[8fӀr§uX+8̬_)6ZOtzEbbpuLW}!6 uz㻲.0/dal2v#v1IdYpGllhKf9}RCo PxFcAue-.V|+H&k {, 7o{F?Fvn\Yt5(<:nm@+DZf%{ Q1)iA>Kekb Iݳ&v"+ i7p" n-EL7SSC{-'Z&VD88eThGpdI ܳ V4 *Yrc]+U/tc}$?h֎nj=԰ga !h9lɦCT9ReR`ӎex!5 ,"IkۈNf/ -Myjh//b"DJ(E:߮JW\KtKfxFB.5)I7yGyOUKaS/&e5+Má4Wrl*QizT׽I~15jbr<:+Iؕ6%蒻@a}^r~wQ ըi8ojޑ x%;nv 7C}n*qnq&ja6DoЗV(CiHi %Ţ f?$Dײs{MBt>:/琬.%9ہUT~ c VOq,TIE[)iߩ"_$ܨp2av+|kB~q ~8W"eǫnR$Y#@BFX6Uy%,jr.>wk&{ӳK;5_78 '%>ͻ?{LXbwMx9)et w]Yv3Eaqȣ܂|5uQM8ԣ\q`Ӳa#ٹsroWYw <;;u] $>:^5Ӧvnu7Ds`Aތg^|7 $ ԑ W]&)HNkR=5v< sz;?bMS*ceXITqJNۃuKx߸r%6tyAGeg/nln? ;Ŏ UHmMHv ;$4ocy? {Kᴖ,_.N۔,ü>**[۠b~?"MJ̾H%Fqr_y} 盕f|ᝫ)X.(Yy>تVr2T?юy%s)',Fuy%ZMqs^GA}1*B˒!Q3iT/w>iV-8x~7RO$FNRB΂t_kMHqJǔW.'3t7û(bGSښ~p7 }_?(c0saqjL郙qO9얰7Ӗ~(2B$H2^[w GO ?T[ͳ9=7kܴ퉉o~eG)fY+dŴ՞ҕύ]jQm.=A<'x<2㲶"yMc%1]k) jL1A-1Ld_vLd &w'.+ ++|أy Fc?aܧH/2x<.[@2ksgڹn(v0 ~泐smɣ4gn.#g!״e5T9x*>qƓjv* i:ΔbG,?\zUd=æ.ttP}7~=]}kr{oαs8`r; rU_Mix?ۃR񭝚U 9syLX&j`.5&zl>6qiz\~V !c! NϷSAQ环~naR>zSre+/NJu/XW7m$ލ^rR?I=5~ I>T碊xlePPFR)$ꯘio|Haq>˂#utm,]# 2Pm S\VUPFM-w^n^jM\H/Oe>l(UTK΀B\Kp"{80~ghi᱕<:Q.NDhѡPqmB]pCWcSxjQv86_8W}}0;һG-Jڒo8I߻TEeUlEgЭh|ͦٙϪ_d\Z'VTa*)=U_6Bby-X\<\衸HeT9CC9|`C@"s8cݣ!)Tk4ra-l:bzow1WMf^Ԛv \Wl]i56xw?1 &<ޘ Z[Rǵ&^YVyoLcnBvU^E+^.0Be8ZkYIj)JZ#@G xs09;_m_##v\{y|9JЇL{nJI3c/vAwh]gIOvKl R?kY&>SF{j$j. ?x3σN`MTl{-ibn^ilzAY[ a|FUM3Gɾ/p,$b-Fa H$R|V $iETv!QJ={q,1.z~t-"SRdo(>W+3QQNd~aޖ@)1FZpq .#"-&w5_wX ?h$Cysm _x=TmmLwZ;xZ;3y9 I\\Cd$#s0jxbW'Ůd9e-N̨Y9uvկ 7}vCO?2NPRкecBR+&*2xdi|1EUh(:8lr†1h/FI8#7n'vkj&Jey'rU助GiD7#)] ޞMM h|q|u0)_t.P>:SØڸ)zC* . |ZE.nLis&-p},X0.l8L|;fzƜm=gM`t ާKNxȆf[~DMn 3"Y }!'[@dm"zTKD(GꎦkB5I$s2w̃4rWHʂHASӰ20Nrj0zsuV.3E endstream endobj 34 0 obj << /Type /FontDescriptor /FontName /BIAQAK+CMSL9 /Flags 4 /FontBBox [-61 -250 1150 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle -9 /StemV 82 /XHeight 431 /CharSet (/A/J/M/N/P/a/b/e/eight/endash/g/m/n/o/r/s/six/t/two/u/v/y/zero) /FontFile 33 0 R >> endobj 35 0 obj << /Length1 750 /Length2 554 /Length3 0 /Length 1065 /Filter /FlateDecode >> stream xSU uLOJu+53Rp 44P03RUu.JM,sI,IR04Tp,MW04U002225RUp/,L(Qp)2WpM-LNSM,HZRQZZTeh\ǥrg^Z9D8&UZT tБ @'T*qJB7ܭ4'/1d<80s3s**s JKR|SRЕB盚Y.Y옗khg`l ,vˬHM ,IPHK)N楠;|`軻9kC,WRY`P "P*ʬP6300*B+2׼̼t#S3ĢJ.` L 2RR+R+./jQMBZ~(ZI? % q.L89WTY*Z 644S077EQ\ZTWN+2AZuZ~uKmm+\_XŪڗ7D쨛Rl:/P1dɫϾ(l=Uhd_OܗEkv-X1tލ`i_y. 1dz:un~Q?3/S}] $e~s]F1ʻ/Q?m򻳷|<ċݺ/q'}I+6EgxT.GgtvՏGU|~]Rޅ_k9:{pG d}dN<6-uBoH=cMvHzqaRK~,K̞}˛myo~v _s>.# :L}E-J-qj}ڍwe)^_$`ѳEVdz[ U w ӏ.ke򟙥%{kB&̖$iCnJ<5':C7A2tN&~qb?J^\S^im3e-~ɗ8_v||l;j D>`4~=-s{޼rݵ%| ~AlR3g endstream endobj 36 0 obj << /Type /FontDescriptor /FontName /GFAWRG+CMSY10 /Flags 4 /FontBBox [-29 -960 1116 775] /Ascent 750 /CapHeight 683 /Descent -194 /ItalicAngle -14 /StemV 85 /XHeight 431 /CharSet (/bullet) /FontFile 35 0 R >> endobj 6 0 obj << /Type /Font /Subtype /Type1 /BaseFont /JXHWHF+CMBX10 /FontDescriptor 22 0 R /FirstChar 44 /LastChar 123 /Widths 18 0 R >> endobj 7 0 obj << /Type /Font /Subtype /Type1 /BaseFont /AQWVDE+CMCSC10 /FontDescriptor 24 0 R /FirstChar 65 /LastChar 120 /Widths 17 0 R >> endobj 5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /URNNZX+CMR10 /FontDescriptor 26 0 R /FirstChar 40 /LastChar 121 /Widths 19 0 R >> endobj 4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /XITOCJ+CMR12 /FontDescriptor 28 0 R /FirstChar 66 /LastChar 114 /Widths 20 0 R >> endobj 11 0 obj << /Type /Font /Subtype /Type1 /BaseFont /CKDUVJ+CMR7 /FontDescriptor 30 0 R /FirstChar 65 /LastChar 65 /Widths 13 0 R >> endobj 8 0 obj << /Type /Font /Subtype /Type1 /BaseFont /FUQVRT+CMR9 /FontDescriptor 32 0 R /FirstChar 69 /LastChar 119 /Widths 16 0 R >> endobj 9 0 obj << /Type /Font /Subtype /Type1 /BaseFont /BIAQAK+CMSL9 /FontDescriptor 34 0 R /FirstChar 48 /LastChar 123 /Widths 15 0 R >> endobj 10 0 obj << /Type /Font /Subtype /Type1 /BaseFont /GFAWRG+CMSY10 /FontDescriptor 36 0 R /FirstChar 15 /LastChar 15 /Widths 14 0 R >> endobj 12 0 obj << /Type /Pages /Count 1 /Kids [2 0 R] >> endobj 37 0 obj << /Type /Catalog /Pages 12 0 R >> endobj 38 0 obj << /Producer (pdfTeX-1.40.9) /Creator (TeX) /CreationDate (D:20090115194133-08'00') /ModDate (D:20090115194133-08'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-1.40.9-2.2 (Web2C 7.5.7) kpathsea version 3.5.7) >> endobj xref 0 39 0000000000 65535 f 0000003448 00000 n 0000003343 00000 n 0000000015 00000 n 0000050956 00000 n 0000050817 00000 n 0000050536 00000 n 0000050676 00000 n 0000051233 00000 n 0000051371 00000 n 0000051510 00000 n 0000051095 00000 n 0000051650 00000 n 0000003593 00000 n 0000003617 00000 n 0000003639 00000 n 0000004088 00000 n 0000004389 00000 n 0000004740 00000 n 0000005200 00000 n 0000005661 00000 n 0000005930 00000 n 0000014030 00000 n 0000014332 00000 n 0000019385 00000 n 0000019640 00000 n 0000032848 00000 n 0000033258 00000 n 0000035602 00000 n 0000035826 00000 n 0000037553 00000 n 0000037769 00000 n 0000042850 00000 n 0000043106 00000 n 0000048851 00000 n 0000049129 00000 n 0000050311 00000 n 0000051708 00000 n 0000051759 00000 n trailer << /Size 39 /Root 37 0 R /Info 38 0 R /ID [<4B02293C9D74DECB8F30A9B5BCB5684D> <4B02293C9D74DECB8F30A9B5BCB5684D>] >> startxref 52014 %%EOF PK!'tt=abilian/sbe/apps/documents/tests/data/dummy_files/picture.jpgJFIFpPhotoshop 3.08BIM7Z%GPPicasa< 124337+02007201204228BIM%מ8a XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)KmExifMM*bj(1r2ziPicasa2012:04:22 13:20:520220ܠ !2012:04:22 12:43:3734ba566fb54630477c673ea9a6532549http://ns.adobe.com/xap/1.0/ Picasa 2012-04-22T12:43:37+02:00 C     C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?-ڦmEYp:p˭]4Wt"nMFK<Jy>j$Ok$1_m Xz _'kR2}O]+QE}aaEPEPEP_mk +JC Jq!#^nm$_ߖ#$CZH񆮾@rO[0u?\W:>3[wzBxiK?UXCxٿ5S^_^XV F>>Yz+s((GZEOŔ2W_kSL?g7J" {hYe 1':׋;BF%q@~~UY5[R>Gğ0j~#Oًu6Z)5&9; ѣ|1ȯܹuOKqҢqRw"&6YCOmM/0)R+7?Z)~sEj"mjYwF{Vvy[՝n~K]w[Nm(oG%?iҧU;ep?nx']f{85`pm<PwI:NyF&/|'w}4!DaPqFF@9fYjmz>J[fwa+# ʶ:0#Y^i:-o}n|b^11Yd/ʮgkQTHQEhiv}Z4,Byb23ݕƕ݊n1zF>u#O6rHd9d 91_߲'?gjZ&m DMֵx;z ~xVO߈&vQwg60$9_>0KՃ_S#¸ZO]?~,_n{nX{k&?߄W=K7c_ICe"Oj)88+k EsP2vю, x;O NTqk'KJϔ$>f+XŽWrX##8#\긫Xbxx/ҩK7q_Hv C)?i,\xRN=koWO5.}@A*] Tgŗ~wb#W3wRs"+< ~kyL5i?JST>QҼP1ʵuQd2g>ī}W<*a2NqjRs|N|OǯiB9g'KӗjY+0a  ~Nk% U/n Xo6K(؆D 1_/?|;<7".&յԘn,]Y'c+X1gϲ2.4̾뱏ʝM3OAZ^wXK_̎%Rǀn+ MR4Cկ|!&1m ZmXQ2+^<Ѽvyqb<sלB?jx[]FGʇ0,+}:tG^6(Uq꺐1D%3t5[hI++UQŧ;5[8SWʺ+oS~U뵱\㗃4?:΍Zx}fkY]vg,9bF~c1[[;}|~)o$b|8S y]nqXiZ-维(&̧=\Xc*Moܫ-Sÿ'gf>ȼ5maLmƞk:k> G⟈? 5.i:> R@w v@Nm@_%eWT Mt G?/ƿ]~E__uYM k1MF-((3%Rk|KOL`I>4s劳V~Fkg7y|OAS<#CV#,vڂ)(FYZh4;{KTR2_~h~u&o8i%o3cHX J|c^/S ݏ1|i}{Ğ&-5K+kwI !B*0߃ϡ~w,uK hodRwdgюAXn/?Aj_DQ޿\l=u$h8?ƊF^_z2=|G襎m=Ɵ%}a#eej^#t}ms:d1h]NA1־DſjWږuYy#+$|_iyuwv2e1F>\׃I5}5]~gpؖBi>#g?_}6R=CO飸Ml*#8lJbA?j|AQ/=jNI b6X[g*mhQPz1GPk W˯O JV۾bQE(O@+<1+c@(Aw p/^&:irkmxd@GOO޾K(hw7&˟F#E5&VKm\oyG8~~?PO o-#^6<$,ueg5xgN<uhal 8xb}FOZ\L)G4=OTv{_)x3~1%=b{$#-9 w/ZRmЕch>x2:lkW0;gZj2IM^z._|0dcbsi J@LHZY^ǙXntWq"+|_qßYxA* wFBaAW>:RdϘSM]2B1P4բiqQby S*Aȭ\ ig's ^Ekׇ,ސ:7mj;5/x 6d[_KHq l>7ÕU8#q\3\Jɗ! AW ԝ59;&rUrJW|GC:>Y|&е]#It8f9D#0>? .=JkAwnK`)ei0 ~Ӟ- 4U׋&[ƕIc3*#]ß|׷&y;bG ]-SZujirڃ!Qmҿ>7)}/Nu2:ݙ@9ߡ]i~Rҿ|g\xVoˋvm9b%ROΙ}:êqʹpR+8Rp$qO*:jFF}-/QҪeGL7|ssin kVM霡W4&kxֱވdy\ C޵Lrk>|o5uxQ$ f_+yqj'i~S>W:޽e-oʣԊȞdB&p{>W4Ɛq&Σw+6"/۝ϵN>^ì:-LYO!/^55E2=kXF^Κ}6_SszmxYWܓH{'rgiڽ3Nm+>K߅_+_3OIK]51yfy>dٙmÿ "h2k;QrmeWkXZ3bD+J$'G>2/^+z\ۈgv?J(WRr{F8/Ycj- HFn*h~γWO4u-5(s ɹKAs~ן|7sUgn,WEđv\$w΍Sg4k1^Wޞ6??^!{-7g8nsT>F RRbK{b/{`/^Nxbϴ0UWO'_v|SuXϭ|-X0Ҿ:zʿ ڷ7>!_Y,ot ;m.'Pe!݆p;?~!xqme#Z4=o{ƵW]+jXTi@:FZ au&u&=ݷ\Eh^0. zփ[ōdEi޼LмIwRom&Y1وc1-ZG rN@=Yuj2k!yd(|~eHs$=Fk4{/ǟ(4wI݄c#Ykr8<zZ:#~=нZs\s,a-5|XQ_\Pym埛 xKb"4>Yh#].W]]UrJo3w!r~boaψ8ًYxH4M,1 N77?O j`" JLҪ.&3|u`2$GgT%QOxamhmyg9y/" vWr|3{sdw6@d_񯒚Fԥ1`oZMjacQW>݉0dy8^!ЊK? kit̘#G 0Fۻq_z3ƿ?dsl)yd ޿{u_ ieCBx}B6'Onj_E[BtOf˂|w+=1"a<#oZ`2=/9'9$oz-8Oht64;Ȳ*0xxF O9]|G֦ծ4kkh^l;G%#hnኑ_+YuFl$nWԱ45i=_ş,.M&E= Y]wOBc0y}|Ğ fik0ؔhH:cƺ%PM712e]cOՍzg>cVm#/ko=/l};rGnM~4*rkMJ QiW_=Sc](… Nӗ4kvW|,>2O>auybekI|'hd_o?j'cfRkDhH\6zpx˒v?FKxUI&B7ACξL߱׀A1NƘKm:1c>W{f& ^2tm5{tt~w<#J=%߯[?\_Ja`*&Gƿx'zu:ׇ|7[&=?/bYػ䜚mrU¬9f0jf}M>E)-u:kuPOm ;\v婈jIXҺeDTpEaG 偮7W>z_~E}y߈0T;n2z ML2 ƿm/Pn$\ ދ_طCPqk%h} |ᒔ*| ݜO@)֠h |ޤPhHaJnH#HTdãX[7؟fj .^Ӡ~[K$#,x*mquך?CExLoḢ 'RV;9, */Ê~3 ՚^[؜UNZpm4i+x,¿xKԽc -МPȯ/]ԟT-.z9L0.e %^(p3_Q/yrxx/ǷޭuHtfY0\c(O#'۵iI7& L$<:O^ [آ{MGNo#A- 72"@Gx?|e3GmuWcM:xg7ȣrlxn^'GTgcҬqfEP P rtں[~| }Hռoyn/:-,,}䍸b0{Wщ+?>_jZ0$־ݣYi9 U SQ_0:lBȡqF{Wj)'{X#̴j9i?~ x:^s_ݟT `{bj&n&3q鱃$eTgan?^?फ़8tOzOs }<6)\?z13q'$lױeU[3Y(W//fԚ͓H@ZXhd ĝۈu8'۬TQy9O~_qIvo+=xk~0k;%K;>!,-ܢPYp@$ktoj785}T=[;a5԰*HV{k%%d#Ũ4VT'贩MZ{=A#?ke[pkgl/xIaS7HOm 4|* ]kl!BͨkZ6h]NHl%¿G-efx/_مėXDM*]ֿl#91ᗄb/<)Q[ Ҵ6l7z G<)), 4]ljqb VjPz_i(n,~O*gqj_%auچQuS VE$Zz#4Aq+7eTC0 ciSP} i{u{@ng/5zH@ ,donjpjzK4 34{#[9Eo[#%Oko3 %[6? -m;x~倵X>oRG~W;k}GBAjH.&`L9^H5* #3wpV 2>RGï>9-QOԧGuUp,Sݓՙaۊ:t~tB-)EqHDr9lM?lmmɸ"ܡ570Żz|/P~`4Y;ѻF  V#*U$j>Kµ=%N?/  ;Aujq$Lvbđ3Pxp]&,ztVB3Y/'_> \x7ÝnRnZ}jK VpГ"go &uܗcF@pz_RZtF)h~fSc5yHKV{y3ķWgkpZ̆FI'JoV"K;k+ۉ7y26H9{j >3ͤ$/~*2oPܕWx{/ :N?Xmuոr8v({~{K8jTbM''g߉d\Ya(JrPQi';-o[^GE;R /<‚78c5R/ =jѵ;]fx"2 9hY8{v:"Q|; @r{Si%X$ҮZI BQzo{x4tцT 1o&ͨ.xK[&a~aO-loI:Mͥ>z$z+cc־KSş!˸Ѭu8`3[q?*3KWsޏRTL:zTe|c=%"&_$4υ..uXotje CqOJٺw;޿k&ޜ PϖC"a$f18c)Yu=K g=|Mg:gy1$N6; .뚆~KjW AZg/tZ.Z:^, IR]}+ IZ\\k#1(ːONWd quM~EgIRK*}v&_HA*y !>tkqh-,^Tz(\s< Y}it끁ؚꐧ()Eh|4at*I"fdu¸!ǥ}_wPƘ??t{Yu8-5 k̫.]qOz;uYH h Hw)QdŵWZׂo!9 p=7ўMF5}c'__ouBmK_C%ȭ0Ļ pȑ ܞzW|6I싪ρ; YE!'8q־Twnaʝ5dW|7qQi֮trPna*U@āž9/-Um4<`ߛo&6Q /{z\#'4޺~g;u_m|;!xڏxoA6RŞ!ĢEu?$lMW쇃{υ (-!@ sNkKԵ[)u?6 &of®W=W: >tr,I,R\5#HJ=`Wbe_>|?/}ιwm%sOnĜRk kں.50-ӓ|ǚ@V8w*N_Ǡ_ٴϪ Rf^< 'brr9Μ4-;H#Zu7~!+mNw3+9<ǽQ~j?a sNG$MOJPNN'q=+K:%ev1x[zWa{%aOߐָ QoY&rmޤq-?+q ~kIlsW^LQ]{]}C҉?ͨB1 l/ӮMjK"L6Q,k;6SּY}{x+P$7I}Q89?ŒAQ\X7%ݪ\Omqp={xԣ >?>reQ'$ѧ)񯈴=xFvC½R<3yJ {mfұy⍂1ɯ~SAt ^ Z~C$EsvWdZnI?gԡ:1]_5ٯEsspG;*WwiVX靹OW_/:rj))3T/ ~|IlotIDJeXpF=^,eՕގFhfS,$Q%z2K!\pUt=ȯտᯀ_Z֯jH_b;F.FOҼ7xOα$w3i INZoNxc{}d{g%k_8ړW~(27? -5R *KY[dsJ 1m3/-z,^i*OQ\:k +_:_?CT{+D]s<$N@%w_?klƯsg$obIsY8_g}_;8(Gw=a97WɵsSҠ4lefII[ Q7l wJԏD{_Ž~W\j 灹vS_Џ`#OUg`csO_7RDRm-L'6GÊ~^\^N^w-=)yw=;zrdJ= }G"}N"X.˒h'ʿd~+hV4s }{_d\s#$!K+cz쇆u7ϩ^x P[#HLlBqޝSU`WbkV"O0ioW=zl_^O=o|ˊE|?/gLl='%tk P]38ISq9x[9oBo'uW"Qp'WN\aϥ~f_ Uh탻y'@N/qҼSI3Jú+l[ &)c*g;?I49Io.bd's_CG,)|(a-! &?vѷ'j/|WEXdYc!m8eS1_M| e~ } <WQᅝ.d%đK~Un7_MduzE?hOuM\Zv 5yqkdžxs֭y[ypAwU=~Zq9>2kloᴔ^2I0HT5ZMF{IIƔw>,Ï -g{#tPO7ƿ8$x'[zΉsF5L otҠ?*|_ߵNLnwe~Q~<%c?X,b3}gmtdD$PI[(K1bjF9Z6755ˏg78۞+ƅ/ŏKv5:xC1Q;`vf_[Duk.3OS: i;|sx 'OK;-4[1wTz٪r徧:Phf/+}?ĶWdPHX9O3buڛZ\̑pyhz*+NMeaa+B K?-M^<.ܖiBX~U:t77kKm%ܲo1Ve 8(uyh_a.,jmQAW让}kihC$O_ĝē_χ&pxS*-3kjWBz10 /W ,Яu]:Ek(.6Hldן^kǥ=:K= KrN~ο_SV5 GD )5RzV%RA$14A}Bǜ~ףE~$ *L1$muF}T~a.O?i/\EΊ7P>yTyQ &EoHuNΑwn֗ I c`H8da_7_zdݷ]iqD yN>`g(ǚK(GksC|@>k/tl~ӈ^۩J\?-dmOv**(HȯK= AW9;x ygw,'TnE~w 5I~?D:퓧ԇ?J+M>iJ+ϭDO|s*ࡿ|tUJ}ϓZ2jŜή;D7݂+ZWF<+e6ڐL/bbF.{Ozgj/:_!f{% 2wʫc| k΍"VAȯq9iq{ѪŬ7i #R\v8za15#]NJ5-(֝k'< xN@s%4& ,s6tIXym:M: [k'"9ʾɴ ~[FpވQ^ 9<)oFg(/}V5{Bֹp.\s_ |eq=ݖ$cqem| zW=NmNjwH`7cU= \_>#%BvE v\k$ sRmYI'޽O캜g| q []^6jX|=]+{Y2Ţe=xN}_񎱡eqyj|TpA69xmSZlU>)$WqCIh`!fqT37/W͞oR-E/uzD< P|7*bFVk׷~c9]a_ '-,;?0,ov3m!I=M| mXfрӮG#H8}ˣ9)FVh?g?~W7V7<76D{r v0q˻޿,쵯+ ֬޽,VpCq.atqq'|vc ^Oc9KH?J,u8)5v3PΖ7O1*T=XhZ(tX/ d|b,*9;qo^e΅ KqCF}u!kB/?t׊ gq8%Uh9!XUiD/4uO8epJ^SY$|=j[&Ych$?*-Ey!ʀp=Fz֎^'e%M9>֢vR-A潷'auV1hOYJ#Ku@/yX \~U4H@W׭5O|RbH˔E`̌ybx?.Q%)L YG_E|)_j?\j7WyA\]AQsH5sAXb-_h[g#=xt;F3kT)=bNL.@c_y}e@JQck.ߖB#IÖЂ+^+u*9?/i*8[&4kS푦#_ *E}VAرG &dʐ0qh\sȯkӵ(_]NnO.!;( zY6*O][*W5aJ_9~_q/?[_i^&<%m-`Wh(kP~ !ZoQiuO&yԑv+ ˧yeR$@{4_c~ww_-~P]'R qLzThNI]#Zw? xgA_žӠ|?Z%+4ܞ[ kv}:=mtA,GJ~r|]:ѩ~{$(׾mc{o]+c-V$C_OoxçTcmU#ABcז< >7rxY񅭬f}B*@DW<m{Vk>8`^1?h~?' cZ߈f.o]R䍫m<֟WGo++jRG T!oe>⾀Uݕ&|.b-5GXna *Gkklm,ydKy/^#nE8N3mP)CKOk|D5K{m3;fA5s1 ̪8>~^'Uh~%[ѯ<9l[V^yn[i3 ߘn5cN-j$V2$Ќߊ1]x2a.c7,U>Y8*掬y{`rWizê~ž-1TC!RqI}[ş 'Oi}+󪲶G}?/zo^Nivma[$ƀm'9?xV/ 4&HXa#7v =[RR~ ~_qi6ƺn_r[gku^^e%,p#b5]cRԛ_ q4*I`~ | Vak k/<5wWIV7 NxyiF/W K)TVU޶|X ú u ب?*^>/ WQkė̯s=ob=M~7=~0xk[2us#r$N8#8 r+DM7_>}sGMg̻RGh!+Q^GJ"4K 8qF0=l?6|Y g-g ;Kȭӟmsٴ'R28~:q\^2q^`>3϶k_h^m5Im-hp}_4xZ\EFo48m^e*0^N}{V>*JuʣiS^Ѧͧz?aWd־-t䂇Ae/Y"x_Tг^o^ܷG"{n\ ۉx+`jP'&F[p xLt+ԊU諭.uV-OgxcJ?G! )[ r -ֿ?}*Eޙ[Hct+$L?ȯ w j8޷g?-C{¾:I|oii ~5un(bo%,V s>yu+Fh'4hB'f5~ -ݔBzqkҴGXԅk-ԟŵNz+ Fi.o*է-oY**ѣxEФ4K;IҒQm$l2y }~kG^k eΎeԬrCb"}v0[\?CR=kWAS| ֭{b|@;TZۼϮ+dhM}+YbGT9#\c*?h'w~fn첲*nEz-; 5L.ׂs^8VA9 yQSs˯IƱcg֬hxَy[$l\OJdY#ag)E67BϾ1_>>"O.$Û(tSLľ4u',Coolb"R8%œȯP<)QI$jF<@RK|j i0z5€ׇ$V˪ƥgm}t|gFOԶ^neB&6+so ) *2P}y?lړ/D_|@LFG}. ˞=͂T~c}Kt(z?MXԬvH?YԠԤi=<_Y8ɸ΍JT>QoچHg_]$LTpks`+q'~ìA%_yIp=p?iZׇ]"O)[m[tky.0*8ʕ`q}⏍BixDdAc:7Tmt1y<˿|Һ]#'NF^G@_x~:Ư-" 4eO-SDlM[Rl/gl6 '+bb|QGxF䫔ox_bl5@onlѢW}L _v䵬wy\\iNӽOC7[2GM[KBLvGTNc_:S?6 OɶUFrNÁ}y+D 4 HFU~4 KxKHq򬊍,\jάXJ*7ꮯzSW ,W1ʙܝkZi[]In ighLs=d,R}{H6U䌎x5=HK$븍޺/'9쩮UZ̲,;F&F x|$񆱫cirNHG_^: Ԫwwz_4F7Vs !d]4*R;oUKU[yߴgY"mR Sڿ(SinM 9s_5a3Un$ɍ8kMbŴ N赌lQ{V.]%ޯS1_ad7<$W6;15>/μ]y,OJ8I&QξT{ocbzƻfĸ2"]6'.QzqL}*Vm W5 CSE2ђkoC氐%wv/ោK$P0V -;[6Ţ.܃gYrX֭{\8tX?}j\ZCq(*vb,,=@#)x>ڀVWQҵ9!=}@Zh$֤|19OƚdK-މpčFtUj ~ }{i6_$E7QێpULVmc9$t¸W=Inқ_MZ,k+:tݏ*wʖ~}4MFI 9V<Nx^VX"0 uspT$~LTtTT~P\)J#6~'.ĉ.K4e=DC_3!n4&`c/L(}Q 핥\%U c1?zxzc԰~GL~r@`u/'KoƲ(U?Voiq&m"=t1̿g~~4-緺N&0 dU=:piJj?;Q_K '. ?OQ\wlȯycƽ'ӇTxtC% <9d4{c q>-h? hBM[=˺.U-K[a6+\2HRއR5ZM>Aki7OCkc6#'8C_8rM}6hor73ZG Ϡ⽼ޥtuZ&BWi 1 o'O~>`;[⾔N[5֛b|/I~ 9Fz.}O5| 5S=~6y/9yx";-a epĬkGqf0H-־?$!]kJ:DOFfyKO$|[.6%F*ΥN~]ISh[8*5OUdϲ?g?F[LyӚ6`* ? g߇::sFZ(2tݎD=ɾ#V5ڳvFNxgmcYQZ\k4NaeSeӡ\}} +pNX/~SȢGı)j7ezZ~#ҽm&9̺W22km]EpJ/@U@5[zUym&%jx`|5Prg.V|KҼ]<1ظ*+y=+پ.iQ[Ոtț!=J6`eM7U.kGM7zqήtIQBe}tMV[ Sȷ@qصKռI>!!ɦjS tSYT_ < )SJU[.]~~Ȟ&Ҽ%5k_jJ7ayϊ>F/(}ST ox6OBȠt:՗g/i7 Fd(IKfi_|R0_%;aҾ/ ʤUn5z 7IۮۧΏZ<6`v^,\lAm g&_y%߅|1u?4>IuGPHOptqڿg!nUI⾮.T'uˡnQW V\nT|riXSS|ZVІY݈>1#c1@_us>g1:}'#iύ). WNv>o^! [o(w$S3J?r#`jrpGci]beWP_,'^cc}pY 1j# S_Io x3WmSM+L9b "m-|3tW xi x?f~|($MzӰ`֟oSS~=6fu#h,cw usAi/1K O9𵞃:)t"xLFvھDA\%̌^I\Xq_pW?w6K5,0A#^5ΤU.V=ZfvՒQQG-իcD"2D9\ 8]m դ2q^gh2!#诅~ ϏfĻKxKMr2 =uƉq,_^4gv?^KVI;Mw@5i%KfQXytS\֡pCPinUGNs& ?woљ1>olh"?K=5`G)ԲϡTkx? [KZ[xZ?Ry;NUay?=?⮻cxk;M14/./ʊ^NsK5_.`Ijm_. fA!`T`~&p~Կuw6@O'lgEkh "šTvZ #F!,}X3*2F}+nz4TFSUmv1pW~GJ׵+{Dgm4 p܎{W:ĝe{DoDI,>n.GϱqbKqk6+^ҿܚM!(J~ja/KDqzfٟyOw8C <ZL14,˷X'8"^j\,KsWe>zUnг|3mQU#CڽY&58:w}!wW[_ws% %o.~/xfYf} eVV BQxt^(5νc߳9Qݛ,̀N"<ھ"iꚆxzJutP)T$;ן7*JV[*.af$H. Q^G +7Imm WҴ.eso[R@`dd~Ə;>a ix#X+AyTe'63iE}z{>? J-76D3=g {MrgD>jyCUW }Ŀ?P[o $д\OR0>?W_ 4[kVN7z0 p>O cߙq7ml,NӾ[]57cWZX /_Xa?&Xض~_oiaxui/uMntNg|v}i#75JY۬6d~n?]8pȖv0\'䣊jV>Skn#fHv8s[Ԧ'ˆJ/ {/SRAS"''YhOC#_Z"Ovc*-s6]ˬN=PGPy߽׺4oZ(Ɏ"nQ}.~'SH8ҔgԗIC"\6;q\~뗖Cs y $V5zAK[-.܎!Y ?j=Jo XX$Y~aricʯg|Hu]񞵪^B^c̠ʁU825ў V{p r1alWiNOJHs9U]Φ$ <ԋȅ }OLNOz_Ҽٝ:he7ݜΎs-J+,3/4W̤z .IڮxF+Zw x'-E$I;&.n+Ѯ ᕱ8(jqf&`mⷿd$}+ͮ`''֊+աI7"xLץ6 a͓|(S/Ԕ#Rݏ4'$o |AWvhgg {qTfJpHUW?IoٷNj'|3G.'+mG˚R?f|5k_i6OBބV{.М1u&k44ݱ2 <5Ea]3ZQjݗ"sEοF8m~?e_ u2%OExSmc!'Iѕ~FWfE$ )~Ĝ%ťx?5AgϽX,[ O֊+rm5Ծ5Ze j=o})QiV6[F+(^ t,[S<>Ukw$pUEz)T$W4kxT`XwuO>`#?擠iq|@nSEUN|K}Qӆ[M R+%BB OZmƨ5;m4&R2>QSVœem|𷀿l_-؍Qؐ0}LkotYo &*5'>{1Ǽ=w(7~^W?373f`Ui\ֽ^Oi&t3WT ;EWTp_"= ^]>y|$NIZR 2ܨ |3z."IIJ@̱"/WvZ(77g[;{A{i hV\Em?ȃ;׷sTW'tH{QE|mmNS<I.ƍj 3p 5vM1eɦ[!c=Y,}Wd߸sǪWKծonb,\zt+gMwl~A->H3nE'y)I?E-> endobj 43 0 obj <> /XObject <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 50 0 R >> endobj 50 0 obj <> stream x]ێ%q| `ٝճ ! 0`pTdF1ǐHd2[8Ox1}˷?Oka۟~Oo/B<L/o/lc~Uvk?;*?c<FYr;&[k_CmWPpw/Ao<\w|g=yz~#/_atٿkǟZoQxǟo_G?Fgze?1ѹ>P/П5L冼QoCvO%[K->wtv~?1 ŐA.{…n7 ^kO}sx5_/iy=v[>c9뿇˟EzD܈4lp/8[ ] &IoS[t=~B.|jw̆zk{zCX\¼x~f\ך,ނ SʻE_v7oǝ9iJϭ>K>>g8ŏ OW{_[=?ݎGd$/<_$^C`MGM?u>;}y-Qڥ=Ԗ|Z?#hxNWcom'@w&~e*߽_4 % -{oDl GdP|ž)FӣC[K:Q-- JlѫOE`6[)\_ PPOeؖ0DѶ>cw=M~H4t1#`[~>$-x0ҡqIPZvD<p\Zlϛ5<";j;q\h?BFBut~$K=֌}Ѭ's Oc =jkB?IztuX^:Z'IZNkzGVYlåꨕ_'Z*MӴ4kx@<M[;Kp¶>&|Fvmu# Jkz*~&j_:4aҚZa/z>e邰B(_Y  -1(J'=\$=Hz6'l]XR *>xÃhRUT!2cA>vY ֒X3k7 rcek@1Њ_d{ӫd͎`OsID߼gEsAIR.9Ӫ-*YY ztdb;}Bb?2R;KŶ~r;mcwQ{'V3zo0Y(tV sʽuDCj%j$K~R-χrPkO{>:p\z:7ْ=vՖ΀_7BV|nPE;Z YCzUT )Y2z9+F;z.um{:3j*.cyN PWtD}jFV@opk_j ]MUEC+gg1ӟQ)딝VsЛ*JD,;L[egBVPflKP^ 73gz|IqTP HEjdE9y:^f`pYyt|B{n̮I{:5YG hqAXG[\d ?Fαܯ:I_k NX#T10m{(sSr(#ZHkI.{{]?{6w[)U\{o~V-~9@/fw~.#Sj6&\hq܊wY7-}V>=I]Ɏ+QWmku#|J>w?U~~{Og|aOwҲ91l{m>oGumϵ~^Xٟl{iG'_?c.d/=A~jޤ>s;c޶;߅v쯶؄>Ptv#3="ZisX8reU_SyC|B9Gv p>X e&[0 !"KCQӃ]MIb2X (Eh4U?F׾1}Ol]L 3F x{ MTbFZQh0Iӊ$Gngp=+j\zeju L`23$pj 2yp *Z~-;$ypo{##:)" e/VRߴ&S.7ٞ&z,g|0vN_۬y/t,n'Fb_X Q^i:jpAXIl1ֱ-=/7@Z%4a*CD }Z?Lߧ eQ>QAyy A\KRJ #oZE\MKQϊ.n8&_qL*JR^č0# >f&v1;kUƑuiTP ~]X U~Y+jP0+ hffM/ӍS@8_%ôR7&ATT9{Yu>4Z/ X7^nCAŗlƄY RlQ$^iw'l^ݺ(f[qq'>wo3^+Z:ZDŀb E#نt&ruO^OGZ;< 9t?Cɚ+.J=⒠PJi]XJ08w`ϜQNd<6)ɶ $˶Yiᜇ!O7r;,;F$s{a( m -t美R/+&j7s\H=n\v]+f)CLj$•k=SE|(θ[&XDRọ1:Xh ~{Uҧ@*b*YH˯C֑  yFVYwHάȺhE ;vl$POUA i{)lkdtuUf$Ф2%?ӤRa**?Ԩ7^T&7싦~cXY'A2s B5]?Ãsw+:`c5v<@ Ms[?v[!HFϨ1} QJMY'mR[[@a<.:QXZkSqܱ/., Bq l<>,e<)c!=SOS0et>PjoMpj#Je%+o見Eg_j I*[ɡV: ه2p{N& 9wH]|ş+?V@ ]-hglIw^UR{i6!QXbu .Et87Ѫƣ6[uy^k XG1wCʨ Ea<tGh#ejB缱dtȥZ3Z9ŦȒ6(;:n2JsPG0rMFTZChGT"v]}m2*53\7qqV^82/(A?F}"At)8nwnJ7 n*.EvgCbXE퍋P_`6Zq3JvT4%*{;S\TyճcVgiHPҘh n%.b}#G%uZB4X_ww"6`Ab>hB}ܲ^Ӿ@%(- ,X[&?hIؒޘB|+sտD]mFjRj4ZHlpbl[ GtQ}[ҙTT,li7ih^;Y/Mdu/3_<K=TGƇ>l7Ʒ]z[Y]0~k-C_04-5;!%" H-m'1 Mp^k9(]P9;\=V(>`/^%=UFwB(*e)*_Ж^K4m$_ɞ~c% @ⷙ~n( Ww ֠swiCn ʺgAzbOR*V:G,T~/YClgYg4s_ӳo;PVϙ-<݅ӵUD{VURGk\3n̻\8T8/4>rZ A!HpH9 %"pL uؒ/DЗ>Ԙ]Qۍ|B3s-5_JSL:~mMžu( P$jDL,6o b-B_p\oBn4%I9Y R DS&Tm3j2Uj/[ZHMժhT bRhB4gs(q/RL6Lo!cKzO,HKP[;\&r}a>VOw=4E_AЌ*MTΒ^z9cQakU4(ļ"WyYa*{s/-vaeSιBbܒͥ LƵ9 !d.FG>݋j(_X$W#T9$Mt{VBhQ$~b9#}(qIdR$i8O1!ȗG!e[!t#!v1l׬wYlc^>r/uwB+maUP=[= fܛKR>'$1gTϞ3_mN:usRdʊߕƭA7Ue{g!G|J{ (~ CX' ɏȕMdv7)^xbp@L["Z6ӫbYOm:vKoF0U{ӇɐpW垤3 DLJYQUg MJiPx1㽊1L( [JVWB]Pƺ*aUjRY8X2ɗVEs7=Pf]sUU!yؒ}\+ &jh6[V:IѹViFVhw/ԟw`XJLkF>X|iYw\Sm%Kl\H5&ܢ1N#ϔrYH\oO{-]W"#I H4vRS4$|4QIcT[εJ#FC\ I_!ȧ[?M/NCP&yewMK`e[យ-3gdͪc*BP:ԜBPgdoŞyi~ӳGL7E$;p9^(Hw#&eVd灒=Uhj*^HAsG[_ r>?aW %֪wq5Rcu%QYaLnt.vKs!5r0a1)}GE{sXU{9sRHÞCm(/AC%_BV룾*W<biA g/: *8Wղ{ˡj}Զ.8&d߱F9XØ>'>/c]9--ˋʪ^JB ^AU-B{(LKxwHzoWZMVT!3ѯv."HUd-F$8$G3YzYZמ ewnA^DR_ZϣMPV1Kǖȥ"s wDKGӟ&$T b^^H|/uطr:Cu(rD9s=L<ag`ҼBcUAʤ圴Wj~c&d| 2[WvÑ"Tm4{F\8FG~w{ oÖT-|ocf3&_ 'hٖs1 mvkJoҁQ=`me:8su3q&p.8:[=y0~ov#IM僫$flM\urc^pw8Wt7>^O?W 2 [;{鶮QƊʟtnOhlE_-w}rT-x@mFv_ig.R2/_Ö$46Z _-'Yec,Xi]W[V,J! VYоU2K~6+Zإb|*~;(szmӭJ!cBO47j 舯A?TD#nC=Dע?_w*z|+Oaeq*Ɩ77W, "th;~KyBV A?3MpǢip)%@)DBV7Q*hFX{Js3==(̟jܙGi%xp$Xk#gCXo=!g"MFOd4̛bk3n{zS]>GqtU+.qU/n(WNck6v/]{& 5/!uG_SbjN_ 9VdmyoA7^UTu"kGGxiCt('Ji whUhjiVt{tuT֞?QUUC[z3")9oWPx4*n:>#VlŻxgߣ ]n.2RU5+!Uf!wf1a51<>~6 s=_QXok(U (_S<<#Nǀy!d${:  (Y!kS"=sF$w} mCػx!8Ч<4&{@^/) ʃkd9jUPs=P-)RIBo^޽P,**j*:EW;7xqq౜4^^].;'{ 8ЧWPH!]|g"thKU Qzs(2{=UT<ShyO#e/vaAfazVhLtU]\ [fTn$ 1EAh&ȧ͞B=޳ .,^+6\Bm4]M[u]{yb?]4nǦ鈟Sb}c/ žDն =U߰-W/45׸T][͸f4\h(]$:M{(}499R6M_JmBgԾPy[BAA@P "PU}~}OB?n;hq*gDڣyϝsG]o5<(p@*6V L ](,Sz!*!#UBhEG̒fB"J_*B+Z9ZV@ MFGt&itDg5H@OwC?CK+9Uqhrx)G7-z@庑s/-H_Mϴ mÞ.Zljnpf_JtSd~ofn'ꗮ}`]KH0I-n(ݠQl:UP]@髧-%K*o.c Q U4Ƞ9:ոKwe%%لɭ^hD֋|cߊuV~Wq[zam9[z;;D7$A^n_i̻ endstream endobj 44 0 obj <> endobj 45 0 obj <> stream xwǺCfsrΩ3@9HĜs99˒,ے%Y$9}ܙsW]$,{ֳܳz5!zjXyGuW]Ч)P hF5q|Z)yrEZ57Mcm[&ͺQfT.T+f2`׮:tkvͲUhQϛUsfŴE>ac.Ũ[9 [> ^ 8 {{A,hw mGGlO>7X;'G9;?}T?sն+m>.~\e~YQLSVلM>mV[Æ㧅qǍC',G.KQ\܇Y؎kGUppmsiv9:|qNHwmtG*%*?8p\Gx<[v8CnK;Py mNzJNVMRI;RO*( uu?#"= {+G3ʓΓH~N%?zr?e?xJߐos&|x˼io&Bqt]ѶӪVW+>E4 OUL%j0'cz­f)َ[?Pppᣔä2{IͿET#?8`TNݫ7v AmV>Ws Nv5 Q Ma5Z;f&BXz٢Z2)9vѪYfmr¡pr(ؗZi\;W,&+f@b|$<'zh}_n,a)%CdXbp A(ME8Rx O^sx0~b # ɋDa#?Jayqţ[#gY?SJNzH!2@P u$qƿr%&̏d -} Љc {WA7)jGEi}NZ젔-yAб5F;6FE)zC&ij"A`؋`p5főztI k5t= i,zpX-眖,ð4n4*mܴZi!)z-j&p ͰW=H ]9*<6wIQԄ(l#~nG#qlyTn4|OXՓ <'~rlGV(Z,nI>U68JF1N٘dY<5q&>WQU*l/SΛ{tLq8"TU#'hDbɴjUҴʚ|YdA^V1~F=L0yee8OG gd){"Aɗ*,CPo3&g4yΊ-Ѷ,v, ªuFvЊ}[ UnOۡ3G9GJQ,nE*_ Ҧc飨ჰ}A Oطc_? Pbk b3[.vE h&%Nzeڱj8(lzڡv'AJ7G3SW !9/9C*Utv][|Ʈ}P>;^cը9r]A)r֦+GAxNgA `?* 5ł6(`-AdcdQ4 %G\TUC.j)$ZD@zՔH=FrK)f,ij#+zw FiFg esl/o4xM~?Q_@"Lbb|/(]@{y>d ƃa ª:Pq% [A:hxE;;ySa^I@ 4W9d(QT7P]/u喏3SCIˡ88@Aqo,w+ԗ 銇%CM,AL va]hUu\fWI\ny,{vݰn-56͌C=!LCvc Eah駔}hMAR.ntL3E)d "] n4T́ ՘hwiJ XKFvE6j).52%{8i⒍yJF%0brw6䠷j ^tmہS7^d?FJZd4 0aZ1 h| a(Dôz0%msp B|gPOdT2N B@<lAB0M5#u :WEPVj+;.$GT SӼߴV|eFBf|yV7U"yA=AitnFX>ARCipL1|,v1ȆQ|D/+9fѨ^2j%;6q ֋%$t\G}wqqMv -@.b,T\UMP1R9 0Oi 4eQ,ʺ=ŝ6Oľ ͞`M <O%V^nr~7*dh /4X;7 |ʂ}dZ<锍#Qga7(ȃ (lڟ@ J胰CA8z oyXa6oL )CfAhA!S Y"k]= >AS,s"5`3״$yͼ_ѧXEצߺ 3XOPN4*[Ee[PRw ,H@Յᔽ~h4,+qէGКxkPt+4eT $EGHA!(P^P Th(1)F1qr̨2g̚n٬]1W%;ЄiMaY.Åp.; D}fض6].6rĸԯ9.렙IT@jfR=MCåDjCbpæ%]X} "ّno  êlYD rBV4b+@y= MeGG;r V$NFGAp]-9ݐ4ɫ-lw |E4ne3. &Cd}>0⃋p v̧˼}'ΊUQˊ+'ܛD0 / m7H0ѧ  wyǺhY+?֎U/Uv5]WֈA!(A_B湰u%hY㍠<] a э)D%$!v( C3S#!VeXUVU%nɠ7筚9zΩeqbݴsE y!x{.6!n!\+71!;eb4yCԭe,^pК!5PUN 2(\F9^(ucEyOAH!@MQv&G˄lȧIT[-#0>[򅐫.m0a&K0WPV\s82T:3"QhUM$S+>P¡ )G&΢fMDHZQ'RxSH#/%'xm  _ cn H/4 V78Հ@c,8W$;E✟C-c_9V̓#n$rD>"1~FG ;.?AV4JS&8%~dJ\%1m"ض6(H 90ov8ʊ׎r1V3f SWyx?BOc6$^q@H-"AyESO&O xεu>dG }'7cHƵ}q̀_a]'$u4!϶ߵ۷˺߲7-,B)@Z~.n72Eǩcq.Ҧb†#C`t7Vyv.Ynm. p c#)GaA}~ d I%=(eqv & +~1" #yR?'y-H(jVbF,ZYC^ 6[ҋut,hvij# Y2WA>{ab+h )G܅ Xha$H[k0h@i*8?ӅMfzGûFKYGUк94cA;;3nyA9ݸ`- c=Ls"}Cc6<y)G.|ǧ^bA} rl aT01 FX+hzcބz+#N}$툖-e-d]ħhbL18BA5GKmaO?>?x.K :kEq mυumH.aG=Ϋn:B2XO1^!Iާ(&F:#iZi a L Z5;!hF=$<6ūM)R;n.x,Em%Gm Bf%A܊(֯^4QU&\A&På@9n#{n H[V[B#-<| ?4Cc,"mۼuk!fXЍ~~B4Lֹc5 Pg/Чb}_roX (̗2 |) 7EbлͶ1F Q;taCWLߝЁ#(K{QK^TkYi m^dLI2eDvDZ%|S^X<߅qE*z.GpE^J1PE|U |W 8 @Gd qg K;$K1 8$ MOIGBH$d(9eNJ=H} MTLx8I pȽw9$w$0}=c_|ЁϼvoS!zw̥y&VAq 8 Q]W\ەuG 3iL黓n0H֣ty&` 6yħ+dK^Qbx9ȑ壄{H^P7mm}Iv\M7y\uλ#;ױI;HڙܥgHy 5>biė}ʲOX3~Ȑ9'hu,K>2´PeYKjBI\ZO3Dn7_^k#]yAE' DAh%ɗXIAIМ ݽH3%QA! hI[V*$[ .1j`BZC$CH%0thC{dpo?)0`i:Yִ] 8Gyh~7 2/=Ϝ qWͰn(I0(x/B "$~TϽ3ߊb?z $jAbƑq0d.8F(/@ ARƞ'eOFb{ 9 >N E^>FKȚiIA@դÕҮrTh}fn"oX[v&m\8θcrm]ʾطh"oQ}3?X9/>ǂ\\imY YUֲšX c9Є6.^Be$ J30i $ZXuMhPcRxBO WxA/))J<@uv(7$/X&@۶ j!6o8:Ē"8 8tvM]xImG|<;T.nƠzRSR8&ݩb̭˺EmЪ;~g)O +ȉw''x E5.. BJb9"'@+a%r zd}PhA3eT Ȼ}NC4;t^/fڧ[qG0-ganĵ7p!A? nPjAT<LŬ|Ķ?|ͨ~,(~O_EסҟCe~ ƒ߄S/±/OC½w7K_Kz$\Ql>CH0ڧ7I;73mHXѴi"\ɒ;tҵoE[A kjĶ 9 "hESr⃓73(DPE֑DAhJZ/Y9th|?!7ѷ+{w}7{ !! r ¹bAu\0Ƈa'?/l#>GK9iyZmsuoPXG\ x`ٽ!hwJxD; baAͰSփ(j v&wQ }0K< f EI%QU-YtH@8S8G`1A~ ]A0yA(E/1Ӓ k,0 $H4Drd&u$\A"'XHY4"J|J}Lg~D2Fү"ga${'C]ł x> 'L Ci 9bG@ H:KʸVVAm$d8[|vOv;glB-; 5OI\xj!JH+&}ҼVt%drqkh%\gX9@4t+ ڠNZ_P}!dcrܫr*'(<9v9ѭ2'UsjRN{7A7Wsap?%AB! %dG;NE޸*En ~,d;* D6MJLmQj؋#썼 DUI6NW)9}w{0E׹^Oz*MRVڽtn)Vʾvm;HįjDI$_ |E ]XE- > ["z.<ǹ\yMB}7D* XV%84ehi_q n;6FY0)t́#X^mtQM}4T,-8Sm>y'+jG3Hh Mh_jZ{_5x k B,KH !z(Z+'w7؀# @v˺,!=@SiTq&h^ e;bߌ:6 [fDp85ƌcqX<Y&y,iOZ&}~$I&}n*0<|I=KeK_s%soѲ?C$r?d^gKXi;l@O>X)wNU 58uof[INֵ/C3ik[Sy8" Oc)S'GõX{ y$}R7Htgu?}=\PW"\8˰BHr$ N "< O"0$"< ss12z}!V$R}19uai  umm u*̈́4!0&s#(G8^5ʫyf%G&%=Tq?4 j,F5OJ[!y xKihvV ЕZZ1vQFbdeƸN7} PZ s']h'3-"pnN }M0yH^/晐e1BwG  #0 ^D%;ɔuHۦӶ٤c!^KQ{)BZ K?ľ(|Yy){} @!QS$rLm64m*U6^/`ϗ{&=U15VjՎ*{/PmqU;@5]]Arz9qVpf]{eiJP3((;š=jɛ&)X|EuRH;zHp-@ڇ :AWiBfƨ E8 NB:HA`x!Ny'y /`Bd_ 4xXz%DXG G\@б)&*m ẅ́,ؑyj-Ԓh8Q-(xE'/OAH:`A z]᪮FU; fB3i2maf@"\%atǟ$dA7!}Wqty44Ic=^ [@<0#; 8@m2kاh(RŌx+$y?N'I@Ⱦ~/{*_tJ_eK^ J\.TScq׹&ZHsv8zk5JJLTĖVk;{ Ht8 Hմ-#́$rDVգޤ9Ra,΢Ljs~ys!R )˝K%s1l$1f'G g 1q8y|~}b_b_#BaQ$ǩqb<!΄{@sq={1nܵsn[yB{=2m# [~+_`5<!%Ft͠OO>ސ(6t˪ߔ2-͋@IJ.Bv,&3xͲZ恔m4eO'2ɬcԀzj(KkP)Br//O/˾Ζ͖f*^V\կTTTQ,2*xV\UB 9[띬s!5{lj:Ghp9Q V9GkjZV9b#٬pS٩poeimwu1ukDdm- k䧠yԳtɎCŅfT{֠ZPϺ+vX@гRb}A]MВ (DA8s!'ARGDZȳHY8U8"E G^FÁǐ#1~w;fɳH Dt8IHy>>y }?I&{ VwUr__T?>%/KHҲ72?g*~V{EͯUUVWX{[Y&>rZp۩V| qIz$9w;s<?E]fj$1w l-PGT+vqm;6*HBQe'HXR ?!N ʓh%Gl 8";6*[@;pQ^;})X6aXRJ d|U 73 '#͊K$>SJCO3ȋtkp$.{I&~Hހ#ЋdyeBG' kXQRV"7s`JВbyr i7Vp" |qgA(5у#24/th;~D+_/*{$H@QɎc bJǐ PYYRu cv9@sԹPJB%wʘ9rve^e (K/˽~.QYU5VZSSmWg ONo4ǸFR#3HδbL$'@fH+7D =C<ǼǨzz^!WkɭZzޭ%wJFsO$ H[\tT]"EWw%ԟ-hOqxK<;˻VtqJTr_{:fP|l˨s9r{6OwoAQ)prOd4y ~#GR׉[K2M*2*Q#1Z" N !yn&rľH 4H82ELsa4_;JCK׏nNuAbи ݊F#$4h%F9 'GQ!Nǟ2¨Q@ hF}%ZSg¼Úy03iXA?'t@W}~Uo^?iJǂ扐u.j[IBڵaAfFޗ d H}*.w9|ιXY.#6ʨ\pZˊ HyrU?CeUQTV_+֔}Ӑ~2xqwANRS\1s<;jtww"Oy{'qz^7뙭zzڭnWA3 7@GB۞uHLUwEAu+8"(De_(9;DG^C:R2c%\xȳ9RHʅ@.]WX.~ ~Qz|JǿK_ x 7x2 ږJ5# :9.%IL{])% A&Ќ,AEG3!DP7WuFlH$Z Vh~ "6d̀yAf_5> ]a 5AutqP0»D$50)"1M$, i`џ9 5sIxʽ9zwJTQ]&etߥ_W}W{SQsE/ՒQWjn|SW'bNn6.6{)vo=4Q-\+=̴㧼n, @#1r<̟`5z ROmב[5FԧW宥u"i˘^D ս1MwLҴ# _P5u+9?j| )^$G+G hB;re H s*yJ> ޭ R~X_*Ye\i6":6zJc:4}6]:LL4}&hߕ4s>IMQgi̿$e!#181̆ h[m/!4 Sh_aKTA2"%An@͏Wj@AXږIa -VBΚou1oJ>o9"Ї؈8ό&~dGEta@DR#iIu*lw, ; H `A`ǹP]A* lx&t.z66o;ī*~Q[ qzPmcWMO#OgN Z3,R.vs ]L+3L7Sޡ&X3[8C484#u^dGw½ e2Pjz2Ihz' kIN #_r ZLqWr!;BS#hu>j]10 YRFBYUs,|'E?~V"4<{~Nj7J@W|^yR 9߃4w+_ppaΥ鳘 B8!1Fah^q>l^@qoAz "(z 37u?D3( 2į:E)9f^f],> `13g$DictRSA>FAR#eM8?d>XG? +$Ho>tV9窜p RAnTЛiIk ge_U‚TW }GukQW Hc?NXrgѻ-sMa_ncڹ^hf6z8d;zhʹ'J QOSXz;jӕc,YtOr/Eb:3>Q,]xjWR˕4cW4AI>Kn kㆱu gu1ܨ/VWAdpPSݫ> }?A_on4oB#v*r)rxvsӮ˾_k=/ K4kåH{D0}e^tt42CB+M?T@02 GQT…ɘs)B½"{&N&R?m-usg+,BR\pVת}UZYV AuQ]yM髚o 7:^_uunM\[Kϭo[[gMo7.a:vafF&hVn]mڅVq3haN(P5Ht {@em2Xa+7{ڮ7i@7j;⪖%L)՝j*4(aeȎANJKč#ҊL<dl YZy\ݨ߭ ܯ } ÚнЭc['כך#7O?iI~*T.{ݖ=SS/JZ^[&[7Έ)ul VEH(oYo r=>Aj˾+mmOu5?_ ds57_-m?Wm/}ޙ~{L['{ٱv;x-nunv8vjdOw|Bk9z9|1PkR@Si0u笃Զ)x‚)5:QVeƐ)n]1 'ͣI a>s<*J bDF?k=:{1rDђY-YSފu?mK]s{o-?U+6\llϥ?BݕMl'{94]ṋ4}=E]A;u^:+˭(P`U+t@@EdG65Ѳ"} H6Z Ř hPFsYWE4CEQ$VE`Lθ)M^3( ran#BKxf`^8+ھ P%#Q8%7 8 Nm3PSa;kHr=v߿sEN${t!@(BBRQaE qu<{Ѐ뷞s@RHVHnfiVA''ɨg 2R+&Deb̔7/@DUHtW*}$*īJCVrO/\7ph FRi+o5B(SrP5Re.2VQ3:h-kE,3݈nՕ &Xd-vUILSS; o* ,Z^>`e9YNiMnx]z_ZN5DO9v1TB&C١Kߥ#2w##C0j` 35,ŒI"a1~NCWpZ7 *ui#s8jc0Ҏ :rZKg^so{r]vɾ`N!fO2*3i:tV2,dOЋ&!#Ar?c\nbN#'Fʎn1L&UFpm#>`yj#@:"lk$@ b:+Sk'v9ʶZ)ٍ5;JvJj(Ո@<^-mRr `P%w>lB#d;ːosJ ( n(xh{ m@ PČ( `g)iN!*aUafWUIN$N]/ %!qa8,.i;!I'$b}I»*ܖI+*#Nz 4haaRcDUMU(+ɎiU;k>cG;jk׎wTTc:*: &VCiX[ңMQ8Gu1]4&si^*؊=b:!9X%H :d9NusXnGKޥ&VVV0Mm.i 1K;dT]q CzZJ3!qvVW?" vDV9)%LɈ\V9F #aD45~Lq-ZҬ}ZC:#3.X8-[V}#{G"'??dcI>!/3q3,v- Ko5QlYn3F0F aH2 dPiFfBr@m-nyAj(FVAt UcK04 ֣ڷv>D^\񏚷Դ;A}žv_yiċ,Y!f4!td63(e2}L7 fW+ uEnoF,efT$l,Jmi~5Uc9&+hKJƲsA"]S($r(NH/g5o+f ㈝2QE\Ń C.M6l<::Wyx\^gym:jp]lY^Ul)m0̨&%aCI+tSYP[˚WeεL,Jag:dYEV:ۮʲIs8L=^-#UOۧ%Vv)9fFW7 w1n[CڣC@kؠ m9 ^)fH)I?#&ܨ8l#  N֔"q,y;븅da8+65+ﺉsM˾=0׌ʧ2- ㊆}Cɼį<,pʏl*b"nLJn. #$5/Aɍs #Dd]J>K%9Њݏ2=<؎͈lƭٓ`xB wZw;Зi,ӌ>찗U]ÃCv#kW4mFvL#peI4= | Z-@0u)u|=C:BFRz2u~HjgYAVV/FdU2zjWe 9+pنnARbhW#GE>5nBG>eֲkԼRx[-}C>}Qr,j,ٙ!'iIq^\\kr[\ ؀ q{+|5P%TF=Nt(k^hC7YJ-V{yÎ_ 85a.pBAbUSdU˳!#P,;'H߭aCRn5dB٣B:@?D2_ .5u:e׀*v} [J YE ; H4 鰎4i$CF*PDuCs7c)cn|5󈍾P]sX8g V-Fu[v*}>tJWSR QfbO#Ԓ!:jl#Ԃ~ȶhy=$Z]LDBz3Ѳm+K4FqlHkCRVJCF> "`4CZ ; Ul|wyoo#'v? =s-D(;E 3~[b|PO@@|0ult0YyMM950r\|kC{}eng޽jTQBmiV+UYIDcF=wIÿ^W bU\5*͊{f-w:Mȃ KlEDnjH1Hl~WHnnv6cA\ԇGj^l 60t́w9 Q! {Rouǁ4ӣ"9WywܫfES :R9t HGo(<KjJ`Re;y!Q~+DJH{ >g̤I iNT|rqK7\~Z0OXK2!vJ=Ū}ȼ>J[X+e#_)a% 3 vl҉mB' [&#gE!#! ă- S"qiP8`lLz߈¥54#;8!q۰Y*wPvžj>)GN쇙lj<ŧ„S FhUB&D@C1tZ,?AgĠ~~ (?jK~`ߦ' &fN /YPB[+rÛcCe*.sl.7ލjp5+6KEXFqQ 6"ř z5X!  Vʨ^5XVK|?gc59ZKH~rR;ꨭJD0?l6!BUp~\Y⪈*vlYV 鰗x5L$(Ґ%J[9{ bf5PRw*_|o|Bʟ!}GF#cU2RT=^U>,n&Ya4)OF☉8aNt@pPFIч=>7E: uɘgkE']E+}w{ɿskgv][Z!{jV3/+*Y~BI9"`WQC̢zA7-?}5/NB2fwąhFl Cpim0w!{e*_Wtd5*Q/5!F~2^A]Ҥ9t v=:m$*2GyŰGdvg!V &@Jm(M 2*6V쯭L~X i>R-nn_YVׁLgü ^\#(}\OMυZ u5- 0ZLH jR/0nҌ|\HMsZѲI}ZZ=n4n0 ^5O~6to;4{&zu6ښimp;BaF0*l C.vGՊk͘8c&hZnr4eF HUN_V7#PW"ͳj~iM;2\t=7]ȱu8[q u7,;Nm@,:uy^VPy-eNMQqaŨbU(LBR (o러JCxB@! e5w;C~O O6n^M ҫ4~Sq0rjܠ,VRKc.qv's_M+fBTfs #6?lߍj-![:-ҚalzCY<2BL@#CHYv34^ހI 4+m96IV@Fe7;5N_ZWV+u8C.LOh74 ԘN%9)#$^LԤYGvJb^Hojk~Z@^2>7<=Smhpg Hl'i-LPZ#vFSUɬjp'NH-n t2Fm苒܄'ݛReuқknȕZ_/ܘF˓Ƣ6Wi\RKuK=]6:*e dDQGM/:LlڟlosW+9ԼT0HRT!MQ4b,`-=62ZMqS >ڀ[G峽>jWC<.,#^)༇w:og,;xTvu_̫~U%{lղWFފsA8t#yFN␑49zʨ=)a&EϏQr:mN !~i.Zjq$I]41աkS)ҝt7yi~ //Q:R0F$x[8GF+V WԶʜ(A`!"dw!Yqjh"d6!SiP[~rzji JjPWҭ2fEMyfAfϭvɯ@R z]Д7ۊ}%-UAc]v4raj`⍴dǽ^vKnVK:N3(_<ЮU :'[DmCP? FUus9!>>#h:|Ls\;pL5t<(:|Z:}F8.8E& 䙰a[Gۺ׎DZ"]]i!~^Pdƌ'X$bt@mEN=.7b,ݫEgE$LJFIgbb y{ł E!گVm[_Tk(KLKyĎR<)e :F2HOvɧ {.4p1c{n֌{Vpw9b\i?j9ܲͳZS*~`߂Y"i^U3ϩiz1mO! xTCL^~i+I2b.ZN /9l%| B%iⴚ4_i*;(oa9.vSg;[T5ZU_Pg׫ԙ-*SKiadvn/psݳ-BN/6#B"O$̎gl%e!7oх `b4$.鄌p̼ʂ";$ CDT%Du `a/?j5dxwQ3T zsN6YMg,)5pqoSg= 5G>œzzzsc=걬vkw*'nDthH64ͨ5C }ς!/:GݧգGǥ]G'8c󢱓;0lXq0G}~<j&1ۼfj37Heܗ~&//dk*kq?FKUc5u4_Xo8Ds'7)SDTl6]͐紖YPvTQ섪!DvFw=9 3#󭜾zZe`А畄92!aGIY3$Fp b83?1fw[:9h9hm܃-:FJs9p{V~>9nBXE*Q.RE-fx Y0`8Kɲ꼄 Kd)n@<ϝ{/x?sc[N/gujnˆjG y2kwJWjʇ>am)XͽuG',/qN٘xB6H}#®qalR1-ΊsY!]aks5y=z0_QL߳SFn$?[}Ծ w oFOP78LKw EP^Vy [Pv4>"EH>6.#+F[ ,kG%{'$CcJR8TrTg-6{QZ@a.uc}ذķ" j+J b8#铵㢁0+n6zBp-c<ȝnk/pZB_t=}Ҡ_k0i֪eOv;[{ZuȈw[rϘXK:QqDEԐ1iOG^A\P&yqFvIG(&'?Oݬ;ffRV ܃G׫פu{t] ^{ܜ>vz5|=eVfqͬ.*(ltrY \z%W2 Q2؅=Pv3eEq(X4vAak&K> qPp7 R^̀0!MZ#"7>/qK&U̓W|fJW.NMRu\0p=bTyA9a< ƑRt&V~ i+j7a6Yv5i|ЭPQ.+dʞMϢ焦&~ZyF۹kdhlj;^Tůw򉗪񳂺 xotުѸ ZG@a{}_ujvHM7ܶ7ş[q TZg N@s']6g1wŴKY;9c=^"Jkdƃ*kYP)~WyW*FBsLlm:”Haڸ6VWB3_'n"4rzuZP{? cφKka劏w. Ѥ{6[>޸/첧ED'@1rΛFi= Z9~ZRUȇ)u3ǍjҒ@+avҳۡ9Pnna ~u7@:XE]9,oq <%{In'w`o`wX3Hu;w &펳$͔SVZj`@ofq0=a=7%.ԗke&Ƽ|FffGJ^jZ$CG! vDwJ;&i3Ê$E$p^4ij\7g%T'*̀;M˦>7.]7PWzq(-G`zsYe|>Qx^KVu9`zb~b\p|l]|J>;^V7 G 'ӓYC KSrٔlN\nw]чW&02衷@fpf͜|##9:.%:|,zރ@>Mw-| 9}rA- >%{1R4 x/YBU05y4)Ce1y([^*C&|S3)Jjn#t"z{ :n&t̎(?ƥ&v}$ySI?m",Y鮷(jE4uk!۠G!_s^;F#{[[%=uQOב fڜ1g 2F aLU> AK{ (ض(H <_v:KƬfUf$QMM*&IĆo:Ur:7*?啕ӹRQx^J@q3$EJ)}[f1dHzxݰ:X(s1;~%vy!݈,%=6+iQ8YpZ*Ve'ԙ1Nndn}&> P#SxO; ʄĦ{ܮOu@U֠<Ǿ{oS]Tyk}> B瀥rZs%b{ms|?${ݯFZn/[N2OOZV6E/mw7{P~5֖`7`xv~y)X=6w~z'*}wgC|H/uw7tqs}/5ˠxm(W-E\-2Y((,*w)TC*zcO93ym:Tlj.rA-tifr0=:O`za[dF7*ldH{z1n[;uKVnw3uCLM kY4߭K\hBQyC<~ZxQ=^o5Q,W?uX61XgzҬ8 (+GtIQIŠ_ꅝ}DlEW AAtc yKhT|;'(Zs*!w>P?L0_9?o77 ~q9rQ\J]N;/h_ x~n?"  E}¸2&O0u尼riEy2BvvУ23Fbi(lPnj>X̍~3fPס"ǁ w=@xW?U&7g*"7g54T+~Gօj̀iUMC@k?TxˬڜѪ3U3gGGĮ>?m^^4|pv56_ߠ^= ? ̞(>@pWsmO p 0T]@~Fv\&x,>mKi`=g`G/b7FOR,A;Sϕ -Ex;t1'MR)S-qp;ezX%1Dn@ sFnlzPp&;!?`7&㦧jj5X6͚I¬|f=j+]¥~^? UCN%0Qg]JrXP_)J BI7P ltS!@ R -2L?71N2_6,4}@pwӀ1I c>k7蓀E6Y9C߰Kv ?-8UǥcE}:ajD;̨d`E/@T&Rdti!,5C@Xv^$&Gw ڌȈB'|EvD~b @9 xNQccLh{Ba< ׬@3 t(t_<7e9>zi:^ o溤陣{](k|k~'k> } wyW]Oynq,T=ƟC 5վwk=~@m&_??ǏyZ$6} _o(m3\@u }ָ*:~I1LMT= ALosEw)wjvm*\AZc6;R (zкamr"ڏ<,$ir |_^t갶uQѳ||숴sLTALĉ(&aOp¾x("n%6oLL7wtwZ5C{#kCb KzN}lgkS.)#fkZD?$lOnq϶eC’>\Ĩx%& i 0ve1@<O ^PVZS/s; Nx'?I\^)`~ 3lw?WoiUG{fW>NSQ0eq3>VnBGRbhYءX9N~[mV'G60 Yi IFy TOSЯF1xUTTZ6[_-=\AߐTS~Ԟ؇wW S@nR2i-kn3YmcػY%Q:8Tu:eX͋9ã ӷƿNM1pzDǻ3sw$K_6" l|ll)U㕗׆} wYkxTH.6^F|̢o)wt<42 usw_9߲5g]WOEקas J>Ռ#TIa[b; ]Np|| tß\uė%e3jgF-#)MrDw ;X'? |TDݝM4G[c~~.퍈fnP^ -WکyѼuk^[>m:VFjSp:i/'ܢsjau^CK0cSݒ=/=^i<2 rq)+( Y0sj>Q'<eGAy#mÀfiS=+}^J_%-@5pWbæl @/)YلseC)Z8hTy؆3zҌwF`Jw@+a KmܼyY8")}2|ۯQ3a-?d5`甕GUS*23*R/p$Ǫ I݀Tj ԝm׀CG0x^d҇ۧsscFǰckzOpdݧٺOsuoO\t9r x~^>>=LT?ԯ /_+D|O}wg?;\p#hNWDNE&Pm:[ j4oX$o,.}RmÞ(g #-GGLlˮݹyu)ʢ\H!wL:SҵS4풨$Ǿ̳_n`~ǥI}ͳ }YSfhorF1%D:z8=ƋD/.i㒑QBr9!<4?gХԮ$VS~Z]kx5o}.Z 4XOaӰi5J.BLjUW̬E=(#qA05^I?$مS "+i^uN\V-8OEiެ-4cሩdT:]E9aVI kRẍp]K\I4kn z-.'Th b/IBQn vQD/y@@J #(>xPQ0h2& 9iaAQ 9>$]RWJg׸Gk@t~#nȚ19c'7H7@ng Yban圌 W2K' 4b[>AM()G5yv GԄYFn7tݚ@G9m"KQ aLl GdSr̬hX2.Z+cU%GJ񧭴z겝se&ΙsnUl,-6zFNeYusd|`x쓭gZ-kh^ @P3wmW{oN;ތEybq2m4w+ڧx)ώ_ݛx|Yw9нd<\u̩Z 69)}nAGR%dmNȰĐllBrHz#xU!hփ/~M6 i2=A"~mSmG*ґR܄;-fTq 5 y3}hHQ=T=_W.YW\ɺP+ZIyb  Ho\ yLXTz)CU⢎r[(QIA =,ĤV=!A:XT;4>aiZ(-DD'`s$yQEQB[2 +쓗q",?` riyV*XR3VLlFPs8SPq:aFɵF՝ӳu-f^q|l<ᅷ6PƑmߦ".:~_~~nl"`lUE|¶M3|_޿tietO9pj.7u>0x.߷3ch 6|jz:<|rY{G\--_{vg_wvt Md塖Jq 4s*PFQ8՟NO/aAO>ϯ]kBwݭmGakϰ6aUtN9ka7Iڇ$.A$& LgmXL<8nY5PkliTԫԩԨW~틈{w k~zO /jarpp/|Kz< p3b̔;-NrF٨azd[Q:H/@.@7"]QG)$+& TI"$r_QI+<1wDG""_Rw)ʛTvG|!]Pr'%T$3.?L9%GGŘSEr!zєĪW"X% vaUct rT?'S]1,(1^YE 7*U)i$@LS:Imf!2XK6s)>ef[9>jQ7\s[psuܳAlBXsNy+6?p8?uz6Y6G:Wn#9rx??߶3Cf@w|6%⏧>- \|AZ͛>Cƃ熟o|x>kɦ}bg/|xSG"߅]]={ ZSgN-Cwl'VRl2\ߞރO/>͏?n =kS7p9f-VϛzUG1~ӄ&f(9EVx;㢁qQӱnbHrQqA}+P= jdbh ITg޸ЮeD=+՜ꂆ %̊*6)OpcܲqHʶ &,?Ћz!85/N/ cT"zI;O!%K$i62¢(wudH=9qL:Q=o8WybuuG<ũwH F1~L܂~D8>J3_WfwiO}OgoI 8RM+O/.;W~sp\ >p$Ն&׏ 6 c/.ȭKlMegikkoԽi_T6O2wKĨzO΀OGM =NlhQtIvIy5 ׬ /:ߒu Vne{33g{:twuVi"*"fsΈYD@2"  JP=w9V^~~߷VAAl-ccy=qޯH&Ѐ+ZˍW5h"|gO/)/w2EdދSe&8 xtXG]~djiT [r@esSLy}u4yuaQ"5t1Öl*]v(e+|OStAKF+^˝_^e3z/+OӾym$K_+y1Rrj銷%OG//+W_N`^0:"˹׫uTOgYZ\AkbBwZӹ x jP[6hNZ+_l١5 lvuLp˭ڝpHC Q-ŪYS^P,84 #&h87 <\C@(l?^^.w۵{t}lH7$\KoD hK7)Q'{v-پn/z$?/~Rgg{N/|K/eO^_$?3Y骽j0r=p{~𘚃g@Ni?mj$ /NcC;ǐ]4@-.@#sSBD8$hU̵J\8:՟(/RCC_bnlm s18:jh6MxqVHD.4 عo`ފ I&zlu[Z+,eg 3Fk~a{ivQƇUNe+HR S/rT)Y sQAЗ|A:_Uzݐe r_PR ¬R w"D8`n>=@<[Pl4_ߓpY1[ [ۤyu*>mSD9=ÕWsFNa\ZC)s9ONZ#-r*V[rA4`o?kc+']R@-[ 6RpÍ$Aò|I>BZП30A" y! \IQ_n@K><[t{tC`!4$?ըc@!O^`z@-ϑT V n4|8x"tRyUg_~sߞE_~b/׿~WO^N|Lue})9+ԏ~m90hAs& }@-֍.#˜7& ˉnjq/;B-]B iQ j~rk:W>:џ5 %0g|kxv>C6ێs6b#yQwTI!<89oYv 0F6V[ѯ\~w(O!age|(Mςf/$7YN{#N1DVۭz}I&QYsvvAQ:4my['w5A6粎rhHӉ|{$YNw,6D|R{Jˉ'=aڐgיtl/Xt*τ?/,[^0įKlէ+;D Ni7wm 鋋8!MDl KS'384@[o+֐KLJ&]27;uzW xBxyO أA:`! PIɾڏw.v#Kr{;7 B_f@!<,UȵIQbW!ظ:dF55džÏ%ק}𷗝Eo(xE5Wҟߨ|'O~ I&A Vj?|7>c% "fԵO5# ~a ӹY(*ZCWkE342j5LAPuȳO- g/7CH R҇NH˩jLgy Ȁth+hW:qkmMe44SJGHQc$=7-3>y79֋iO ޮH opJSuBM~M9Ndl| m6r Q=8y 6!qtCp^ ,_Ra đSlT v^#7qr֫5н!_/$.ЗSA -x~ {`][kvqe "D+ nlбK,&[kkkaZ$e$/(#"I% tcmA B5'hk 5GZ 5޺JcDbr/K5OT9Fx(Ds XЀ\|mHV!m[~PJ(;b?1oIS 7LdnR~[JX;CoU! }h¯)y@ t졐" v_WzLFm{3a-=)˭-2pAb`$}7#@"X EQ(8]fѢYC`ٹ7"9=i`wBx<"HM>:AGk47DpAB #j o.բ2T4 ?`H$ oFчT(YxRFæm%%`71ɴwk\L2贽jK/L?)C{Z+sA]l]%cxT:4lϼS!/rJfI^YYs|uzAJ.HT!ߋȃa>뷼7|NKf̬f $$a6cܤ&(J߭TlOGk4mҚ;;S54E&nקs% 9l^efwoM;\!§PyBCR[cet#u}-ҁSs.]-wq8׻a62r8wuÊ)pkꈅ"Q(YK7[+#&t5Q\jLsXx`œQ,H:tNk̴GBR%gRyC4u",HSo¨I,~m۳/^ |nĥR~L=I;z~c/'5V 4GtqK8o@*hU;{;:r?8~:K7w;5C$jQO+T0S(gThU4 #(AloCZ+OHBJIE2JnpM6fІ@k=hTWGXICq$_y0 F& ?r8Y쏜O^nN{6dQ)wLL,#;bt8vv(0$/M^&SVB'$0l#jqCbkkכ0#o竓١vQ߼OMwFJBD!'K{r4UbM#:S܁Vr%ﷱ҄v#jc>{cWˏ9xګs'p hPD"j_m>wdYE1MGBr3z3 g*Ewu0JoVcٓ)$ޤ6Z6n(k$,@7cn{%OxrL)?ރk3Ws"Lk.I37nh}έx`Y_0=p|4prVex.ܧ׺a'wQeqY\ `#@kǗ/~3{) ~M;xy&%͛)-َ0B ajvUѿS9ȩ%̲ʧ↝LE/-g ȈɂqhHP6Ü,ޟ,=j&+.q7'Ol@i zPFajRUiչk?֗M е8&fXw ]kve)Lq[n}3ׅ6l#EaO=1ڝJJxhP,ZD)j (LtGk$"Ep 뱳mpwএiloBpxpq ]/msDL;56͕5?'@ڿ|'4t4 7"/ɍ6xH%!<g`G>,O;t ECNJ@q?Wkt[/jia|V®" S5f R3r$bq 2>Cf pGQbZ5RYG݃ 6>JhՄ` HS\U"5tum1D"Uهu(5y\&A% 4"TUtvC>ۃsYuFCK%I OW)K71j wܣzdD~Swnemح Vj$Q(=@m-1A^VC#dcSRy@5yr;ze-ȯM2M*ȭW|brؓҥah^7;ݫSS:uH <0P{tsG E䉞2A{+Oҙ'|er 5XcOƋ%QO,eO&Q%d n\C65>3̄dXbY$f~ġW?$^G,|  {DÚӘJ@S n"3b[I9@_mYQ(O6tP Gwivu N @ws4n~?R0@jsIχ~5}??~"U62%!/SeF&kC<@o`HMB}īGUL&u cimǯ Y8D+.̭-QJQKȕ\H)1 #E{EGCKbmO mo4ǪhY3(F#Br_h^fh08:_U,*Lb$ͅ{U9~e}>"q=lE,S3\Fa1o:Smvp_ =kRyE2#w)|/c<1v85z!` =yHK8컚" F5g',?_QO7+R!ywDnc %#̢Q*9="*)'kƂ$UNw=CKDmyS}rC*eCRjvե.l%@*jg| R)YkэfVi,(}҃E*b]0pģr@bg>ݚIw)#椏fD0 $vtx%) `&e'-f7O0~R_<7H߼?$I>aq##@1fZOsGgnOEEsGnmn@Yҥ:9jޫ#I&ct0D %ՒU z~=;WD*\@oQG EBn7osVbq8\hiغX%.וpf|qQsZ=*؇<;*IR]Y"_YphZUI >1 e\qaPu^_{^UuetlɊΚzZS`]Rp~7=Sҩ=3|cY/d͕rIǍml:wnu!}`3D}B-5Ae> UJnXfsÈmNF1~\!L_])kp{׮[g 8= mzq{nO0@~7&{C!){/?'2YL쯁g~k2gǪ 6xwAtdBLq;M$vN<C̬gѳ 虹)DD"ycb`@h(^^o/wc:ݭp->kM0:Z *<5E<>,TcsL94j͘DNI8'zk9Bb氼P+S6}cwC)’snѺżS l"=FB$| :(Cz$6+qLJg d'5”9_/F}ةDe \ŤBlE:Lnm~ދ2x!)Ϲ| ȭ+2ʐ4y`> +-kYV_c hR trh`)/M0MR]̗rYz $tyD .%=t)FQиWu<%=I7h9"2p==_I;{GnSY<S-S3oOysYkťyQ6r@v;L74{⿱w@}|6L.w ~{-O9kEѻOlKUk{跆$nF}>pOVnhhX7aEˤ7.Lst,DSK%Ypa<p\ʘ #bX -Q^gh 4VFoM76@h$ŖV]U?yAJjQH5,SZTa닥mBpE??dj14W)/y !iq񂲠<;@ZsVI{ ˳[Ƴǵj)3rw({S9HQ0{{~" _pAZBMKqY2X;!l[pqW2+Vƺݜ:8%bmT-X3' ^n_`%[c4nbdIf+U U*;^".]0;:rV>>|pAFq(A2^|SA*9?qϙʭL9]A/ [N ۮv άӀp@({@~ .n?DV?(0f;3qo'P/?i/jzo_uj?;wRR>SCfT6 \@Cxw9D a B[([xЈ7@9bp*Q4[S< rS@EOoKgWJg ',2r\I.M!+p)!$`C* ՝hcG l" ^j!VvP _Y^PQp]k#8eeiM0 CUkp)`8m4U،TI*҄-3H6.j*d,{AT睖G%Lt3$nS.vʰq`}FZ$+VFŷ,ZM@;\(.dm QdChp}Az6;kuLJ? 0< `.k۶s{bg{k'{n_"*6D{lA7˼qCx`|d5'c1%]AX& *TK`|R$dw&y&Zl;FoT? |bpGV_#o [ ߨ+ w/=0iC'yUӂ/W.p*)j#@aL#7=SCID`}Vnm4x|`PT\w<KҢBKmu= ,uBKzȀ'CM׃' +kq-Fbi|:M#RmkMhϧ<"cWJҨF pXe-u\,鷬)t'{p!:g.DKLz'֒rjrMl-ȕQ8*NEa5v*s.DaVn;ɳ!%vv Rz+i=Ϙ* "yb6[ӎo䭭eɌ}R0W݊s`Br 4Ϸ|?x- | $<eU eAKB潸8靟qq5nlr.Rp}쬄4 `]."RY$| p=ps`Bq]>,  7\)'VSzo_1'o֞aJ$$?W|ňxa}.I󗥺[zܝv  O~Jxn$ z= 궹}ܴ,YU-003[G'T<;_8[4]a si85LH%](M[WԄ+HE;}T4EujOM}}^_|UdjxeY ΥYbln:qug=-R̔sf>3ݤy{@- =@V}o]8RiZצB~ ތKK>vkJ1h,@ ~zN,i(R63xWXIfy2XsXp\['EH!mFg⭋^;T0}B{ܱ`=cTb1/ٟGpwG 3]jk(Nx!uv83[Ym ף\\#S!B+A, Y3ྌx]!D Q8z0cwc~PCkp6|qZȵD|tܙ,@b#&rnUxp;Wu} =F[lе[k=f{/u?}WK'_>M_=e/G~|M5o9 3_ lP)ZC`{=_LCa~nyꢳzh80:jՑe]]9,]LV>}niR/ٙA G %va[Ys{GpUqhecTtnP;c.:;@'} +BZa5VT݄:pYѨv1Y`i>5o2[+8d:`Wvq=Uʬph \ی[KƊgɡe| /:BtLum0(G#+w3#NYo켹XS![v ݭΔ]rlڬq9bSh4P5toþm~$X * 8J>AA1Vq4\V-iK۴F XJ2BӥULX-a'`S G?|v{7h?I3:`[kdj|{YϬ\zU.S[66ata*t]ހ+pB<[ bq0qkY[g "\^!` _a9_jUH?o'I~LI8MzkN}-C'ꏠ9ӕ㙮 `n5)"Ӎ>7⽒0FG>2I(uͦRQqpL5l7L~rr2L"ŒYLm?fAژ>ѼNG^k輩ToGj = ~tnk(6V?ԗojO锈M˹(*tފL\~dv7nxbh~vV.+^PH\Z-0X;ewŝ8T(wUcYºe,xMNL(_Tz9:hMQ}>`mqC==Јge;7U^7s$nMbOKHMQg&6#_>j]ts!¿wc>?-8B '1GzM ]κGMc ,zW3Ǭbg1s de:UJ[dR1ѓݝ'h/61\0 DPK>P=kfguX,Ԗ[ Uv\`k:RA V|?3ٛ)Yҽ"llɟEW*$5fN"aKvb`parvIԥzр9 T4H\G`-wShoò{>إ;KBC;ܸˈr63E{1<&SQ /^] wúwmo@$R>H?~^~=뛩f_'.IZ}$=8/4畩¤B K%=|| kvf?ѨxO[Cr[`yF1 ϺG-2¬?/S i*vn|y\PalJJfibX|{I[QsZRWq04~F*#'Dt H-YWnofp>=d7>EPrx@;;y53c7P-[֞s(| %89 LMDj~} y~6,9zjxoa\Zww,!9KS5VnC _(ҁ5wbo 5p]UqF|e{[Iւ1b3^vbaC\tr˼#.6L8׷@n D&pd+w0jw7V£Y=Rxs۟F7jwiv3ʓ(/-'sśc-V~w rs_b4 yJ=r2vp}:UG`W:vm_d.p8.qL/pk&ã,s[ sQp.[ F[ޚ:܁X|ג]a t ~ ` } oD"e_<)Ӛ_yEEw/:~IdONXx@MzNz#R묟7Qt./ v&gtnX~>n |RGiS?o"Itoi2>l=%+%1©]`T穕Ԋ2fa@T9|q[AyR|Rh jCuP0=) ?H F;AcWS}WQcomg¸uaܲ>Z l.{WW (;%8>B3Q=X~/x;>;Pؽ^ҒX{p:`&LvЅ NH]"D<@el`s֚$*M_l޾ZI#JP|/8Dg qEʶJC[UOP}ٰ2~m8kվCSś"A=㟁Y=˰;pM8`}9mq::-Mc-z 8Wu(r)C; `6R_&suQӕhvZ{ˎ;V^c5nJԪx.cFz9G yJydAԩ*G,ybB`yu~uXo 7 r^97!FiX8Z97vBJ Ă< y,"nX$^+ x5Çg1lk+ZY.\^RtVZ)]4@+ `v#"yDv;:50IgڂQ{(f !{ kG0({y'X)FW0@C)!T5߾kM{Kسɋ^'l&a}x- 69ﭵj~p@oa3y&\[8J21[褥xG7HZIٽ%Azcro]^+_Z\ۨCd F0U** a Yq vېͅһPs= 4pB?h0m]-0ÄX}dX-.NR<= ؖCEhkepREHihW7n񳞮bsuا;h8a?s|]w7 l.F'z ֎^#FN 7h]Nd Љ|UyQʼ˲ ⢮Zkl:$ |-w(/M%ꐉҩ'(h@ T,-C0CB`*^Ho.Ul6pR)hxUN.fT (=@<-i[+R&sؖD`9uCbσG8;5څ{_,f4cπJxwy$(>@҃SS>V?w۝+Wݩ'ꋗE Nqy!|<#(?|W D*zY+ܷzvI"YęW_%,}CMx|7QsmKS[+⃳,ՋIH@)0pQmQ |C4؎o${={|lA؝I {:KO=Sݣv%ʍZRjF-m +-2A/dw@a -^#RYW Q\C9VjHzTME+ȑ0n L Nt7_Id8^qqvBPɽUPbk`7=^-hkpn _$CE!}nߊphKJ`s9E4H*O`v8-X1[!HG^r-9:-Gd8,(;Y`6xrzĺ.}O>v9U̡Vs9g &D1D%'Ar{ݵ޷x J75\sE1Fk+k` ?*,֢<80ZߒxEf;gZf]Z^kmqx>B>nƂdk3EB'M-f~P޿>-ӒUwo u?je/z4nY실 +o^Q^0rSPxcHt>E>1?,94ݟ&/轾N>-(m! rIƉpwboQM9ifN Dbz-\ZE,XC@z]_~49mU5޴ֺ -.fġ%cH"RM "q2hr-nxn § L Q Nlfj^HBTypiޡ_K M”EsY!Ŷl$/V WkDƒlvT8;F,p={KYG\Z-fjjs埤M ceUsQ$OS_mn3H3dǚ}ʹ`~>ti!TrL){4X#dv/vfHzREjܰ횣'n O42ղ)5m0񀇓=rѯje}Cȟe7|=84=s|Z2Xu7ZVۂ^UU[8OY=ӂ+,| G܆0@|qx.x $wޠ? yQ;W:toҳooUο_M7o*U|KL'Ozď?O{Jxb;39'}6-:+.<-ؐ3C[9 /.AF(ՏW8+51 K{ք픓&~SOn\&/l֮6Ieۄ!9S?e 鐑,f烶m,Qwl 5 {>A*h[ l#Ily(LBNOP@3:>\nȭDB( 89m^t#ٖX;?b\Xcm~\wb#+Q o*Tf]Y2ϊ󪾈*3Yee7Uƪ:꼟A:L#ۤC ;;l.r|EQ͉cN=4eUW7&Jj5iGc&c^=[Jxs6F!^9:?쭫h`:'* D`ur_89|?ϊ 9>p/ۧ4g#e~ί=hU咽Bϣ1!#},nw#U ,[+|]ѶH]gpLP@jt7H A,t "pA:`4+30PƷ^_|VxuB5t۴kEIppcx+fj!;fz;j9-e59SDJӅ9m ӛ9|Cu>9٧Ԡ u ,QI2*UCW=E`Ze7$b1<$4j`4H.9K%ˣZTzVE5Nƅ) ,( &B;o bs GWmAٟS7o)$V} Ij)_S~I5[س /Vђ_$Ŝ$*>R\%i6 ~2mx1/_ 7 QU1B7"_?Y[KʼnA(x2Ll8TpJ-\G*c,3aa;9tmEˆBM}!z@(%-0D;`rw8+ pLr͖t8|vњ"\qeht!'(hx7zT yQjzTKY\a e92Ԧ+U2Ky|cSP_zV_tVyڔnUU6g:,]Z-aRmF\_Av =wKTl9֦5[s iD_GuSu'XL)wYlԟWqwDرMV__NHyW ,{51`Kh8`F!?_B*gguAKXzN2g.>Ѽ`d8 \NAh5 ^ p.@uȅ(n.`CDF -ڡ]ԅj(+bݹo۲k9 -,6̊*_]jw5?&Z]w]O=I<'ohCڗMͥit8E n}|_eW-\]qb8s:0YMJr)X7Wʜ.O3 vF vG #GùlӁϢ/}|̩J̲:ݝ9]C``;c!3B0#.Qid,+bPQ5reCwF׀w !+;jt`ٴCHR7cdyTlaG]8'^5S7o7Wn 7)Bqh(>+UzkUeՙŪ֢/ܚlAC֚;O3dm@ٰvo]SS";#E&oH8&޲9fЧnWz3ҩ*\5ku5"^&B4z*Y&W'H @P=ta`CЃnaAQ ̲ALX ?b aE2CGO9?PS]y1R{I?o}ˣ~|{?2fիϖ_?ٌ{ KIfK)+&7QixiHf@dSA% lI= 6ԣ%~`R(䋉) ~b`Iܽn9lek{0Z,;/;.+2GXc\>o$t8l [ܛ(vJ580Nc`,X z0FsP]zklǒ~ȍD3[0w$p Og:ww }q١,/eK5ڢei޺0mYt@+/Q4(s%ՙ܋SgSmђ~vҌ4磬y̭s P ޶0_7&LQB%ZLfJ RVVcwYv`H:},>"gF,zfǑH;NjBHP9To>䴷X:Pe1w7wp7 &, X3@U U Է@oG,N7buEAݨQ +B3?GÇ1T榙J=tsm8A@Hqa<3nѴ_4W9UGNVJ4sUgY o[MWqE6$n'nm-CK^ag\2L7Jp|X)gx=WY,?^lrG\^RL9a <@| =2f}Cj(WsCg .fF; qGH;͸V,],y.Cr7ܢT40ōTJ +Hǩtz @ +{!4%<e'CaWn$_G>?`otQk[ue?UW\RSA95?gYSzoi0?~{jӤ#ތ?{5ddėw_D_eYɆwwwѺD9z& $,Cņ0s` , 0#30]짋mGjVTY(bϕpK8bddP8TxW$.?/=-Uij桎a?n"8C D%>XQ66 " ;`p"&j"X%]tB\-p#@@跛dƚ-qV4$)~|H&7TA:ʤ RHgNZh-Pۊ55YlU}趿R?ܡo,䊺 hsw M o"v۰[V 3TL%_ʙ*;`/Tjؔz6+8]-n" `.|q 9dc\C@6M;۵%qV4]# iԨbtP1-okB@LT.o,AF`i)h/VV:J%yƂys⦷Z.Xp=#2 K&`$VB /anzsҳ:l&ꤸz\X0TWk)M}^>uK1o\({=[ʌ?,<>Z~ V .,vamg>- ŧ\6"y|s6s$>!#D6ܽR3R\(Kj0u : 4 gfNu#kt\GK5)^9ʋ7vջPǾ2~e>+>1>vVWɓgÕO+~xTKMO%BF}nyCӷOč} W o֒)q[1̌woe9׶W~0?XU/q&>?v6P 8YY^l5vE/W0sؽh;ֳ׫ ?S+  =E"YdrHWnMt^OۦIQLfC`Ȑ]yh_ ȝX솣^&F7Q,? \G.vkӸMڃ՞sg˺Lrnf\1: t67՞WICohT6]6^EiWTTk*:m*6h:-Ǝ *oCtZ%C@#Gֈ\0^y u8Z+8WN;WΝ:&6<)$k =q_H: VzG78ш@F:76n# <'AF>NB ~!p4ds(hCZ*^Kl:S#O@N5"wh@ rEJ[%,@=@ޅ ҡ#~J/e:fr~}Lg%w?~T\(Ma$w?x;󗓯^ŽYN_I[O=p~~0-<=IVј$ZQ>ؓ }= ~J%NC1b8FW؄Hh H;Dof8c4ju]`OʄR>X(W"øPWˇ.4&x~znFto#m@X,P"s \#zz $vpN0*8LA^^a"\?1ǵk"o -| ?z┒W.NubX:-h6Vk+5bHGsFZj.U7 /e*AڊJ/  yRHkf}ާB|IĀes1J!cxӮ !Hdd4VwR+U,*rfT \MW4b6bBÑZ+a+l,%FlX5LV`l'%( kv .7g-\ sB ~bnw2p*V=de ET_:t1rnE !Q`F!&{S\ч x(k ։y|-w}?㗑_d<"d>Zh!Bsb B٫eG*vV>*yS8/O`Ik4YlӗO_\k* !!ɧ4ާwiYܘӜGʒǺeOR|UCUE |9I^NkĴ"rbZA_+~8 xSz i#^*ɲp T@hWQ4S-6= 3sҌ T@.R`H_HP @]@n`(du QC1R :IBT.׉F;VciltଯCRǫ);(VQշU[ʔ@=,wz+5u}շ=hWpd0f%;\3#'u+r| 3c¨q<<$+C>DWy>YX"$ ufVAI/oϹ, HO "H6m0?mH wBۻsSjv^kflkrp@ŊȩHs 蟀a /z:@~? 8ȧ_pCʀ (֮y_DX!Dȋ%ymX86;6* j1xe *n [DfƼÙvE7:Ghxk1\'f2BGk.U`02"P"-i_2'tرPн&W&7qP+J;6[χo~Hϸ_F??xӹg V8R^fH'͵+`~[I=iؑ11qcc'^cS6R^o';xH~d%K3.S E4⏷i,Ogg:ǿZ飵9S 8@Ev4osohpV:ǕOT+UErX1^kL4f&>-dtyn"VB4`-džr&"*5)R&TL;ux:rB1Xv5 qc@Z,+ ZljZ=?HZO{rn]1WU )S4ku֎Z[M*[R.UwZsN;r%=%rp1vgR `Lw.<3ƺ=; `v6Õ/盵xjWjN>y>z _8p 渧[Eʯ%V' zy֥q1,{&g@ Eƨފ n1 D!أD Ep&M996AR!r"ЀH~eDˣ@ :pr(\X&a}yCJ} AeQIkozӾJq$NbNwBSb RuHIvQvi<:\Ky3_WIM 3?Nzy {7wOObb^l&SSb)1DɧxYٙWE֚\{mZ}mmt<Y !I~$k30nhGfj9L3U*(+=ҡj%#M\vEo-7k̭vDטm>s؊o-ʳ"'^z TOc9nsiߡY)E \O]Bo>?~@ j9%BVmQM>褮PT.o*W7k u7lis ,VuJuc7vgO]Okb33#L{W"+oj"l>9n=uH C~#B;\7{-*Vv(VS i΀R:*5Chp#;-BEDfÇk;0xO?e:i;ڞJ̛FA8s raB9ֱ+J%ÆbNu>:_WrP*o(QuZm%UmಥࢳbR=`m w8]mΎ۞nP7l/x ѩ $c`b u) no'SvQ?[sT'jڎEeaDb 6迧#tVD[";EmJ҅yݨݼQx䇈R d0 @+o8V#)`Mdm6@}7y3:y+*J^t t` ~3/ MjB@NuZb -*#2{.cuB轀6 ӟ`tR/{3~Lq4G< "/Os_r߬đa)N$''2w◫jbߴվ((ێI}OR&=K^"ΙO={>GzOBII9xxI=/L3}g >fs3 D'9y\uM$[<ؘcm/ptIzҎ["c}X#P-6ece FU*ݵ:UwM;ԩ2Ìw=I%'AidO2t`Wl);zt ИU"Pyǽi#[֯ׯI$#4Xf5!òN!7f9UٛdHS1#"ԤP^ajMMםݵW&T펮.'dd=6g'0@c| Yġt ٲr=]HZrlj"X.oF9Xe>ոP.sUsLL,) bYi9NHVK O /P*Lх7um m07X|hgGs3AouڽPP0D/_ 0\9~<ȹ |%rn|]D})l 84! /z/T^p2^^Í Ho5(6!g{SmT62{,MCy<󗩌Gs'~~NƐb E;%I ;u ƄX|u-ϋڞi-L~MH'M}4880|l,!5$e5%a59f#^ZT4vV7󹐛w9QXQR2[V+-KYE#q/*6=uy K(Bɪ;+?Ԍt}UڞjuOEn4 uIrh`_\/R`C `J!#vҔXɶ!kP  [(4JcG({M;ifuͺj]YZYГҜvyFMԬՋ8|lSgx3:3( A@@8t?0n*%NnQO)M' >~9<@D݆MlR=͉4sܫs̴NKt[We{IZNxvHv9 yxݠ+z]Ձ %840^!a|-׶s{V&3 RtݢCC!0{~xtȯ TNQe~} E!H@[5+!zn}O_㫸NLymI/QHo2d\yQQ d~ko2Iq(g PFBNӑ.`R? 1r D@fu 0x[UGu(vT΀8QzNm83`fTJ /__ULMiYmKX(xa6>a%%HH K)K)Ko-K#}H_`#x5tKrRvfn39F:dP?ΐ[5/}Uk w:rI1f\ w˨8FٷK .p?,/YfY7ȈSw885Nw:( 4Y#Dء"[;a8RQ SVG|۱خ5cS>Eʹa'qF.,#'!p@O(߅Q-*|sD`@0J ww#}?=NoU?g2ė'YOV_n}Cˉ& 饩GEI,GRSY{ĩΗ-O;}~*q$>e86a*.&i(&q<7؄ԩDަ߿IJNNIy?L ?g~Λ('f-|Z25=?PE'Jdj8HbK&fwE,k6p/jZ绯HCq eFXeE׷ F]6c`W-1#RtV %?Ex0f)U,83.N'⠄4(]5A4fS㕬rzn_^^ȬٯbUVrkr$yiܶjXsŎk[*t퍖!s箵 H@h| 2 0 @ ̯40;lbT;ˀ)"&`G'62:$&)Ѥd6~%[w~4dGSW)hջ;)#v,W'B`D=OYqC`„o0D7 &|B[a6R”pk<ݪ|%&&7JBOpmZhwݽqww1 X;ڪWQ (:9ң[`|_-(JBŕOK[21~̪=1so7~nWV_K_CnͽZ|k'KCv9wzQ́(LXpYtx.@瓓vrƙFF7#sz # g'Y Dzߊ LN`re?6^6~a]۾%9ƭKGۓ[+A܈m~wc{g7soo[Zx={?zx3a4 VǠϛ`zy@9pM쵟+_߆Agf94  tl>2 fCd6WCہ2p30!ՏZLE- zn_7_U0fWO0-xKh웭[3϶n<B3wCwm0}#ڹd~잃a;?;? ??MBciD1sNo`gvsٹ!(G̢IvԶ Kb yE ]v⧽ b].ۘK1MYH{>ڔ0g"dK&ƚu"}0;̅r$UlrE%np** UHn!X^W)ǴNDC#q){GC kT-y5߲o\#KmCR]rMz Uo N` Z`ht^ 5-knn5uu.vT-*/+y{?oe/^9[יnYC0jZ8C&րodYrrGv1 ;]-㚖yK`(}j?VhTh AjnJHkds8k!ռG9p (@f*+0 >jŽ ѷwqąKmW;'m_ե4lڭ~"ա[1_^vx!ÁGUFj>4>,(*,d[4}&O`zj{i>}1_}k)[mMfisz tlPC W/3~ ơf[ctȺ0 ./+_7p:L} N NM/BOSqCSw]0u L,` {/# #N&>Envy i>b1-FIPr5bQ _P#-;iҊ#j5NeJzc2Ȧ<ƘqRMXsƖ)+":жTY1Q<e+؋"Q E,W.Fp*TUtgı`\>q24:G. \W+W6֠z1Z{@uFeJ䯦=uwkF-wh\Uu7@hkِ̓cx^~?wKaoDT{Tn BAS!~!{ݒVmྊuGFa?p՟^7GE$s\//v/{R\XKSS]z *Ŗs5˘sa|Hh\?*}զ٢Y+ܤ]jҽk5rol5>ql7?v<;XXv2i:Lǂ@8 4N_^x&Y'`zpxZǾ;ݷKI"dC6^켽:E0 c`xe0#8&߀`Փ!&on?v-k?z!0pHXrX2qz,ȡi8gM~n'{3ΟNK"9+G^# 5撑pو`DrEIդ Y6o˧2PldJF4LHs.ڒଅx[!o("Ata8ejZZnl4*HC%+R&Rr5ғhj B,vá}k-k-P w}]nX neȇY1 *kدUP3Jг*p\I`۴2Y'v]lA'QeawP:6!'6qj/&W*mi͂k@ͫY?YtKPjhtKOd|W-}}.Ǻy THsHyrpd|X]ӯV_5jZ/zooFj9w:_Cyzrqjjߴ|#kMO@Wd Ϡ(|?+e?F`#hYk=]+Cd`"8v \oѫc16<>cN՘_2lw \fp0cY/Oo<m50rd] ]{|䟇JvwGRē&A͑Ryl$˦s])Z=g?-RĄ 1#4Rnȋz%+~̎" ynMV%Qi-&Hy)'C3B0f YHSN 9k' cMYD U[LYĨE%n\NQC3o +s̒4fr15!tpEIFNr |gMp7_Խ"jmˮ+5>jhׁ&/%Uw.r\+ry;CVGMآKE$ pT1vGnz6: L7%c/(hWܻzC#ﱅ+vLY+|a~zBYoa2 2w9C^$ F%-{x(qĿ՚VݏZrɻc_ox\aWeܨiq?kulslf~Q:5{珪[xqe  UoAPٶRKG[֙ޏ4m˥10ұ00Jepֶ C7W~ނv[VKMenApwA0=4y&;֧6[ i2k[ɯ`=y _'`!no(7]ޱ3]y;wg== bo8rodɾX́dC'ag٤Jg4p3GIN>+O6ꌃ1*Jy#隉8o_ТeҳJ2k3t)cȤ)b©l6ˡ|h(C (b zWSjBWb`:OKSM>oe.=L0“0ֱ3cHOcPVI>exH5/{+Z#gpe)qJK{ȹ^&ByT'µJF=}A .]aL~nڌoPejZ_̛f3}QQ%I=I?HUkb'7nLRFVݧ\OZ˟_y_t74yTfqBԕ=mnV/' V?6OC/MAN[iFj͵uI0>.N; _c> v=l%t3,rܛӿMh  /#0t/8r34t=4x5w%sysn3sYľ"A@89;?tv$!2UD:F& )IFJ`2{X\xr)ɳU nN^2BjЅ">yN) O` ,kאM\sɘeMDHM6?"" 癊B A4%FH5ޫ!TiIzZ'TaH^;$Ӳe')OgУD'􂃙vfϕO QLjʺ]smU|__[/ugA56lX/Ԗu6dN7Tc@7 ͛k쾒wE˜6pFU.)EDk&>Ii# 2^L]3JOUfb`F`A_z˸Q)-gzP@7 0*`* 8 *B2VΣW uZRڦej̷Ayh6 bx)"xzÔSt,AyHGӊ*s%Er%xcy5[^ֳQ{Կ@={q94niW[8i> ])u~)Ky״f\o0$v)!.%qvFC! KD"}_'}d-D䪑<..i#RdU%#t~)ZQ}葱8JxҘlދ@־ni_}PmٲV,/v_zt5{*Bd}\zIt4;@Y' ]@ b+Beyۭj|sh 4֮Ckwwg%0A˽`-[R0!W"5R|jbX4ji2RY0U&Mmbt8J$@n?>YZ01Zs" h"62Vx.Kᒣ8\D`h$*$o75=ku0BvL((T|-5>*Y45:%)L9Ot =.q?DG!XjEWqO1b:=IKa[(|L$H`'УXѩDŤKR'Qa}!sh>Em92ި˿:Ϫٳf-_euc7~0~21>w v츼1hܨ{?T@`)sEAE&B)<KAcxGE%sqǕfFv q^nE!}[/Bߕˉ25~^(A J 9Fy<[[3l"MQEUf3Lߨ 8|2%)Ɋ TPȑ:֪ƗI~-FG7[f#ȝ+x/~]gi6Ɍ;OwuDp ?12tw4u8}8$2||{8eo d0ۿc/(o\nGv@m@к}k yEYyZIEiG.Q+Ļet^7!JʪWĈsv,J C8ŅcpGX-t,)GJO@K8# /lkyf#ڷ*Am/pn+6,eqaeR/_MێTW[֘_W[_Tٟ_ԛՙW:C=CqZ^M[ ;`@ xkMV`ږMoyQgzՇAW˧#o{] TF PTy&8>CO6^^C+̲ګoj_-On``ze 4@sloo}inV\yrqlfd={o3gso>t {$UH8$aб8t<&i) 󴇗voߓX4VZ<#D]bnKD]f)ᎈtGB\Pvf~N#S #R\u.yZ)Ei0aђdH%E$$8B%( Hytx` 6I FE SE呡⣱~$c"V.LOַ,Mbv.ݷ2Rscs}UEMVBvk t׿k2ՇHh@P+zj'go5۽Ӳ{4t5ntP8=S 1zst/%p羃5з VAh\A ئ"@GCVݝƫ[ 3[ q0 5o;_3pE"!`D ȾP+.cLlx!C%bp dR&DΈP"m!.s%IdԻr5,yzN&4[gХ Y&S͂0  m;D[ Z/Z7Ы :PRj^*_ؕ+_ʫ^ kMI8O΢Ǧ" RQg9)IQ8lt"2"sSJ)cu@߳T4+6bB 339X#!']L]:AQ =rHxKv 0J*$.4v!5O DFJK%d 1&C.)C9VBF1XZ"*+ׁ $3y!65}(־]@V]\e_]Ο6wf 7mti97}~xOo R# ӥfZ3ZsPti/cBP 6L͡5eo]]/ȧs~.CY̓z6.\32.軹 ?X=4_@W"E"5,j ys$PF&*6U T,_ جxQusF͚P`o;v';AjMDѮh1?BFpXāCbB4KF`q81,9O'(825)E%h ΪmKUx.lF^(qw8,cLz!'VtV<KYt4-bB DZ, !A RYRéK9,CTץ5;7[+*e ;@z2JFw%㦟@ag yG;Hq3%1'K>C|3@4As 0L,uZ}1\Ҧ{r{b6eE*xE0#R 5!4J5K3[+5`4%? >9GKSLH)2VA?oWFш\J2cchQ8=v]Rr[Q$pGL|ZR]|=F' z=gCx#rXZ5-z+*VeAbeӺT`Y[b[v zEeh5sho٪j7Wwvu. -ɯO/ Zꗠb Xv_x_n4ѫ=[+_xhjA& r|7gm}~xcz H { :[1'ڑ}͏I%YL'`g 8:.G1D19L>Fj\L(ah Lq'_ZU _sJK{恌KLg.av\kŻ*X'^>NIG/!38'4cjr+ 1x &9IHࢹH69/>{aC#=lVTJx΢*u;-WFI_I8o`2PYʗݾUһZiWZuU^/uޟw4&-;RWdM6:`i(|WMdvd⺢cU]~C{:N,w h{z=h黼ἶY T}Otpe- 7?(8@eւ pu?بk{kַΧxe|!`?vkw掃H{DD9̃ ya{`6XWdwGRe'53#oepf^bzj^=WqBXW:O'4ZUlB.+Hb h/dm&"yL+ "˦^Ŋ⶛S뺁!QG8X{n*v>mZx[S_h]Y悕7s[rP&|/AF`kzs/,[V=&߀i0ph:޻z6jz׫V+ۖ`g?wf{oƎ;dڗ~@L8 OpR\A_ w/bTXTfq#(.X5FÒ!!AEh8) R^Q-l/-g(kh 8K]`KmvFё FKw ,| =՞[+5yRiH_ !-*b!)Q emdvr|INآⷚMv^t[JKvY;BԤrkvJMxGl¶hܫ}tnҪ)RE%_K ՙIM:IR( \ I^jdm)|Կ֫Oӯ^uvFf#nᒷ㇭iyz44Z6۫? ^_7&( {rYJ_iMPu_67[i/AQ]#`ԱuGOKעV]<#,wH9{~;|)%:LL{4=?Eaha26Tz8vC?Eh xr< 'eQcXIT=GLSNiUԂ>J XJ&&tsO cnS|_ӊ癹L0.st:M9II$a!&_&%<} UHcyq8rٴ4^eQ 3ELBr謴t7V-\Ia*4夿^$!1@EZr\חv;^[^:o,%:ʍ?*2谡rr?N>ruɺ]Bu2(Gz Jn /U[(mIJ|]CU'Ko?̃{`ʦsu󣭁7yZ)&D÷Fր@- dAؔ V5O+Cc|f TM ptk립yڸ\Ͽ ?aH{ ubgnw&Y؃u7/诘=%9ő(dD(J@' a%84#sxNK'T1錓(Ũ{آ_P2g 2C9;F{ K, s^0-"4 0TJ0 V1.gx]m** nH*UGGh&;fY!c I D4SRxa3IE#()㴔4Td\,58dy.+N-qhveq\[G`_,jUE S7(J= yg)g'"# Ą #UFE PP4+$8US$̃8kK<#R\\C=]V.daWP TCLҟ$q= zIw>Z߰xQpm@je7- fˆ48%Wңyk\QyBKE䒺tGϔU{y%P9`5^x NYex0j+.WTybS2KP7hݼ=fPտ"w_{ssɤgqv93kdƧ-PR`N-7_6'km~ 18 ziJiFr>!1I) 3;.EY0%//z昧 []lEL!J_JHo޳KHC d].BFnW"56l1 I% h؆`ԚQ]-Q *&voaXhnpGadFΡ@1cL0>x:vs9S%Ɔ01MaO<.58>oS5301dsd+)0.V250BND'\+Й PꂩG a%WMוJbi0Q,.p=0U~:u?{YO9{Pѳl*73jUCӕڞNzq =:`N74>տm,ʥƏUU{zOw۽k6Z>ohy{yh7Դs5tg(KdQu_ 8"&e}1W]:)kvUYg֎]yw۱7!*rAIx*#,!B)x.~4.oų<ؤ 5N;U6!((>#F GҎpbHzqQ1LvZ1>*."3Nī\77ݗb<-*2e:`8}v>d,qۘᆞT` QTHdSHrR)G+Utr8hRfJs9ٕe6'qf˺zxK:g[\ ~"BB<;D'1gD8hւȳxtX,!.OEHΤ"Of iɄKٔS EigK2SBKڢ^T7pTb<9.>#H= cƝz:H6X +1u>C UI]URk-UߛkV|/ so<-о j:I0/4Z_tW\k[m,/ީ[9`}jõKu|`>}B]]P ilXvX5, ͭh|>^BU ޕ[/$PN(;_/7ցg'wOr$߇rP?5Y7P6(o''ws 2I0zZε'u:I%*_ʥO$'H45 N+$Ra|M>]M)N He̐dPˊئD~uٚo֘S(q\Ǖ J FKU-Bh,DuL^wzyeNJ3X/p7juC.5p˪19~T i+ F^Fy؈XDNX!/GMDFj ? >O&$$㎑b!ADِ y4Qj8Gz<ɜҬ~Ns*a$H-tc2~Dwl-+S^ ⱈS*2K vGr΁Rr#cr+?uQL)Pf jr&:fWoN ;lZ6m`3(@nq=2#픡uSJAOS|C#OsXh~7]T+lv/44o+5FgV3F CqfFFj-{yCQ <|1Aq$VfBWi0-*`rpj^h&X|>f[i{"݋,  Xs˖GW쁧j]Yܛy oמ;w&",1I w& N#>wOyq0/9=TZΡb1:eA*Z|"`01XhBEb" ܓBHEp| MKd)(GV"ka|X 3+1bNIṗ42C&|%5""8$wi# bY#TX2[Mֳۥ:.`T *u3Ǩ@܌2cbÐM)fC,@grJSUj܂{'s0)PN>?UuqF~Id4 % +tPMB&hLV[uWuWκ AD@ No9sLw<0p,.b%y BUN Imʄ5EY o;2K%,neÅ۱n @i6T8^rZZs*.A㊏w! :B6RYEjDvl~0Lٻ(PDSjKVۀ7xeaբ^}(xF]z{Y܊{A[0V2:m(pGYMYCMG⚱a(wVK$#v~/ę*/diQ/#u-RNMŜ's3XD/Og wipIJ#NB6ʀ3LÞBQ#a. ^6d,nO_ܫ>Joץ?G\ I45xo\w! %͌[iS7m˨hdN]k6ثQì* zS{{TB#mf%}_6 ׂB%-ȁ&#k`ܽm7_\Kvm|з(N˳}JG2 IJXahg{ћ!:h-ˆ oϙk^lD88!O.x4qui adȯ !S!?4 )wؖ8a+ Wc!u.Ym;.sėGh=B뙇%ˆہͽdk07Բ67TN/I[S܁5,D=m[#+ōI+׈!I V@$ t&k:X@I͕0&”5q!R<1ˆP9R ~@.| GdaOrO[b̮c䟶V*Ƃ[ש~ zu~d>X zVlsQihJLR%oƍ`DIhtRƉ/WL~yc#DOp0[Y 8,VwvZL 1@}7>G+u(7LDg]}GJ[*YXy͌Luy lXIQ]ҩty10+cα1ygDυʄknKp]Ni?gA^8bh^,`!>Ӊc[r^'}+ZVfdmbiB*6Ñ蛉o`dˑ'%ɐUbt.5%s0~a)*M>ӉT[.Y.]xWʌF;b8w""sߍ?-}3.zlݢbs&=;9Ǿmz%SQmne&8 SNeTr 8uȖQoŖ~w?dk''< g71/y^o n,gu?0VNF5k61VmAYIy+"]Wl$NRj}:mupH V]SC)kfp u#I qQؤ6u I[e,MTGdD[i0ZvL@ 4VNe7_ ɫђDzX}e~,+rkʆr,+ilkS^r&GBIhfUʍJ%c9\TUHåGd\9oZRBJLקY8$ySAKAKH!Qj|#m~ށ zi5(XB v]܂b\ Ddg3N8SjӉ`GM$H7EJv ău/#N ( ,ܘ4,g 9+k&sB CnF=8@JSvBOQ#%xH*nI^Gv!]R Jl%J}ӥ_FȻOYYO=WΥ2ѯ HJQ`xh_|-RsЧ-Eé3+4_9. xPI~PHO>3W^B ۸ǞUrTRᥗF-~wrwD8R,P4U_  )GNCXFs([,z;kƪջl!nJYKuSwt3QwB JWo64z8 ^# kf :Aj [u6*ĭq_HQfJ>xu;]F՝\e ;D6++=v!dX3]lH}eCOt,*ˊ$2p=@#4GO7ӏLSݘ!9b &a%~E-1 zT HD+72yYZn q:Dze5mVtW jd5' 'n1:gw\AjW55~2=ΑOhCziˆ5(಑ގ=t8c7Sلx >T뺔9F̃R :/(-L|ŋ \%Gz0=Zzp#<>oR?S z]qq 4$;)e:Ezk,M~$S8TOOފ8nfAVKrzɸt`̷bE/E#iVh%?Зc}U/*ɤaX;7dRBPV |#/枕{e"og2'+G]ϊizD 1@{bn%}#z?u>]5{wS6&lӻ~!`ŭYM]j|twٴSf|g݋bRne]w'_: NmZfE%iI[DH,TlE3UJtxpx ԁlC,6A:$˩b$7J@ fdžWH(e9S_ҏ|ݲrOɳF=ʼT43O琉ܐkh]t^dԲ7,% yEEoTЬw!l\r{|}+!ӥr&@FPjZquwGD:S #QmfSumV'0oUjlRvܤjc# 6b!.y'VMʔY"Nnr@UEJEV>.˧/K!-4L[G {Qw}H8g""[G彬 LO;#q|>֧qCaI4%A2DXTQ(%Ff? =kR )em/'lz[f!?aÎg>(o_ܪA]w6HJ3;ǖCύ }2i۹{QKZhI㙸vqՎ,) 1 TH=lc$#nDJ$īǾ(~cO3hNܼ}+>~ ^:JˏؘU~ϣ{,dW/ *Xnsv0kYZuYjm}PqoρTc;Dy*6@ަj!Z1\Z.4?]-e6vݬ4T@#:fnrP!;]bnjTr] !I؈o62FNYd﫲_~ohAy\~\~\Z~M-Z VD@7S棚b86lLͳ&~Ʈ  G 뙌휨7 "v(_#aaq׵y&Y8  hoKzWW*Fߊ5@۲Y٣mǨ ҈qtƎ@1dRbB:A04+[CKJsJ1Px~] wV9!7E]iջ #G)d=J+s&VL@6StII 72nA!2lP[JTt{|ܱk}r04\>;I>4nYzGlȌ8evV-~_ϔ3ミp_LnKZBLOH0"c/.a.\Zⳓ}z֫' c=n^w15\Ұ͔ݝ6N fWڝu뷱6meB\f%Uwm׻ku&ҪH{}I`s-k{+y{yWmWCMaNdb6+b7km#Ri#_Tts[ڵ( W"d-cr R0QDd/3U*dvJQhE;o,;>͖9?2 ݺ÷ߗ?ߖ}]p g bP7żH,\2>~+AS8r8d8sQSԝ2fh B%ݟiDWS~Kw&m=5? YDs$Kz<-0*pB٬ 2 #F!oBcl`n `pL9FFW $TKguFn^*Ӿ)ߨg*<*jQI /EOA4-!/ }}f]s-q(OK +aQ(82QQFQ7d~[-0Sz=<(.0l:dr <tp"1&EV:F/#b\4_I-\-dˇ05Cr/z~ϙ@l*dB򥩁K!@5vоӏN_̹/n {V%gN >G_)=Wy?vE(xogZwV\6 6Bͮn d̍{Hkv6W׋׵~fl]5fнJݙ:ٛhy;{ػ:[[.ͩe1kLn#(nK :Dڄ-UAqX&f0BM](jq^@WFZ)%V "lN*';8D$)YXvZ[?_y\.{7Cd|`-MVλUq*_W!^*j1j:6pzm>qb+aL<;7@CijRZMe |Ng5Q$ kdo贱u}-Oal&ڱq=*> e &;̥[LJWI,d`qx6%6 fި0g'ne'fኴ_,$8y?'1x6 /ry-mҔaؐV D@7/%1%VFl7:vs"x=</c~,s BƠ5єsiI)&(zt~C7+ "|cjW'}9QN cb}g>_G_LG_{N9ł|@ޯ͊ t영 ^ leSlv=pv/3? 2`367[iT>淛a.|GwQ`VqVj`hަM]ZD{GU__K֦mȫV-lY1):jIȮe1;$i[9ve tATiJ#IUhPr#QCzA4xlW, N+ LkxwD_o_6.#EdQBG6v Q3!㡨$*sy賂hC|#Z[=w[ZXIZN_ZOUyɉەvBncVa&c2#i= N"k(F&eyX:'u1UcghX: dt Dmr3ธA4S܉pzQr9Qe'wNJCqsGϥWNf qetN[eS3 i%0pBC'0(ɦ&K T!vqnI٘f' ڙ0=a]gZܯu߆>~|?W߽w Ͼ?/s^[Ѕ}w잗*u)'hA;/KZY#3F禜79O石埳,>'ˆU,WSK@u\.eC@:w>`ɆM* M-5 .׳)1pvԦn=@x׼ͻ]q= }8Q){[y,~y;_+MFm3^^e5"fgax if*F-kpJ 15F4h&ǩBZ.-my⻣܍ҌJ:-__^{ʷwz\!DzL 4qQ)J6ŰK32,tm!Jz05` ; +ax`f|:/fͧs"עu265oMJE$$7P.ycV/KP ~hvpK0J1~Ibڿ7ƇIS4kBGglF=JgKK}j,eJ31R@0id=lLFb$%s1h(Gl H X }!<2g/.9o4Cc)8qU NQwXC0ʻq٠_,6p7?Osc w?sn?;t3+.x?z"x'MW zz)GD:E٨{x,3d XF;=7Ѝ];+o IOCNBRۮBw?vϺ]HY]#`ٌk]sf|fIEF[v%57ԵoHڵ;ϒO.e3ֻKݍnjz=D^kBFVqآ)j :C&=%%됎]ʅJ(!DTxʊT` JPfHjɖy W%A҃\Ae_;N[eUE朂s£8V ]G kNan0r>h0oE'bs);bш뜌k aK[׍)1T\m&=#+Ȼ@G#(X6zMO@b-rJ @aΠTikyUs*֘?,e.J9Pt:!/S!NG3QGqQq[ hqI`RjTJ6y!6%$3b('4uqg(ؒyF:E†"[$= 1'/<2P&*}B,! 09LxwJ|zi }f|Խ6yqTg.ߧ[6mNv)I8C ݼQ>#o%N0`6~HNty>yKy'߻Yr(ϛס`T.&Ɲ@7٣Λy?Da;Eu=[y 5ToeՏ viU?@hLft۪VV,Q6ch;kMs[U;*cs$;f׬UX"EXb82Bɓb$jzi&MR(Iz ͪx}H2  &ɠTtOZ#튅޼ 1 |.a,N?s' y*K.be]f6a=q /[BD>uè]tFK ~ӐxIZ{&u ChH:aa,s.Svx7բ%$v# Y)U|V:R:!l~D:)"(7CGiH̤9"BR7 *xZ]xK:Ѽ^. r#Q塐l! 'ʅPGRd˜N)#q²DQi `6L ~'f`>Tʹ.͔^w <]VF~ФXYnS/;<^z*@⏎zM~;|.e>DWgR9Qtk*a/zARуH@Nf{7y5BEKfL)n[uNACe4Cv($(Y1f-a/k JN.%4$HP`X3J+QRbx  QP Fnʊ_)n=W6{σ5m@qѬ\@adT3*Z ▬ٴrt֦\T@w̭^^Q0 mmW R"0?'RQ2JnT4 *5\|+%).{CiRY!Zs F->->f#)0@RGlc>SSISGUK'g9:IE>ftJ53n`6ZKc!xJ=L}AfUdl lm`o 䓺>YGy !ΧRxQq;JEHA.*lr pD>j C/{ 4474h;WA41⢚ Q^x:[<׺w;rQXgu.Xf K|kr:-E#!5Ht>62XCLo!cnLB9Wɴ(8g_t*YΥ<ۍ"لa yMŐn t+D5#AxԍQmhTć@+gu3B2mOҝL(<<DgYIM+/cjUٛihrZ,hljlzEaOTmb! %lT6DAX]_JaXO59ʇWtLsXiY:ѯИ8n(U(u95n4 KYLb!g,V81g%e85i+Ƶ~sa2O&HϡCAx%=2\ EGDIk/WKЍWF>yaw<'o:~lqdzz%8 FҢLV1\tjDlݔk60v!lI^<.LYGRȤ  X JA@D0AQ-Mjk(vUxJɞ㋯ ė؂# <3'IԎOo2 ZYhv@b`LLc9xavj};گ0QS A)ÅR=;;[x(i]ZθM'RV9Tri8_O|o=B,E6 meE]yn츑էilț<{lo|dX6.k§2J߉߿do 7@0miZ1ӛC(ŋܓerulBV^$Մ!ǛiVksDmIc\M@ovARRc-AJs;C%vTȑaήщ,j@4TVU=H5(!2zLbY֫6A' SZVmTBRJXB(I!TcZb$i}V4+&#C5ĹԄT L]qZ(i~ݩStWcuvΨ'˅E^ޠ[~U6Jy$.jb_cӮ)Q3 {պj+vnBE&۶ʪǑq!]+GʞlUFUdh=2(= 不Mʓ8H0P5/Kewx<p˙{8iZg֩ewAb+;1LLD?v~!(gihR4  1A++@Wc#7(^,LX!g|J1Sԣxb&ATN=W'CIxay{~!zc)sXSoh ̟?ks^?G/Ή/~|s߾9}ᕓ#|Q9dC2߹IwG96D釕GV>GW_}˗h2DĤzp mD+w1tK.-]}ycv_g3屾T }=-̈́nc$KBӚ3Τ(I!('kWnc#J\ isrAsQWQfDj{zBZa[z8bȋl 4vAMAI!9q'ad%`F) r1SRH I>+zrU2FU3tRw@LRFa z?u2  F5H;Z<"UnG]:Q;(hNz9Q xHVJ۱~{ -fkZ 4ܺ8 t 9ݤ&""FW@M* ,c(Kq!D&?Ry|AA%nVĴ9&Iұ;>y4HIG4G1ʔ aI`;an_V 0JAɩL:ӱ!dTTDVmЬ@XӡZIkm^mTL0n-씝[6^`E 3ir\֜$:tH{W%tWpw*(aE@v-s1SGم3,hNнbx@Q .9SN& 5wܪհ6%,i ڪQ#B6S B sCzոȐ|?|Au sͻ<jI5FrJQRp.~AʝcQvTqLP,VatJqW/NY4L\u]-(_KN+|qcIĚe&(Zj,3^U3# EgrKƧCG]E`.]N xK̖߸H Ѹx+Br8/Gq3b.~3^FlSzOKWXIZ39`=d,Exℂ6+$q6"@yT¿ۡuJzӦ]UT➪KQZ( T^uMMp'mGn^؏zjծ  LNYni# A&NxG6Uw^Sv>z]UXtU=۞`)qf0뻷]O[}JbV-r}F6Θ>+KH3|0 0礔)9i q 7Ϟ|꾧~P퓻 }{ۂwzaխpN-=v^"+gsr>QơY3(V&ARg*LKn^; .Y̡z3Q v4n3D6>vÂ.՘|@4S 7A=)bm$E%ݕ53&GLדih=}L}jqmBXO]QY=#& INa[O=y/63LѢ 3lJ@n]݃,몵yloϫ?ɪ;nUo̯U 鮶^i;^`I9%>+g Z7\!Eo$TPY i:NX'lxM?ā}OEeuJɣ|#U=FS&UMѼPKm#+$~vG8(vsdzM[TƦ=YݾuOwv462jL9- ƟKzBaGv9^%˥AfI^1I&0K ¬NS2AN,OTϋdϊHdψEUr479=zz 8Dk`+Z707W#@)k`|!8"Ϛċx:,,&MC`6zQ v!"ETXLjI65bVF#YA69.Q/䘌 hi& ͨ"AAG1#TR !􌜑;j NɒOR1j"5P#_Cj dDEGWUz꧿+WL[gV f~΍Ā!|lx>CSroMc۞܄׹ou5CP}A^HڄPv?ɪlIMCUUqJLG^)?YՖ=G~hzުvYF z")# e&^%SZ8c%KZIPv9$jig_Gz`ÿRԯ]4IHv8C@/z%핌efAzڧs$AAWH֛"'X$ei>jU*Ŷ'[5"b}zjҖQ٣šS8@J#ma?P{hUW$ri2x7ݷz_zg~UUjvԯk^ﱟ>SM<7!H03A0 2 & ͜dsKݭԲZd%[,eɲeI0gUSdzν Jnh~שW=g{kl[>aLCNt98;rN?UcNFz /Mu8Tם޻ǻu}A?:nw _Z7߽3_9ۗ>,uVE-$l7tٕOyt upg7/ y̶έ3=[Nn=|isH2ɾo纕2T7y.k@Vl7AZ`}zb,WOg{Է7޾?c;fS #w߳atz噁׏ltg命;8`\x6{sB _MBSHvƹ"qwxpփhq'zqڱy«XY&zR3n<ߐ|_H4j#DWI,Ζʾ։wZ~vG:N?stg~lK`xϭ]ut}lp#??Lƾ=o Ѓn{e_zz)?]db!ꖩc/x{>={WN~ٱ/^ʅ1| N?׿}p]{'Oo`s{ΞX8SW/|ȥ mϜ=3X}i>up[/\sokÇ?oCVuTT[n-%RӇ^l?7{ubE$$$Wת}Te])'qV-ӵlg?0ٛy3C-Sw;%Xm??rwˍo3jyqg8oF<(GK$`Dի߶N̩HRZ$ZY[WLLgHODっ&z^xnr;oL "0g]`h݂M]nIEƛDI7_+>paw>wgokw\];s⣷fu|-SP=MgO4Ov7M` 9wdS-gr❮kz\>|s΋㭗Z/5{|؞Afw]qypσCD:~Cm?uuw~X _?h`} 񤑕*R +Z|i BCңGN{yh3g&ҋⱣ+uϴ.ܺiCe6>پpS@q+dse%Fڴw-dg9Eujv7no[Uy+DKQmtD`8ec겂OTM)ɍ%M-]]hk;Fuw~kO{>vHV[涴*.p )ZX_acucCymX`ӱf۳Q׊Q׆FݾdM/wnyw͋v$),_!JdU"eQ= tH1Dռ6%S$kb'Zy[_z߿X3CEO阒'/C:'kDs4q^LcW2rч@czyr;gF:xiz=m3v{gƦS_???}{#/gxƾv^|3Hn%.nFXٱk={'|Ό䏯=˧_r;֟:=B3/l;?sipx˥{o=pmp+w_9zvՉçN:p{ԑGw߄9zg}M&|LN~oώp:-/8/9ς6'QWv4]V#r $DץٱW;_kDNzT\ڷpk+,K8[W;r­1,!LfEқKjWk$wtSkGdQHF#99K[:m]ݴhZK$B+$k|TRV r?[:ҷv<)#B%Wq7gG?^O,\'c- rpѰ0E!$A\V/B%RPd211*q]. zf@DO;/ub;WН18 :7@DH+~LX1L{IĬdLF#n~&7qTGaĔuDbyݼr;3uB@X05%&wM 9;{DgƊ x1ͲXȮ=Gz0x녓}|uzNzjH/zmzzoxw賧Щ9} ~̙xؗ?=:`|3?S=_23e9.#)6'T:W/n]<U;gӼrW+'Y{=SûZw>sM3N_zb国.sjnyKnq'V>p- Gp띫'6{sϞw}ҹU7>rgZ.u nRI++sԖm{f ?Թ땩{8WmY~t]jEuN=S]m  >VVFYŽvVjrV|]ۗK5b۬g`eSuޕW-N9F*լULxg VXVO7U~k޲CuwV,/WEN9a8BXbp/vJc(V2VQUlI؊]M7>|H͋1qDqxW/w֕n8p`" sOq%$2'(@2,"r*!32"䉾l-Gt|Qz&Hrн19a{5嶠$Gw?zxǣўΌqzrzo]~yq<{싣c}[8>㱉ߛ}wz3_ywn̙/?o߿8g&zye{w3s4Ĉ;lH1%kn~?A"=7v}{>:?;y|رjgCOq]gmuz3wOr >8Ƀ.m[qX*~p~ڎM zsمgȚ.[ۛ.:ɫƮ1D3}9 txRNo[xʗ.=OQUݻTKzm^0D[_答"'Y1ې̔Kں\Af0\! z`쩬T|](cDNQ!K:ǛؠQ8{~*CE9'%)|Q,0r )"$KX@$ ATA9NԪy ΊmAsWn[S#E9c .[-(K]Ax,MРrx "HDu]ٶǔ,M0^jVu+;b"X uy>e_`*[[Gm'xvhK}GxyݩoGCwFMڧzc7G>00?ɿ>}|<|[|fOp_?;Wkr׆:3\rr.o2U.URe2ۧƎ}WoMsvS^W_;j]wqK3]/Mӹep3}ϟl˽otn2ϝl;[*6ĉUe<5.[\$k/|cXB,%T :hᕝkWFEd ~1("3_g?u,2:Rx$k1’,FcծSietz,%XbC:sr=g5I M$( )9aJ]Mo[TUw*"&TI9vRU*b#&2"+CX*)^DՒnqIҦ-O8r2DT!(8'?V.GHB`D"#Pa%YbuA+޻%2HDPWHHxA%DWI ^n+ƖڕJVn,X_"r~55ncQcI!+7?3C=Ό~p1ƥM65kscwο}xxCx黿x]:wh?~̟N?~ݻ}_{o>󓁣_7;ѱnڭ7?J* gEY —ƌ"; ?hscP{^9qOWoye'v|yǖ{^:k'=l|#z\Qk={6tVc:Y.Sd1n׾tҴDm%ji 7U덈k*ѵjN߻*w}}va dO4VD%F8bpEH4M4e;\$11㖘R4 `D#`b2$Epx FbdEY_`4"[6vivC=BXFc.cjغ 80ii^0P D ?yS-!(44Al^)*QlՌ[hssuڦҥYbf"Gc#7ݪί($$q6.3={N41WsL殆uNǖ-ɪƚC;_q^cn|꿜=w?9u.߾g秾wbGc_ٺ 7-Ep -w"$0ڥ;NuLS[j<~_V5ѸqSd'ZAGOzjWOzeg:9/?p5EWf$ -;anN*Vk(_ +wtV͐Mu Ӱv͋^s?M^uĦkf,zy3*.o^|~m[F[W_> 1IjF p#I^8e4(8/ U(0b]+הbYe4=[͈lb#,&u-(%;1 *IeYA  22 R8JH>"d"t 0`NKVVS_ZcgZW_J("bequVRDCtSVҎCz ,\-V ERY:CFfh@ᯠνKחX{qK[ZLwrsZ_yt=3ύ}m?^;Jb)ѺhCWr"id­^Kx⸖Sh'`ՉxZj)6X[vn >s?<}n0ΉK~ƃ#vmzrS5 {jkT,xo-۳z.:lL2B- OL$--ց*.QG%. LjQ#nYY3$*dHȍB߫6L-+iW(IblKdSʓ,dt%{FMH JD &Ma bkĀPJHK̓<1`'$ f% IK|.ī,э_ qO$~l/ =3uƹ'n-m$ E V9Ln#L/5#A3_>zMыz_SzgONL ajߜ8+mee^Kms$!3iQΈ.*]S,O FqW˳G>#u8G~cӖG^=xo`m7'vrm#;A_t;w˅ύw\/X^ eD+rB/SQ';^><+wMQ$ZqCLCr<ámVrMٶg3>**߯O־tw<&~ԷK/߱ocU\%oX|{ku7|tb Bہd1M8P)zl("Z`0T# Oe$^m_1  rB׃o\$Gіd1mh ,j@lA`1չ+B08 p„$lOe UXőhet !2w@«2`w!<ѡDCL>ŨVe]bkS9 SE$QRF=+mh[cʩ]myLE%_cH#!yqj]*Er[g>{ ++/{ Dϗf|aG7\Xvdё3Vj+$O$92Ѵ( K+]{xߥ]lk{_ڵȡ'\F]gQh}>߾AGc_Xt->1;Mܾ}]O#[{UynQxѣqNM j˕Ը/\Sg?.?GHk;nT!"$eM&oh3|Ԧ<)ϷOr*r/j=49{"ͫ\_Wm,^X&;Yo)9\ݰq[oSOJ ! D7T,fn vuZbf!1(dKSx#̠@K]Q&b7-(WbHVՖes nD4S XI[R3S4 ψt@$|#sFUF,DJCD cIz~M4AbDlLV*va*`]*+$jp^n#SL.LfsU#Ou+y{U<OLHm>=:dMǶKM=jnؓQx1BJf:KIbZ1}6p B2Vt^T>qC/[/eo-l'>uħ{iaE,).ucM3^w|}fS'V8Y 4LU W:q;'?:]Pu2:/[\LD47/ްdIjV"%6-Kو%R\i՜Ҫe |T+_,Wi^&śY/fBS4c00Sd$gC8aQdhVduI*ػ%9 3@@ ; '̐7HRs@MDAPqF8o`_!,We46\?EMR^## P1QI1 IU99 QVs bDNLas=^)F3T Qs%iB{M2э/u".d P&*q*3TL gWȪӶ`O]Ey9$1,)4ɢoiY͊KFLͤd$9PDoʖE}eޝ;fcW-T-5Yu.ڷfwGch5{1IdUڑW%hudKu$ @4,J<>wlx}QU W9uë:W}qXȔL6|BAZb u"sqOW(/-/iL>|R6\M^Ñ\XUvtR[DsqY!&ۨr-6,&9I#rX_ sXrt7-{1ь `QSxTlUٜV,+G9h* HIcA!3k1'ߤ31F{D/X`C3u): =;Mbɂgz]Aiq4@ k&:MR[')^Ej"Jʭͫ~P T# pDrT m" 7iNi)^1 C :%i,Yd-~-di"9$CqГ*MR|y" v>%˭hbT^ 8UNI" WH//00( .$vs"!+O[15B7"&<J ^ a,ڦ2 3W2~ <k;? 0^QBٿ [ܙ tdөt4g31HƎ!h3a]*<"& \Jt@,aȀϸtf,DPbRE"xDLjae#61<WxD3f^4R6&a NWc zהVo(.X;^gHuVckn d2X_TPkuUk;% I/k8 ͩ&06ᢜ$PXe48d-b  :͎AFi^Ѿz&3$ (ZOE0ȩχ('"^ϢAƘSZO4P*O9 e I]:OdJ謁\AJ/8$5F ʂjiK@x%\O8:i<\}H"YmHg$\6 &M6ð"QhM jT Ձ̃T,Pt _`3q,ʹJ")ey9ټb͎Աe zPa` BK`.0L+~e,ZbY@X W9-LjR\EU`MjBRޜ-^6޹ xbs.:a@\u9f-"5ॴΰ 3 'D/SRU-QZ7W!r{8y5[G+bR7<D l6yv D Jís#Ax Tٶ嫮h6WV#xD.Y]H8͍]x{hF(gxhNђDJ&ዄ Ry+Ì hꞶ%,I.L D.\@D%kڰ]DðM `81^  pk/t<F\HQ R:0`A{J\_ J6PRk@[hJSRZO \!yp5ZTJGw`08Xc֊dLϥv` s֕Jl,֥T\7aB A$5dL#ާ/K&&i{_ 31݅\dfYĔDzrt3p 9(;ֈjB]^qXR~>wEJxv- "(ZTA$ԧH4mStAaZĄH.Gs,i9\'PTZ^M?Ha-s~Nt+A+n#f=hJǓYQtUe ~54w!]&" 3V# D "RjoW[nβ굉f7w_@LP*@`A FWLfièE4rl2tTHZ(@Bw7@被@ZG4DG 6Bmc8z{Wp 4}f9BzR~ O:P&#LpA<^t; ,f$_jYȥQ,ċ0ch1HCiRF\Z-AW)c6$8(3 ~BkmA:4A NEP kQ&B"F x{-؊( /E7͢@*Bm \"=t􆝐a@0%WE3b-ٴO'DXƑKPvT[]ŋ8^ȆDrWN~gqUWQUs$V4fD9%".C)$SLӹHZs%!9:2gh'Poj>KSsD5ZH(T ,:nЖo1W .H-͠{p%SD-JSd4Z{'Ȑ6"!v {22t.Gs܊(O#Ho+ssxN Aq*YtȜC2ΒLpb42MRD ΂:Ky-ܝ:' 1n9 %&"55_ $L<0(Pka^,OZ$IHPkHm uXw!dT}jhJbN`3*VD$=8jrء T//ȓWdGqn{cG8&8 a(cZTE:/ύI&`tAt h+AN/_AF5҃0Hvб3O4ѡ  \zLFۥ{`ayH Ke 0*ai\j7( ă8O7H$ڶQL@b3GV,,a4/w gvɤ=\>6a*>4u=m`İ L̇HG~~Ma?OA(CrX1A+C EZ@Uy5=?5 EtXy*Eה9y e,9  Н}$WѠg3tce+r:%LOs ZN<!N?r,\Ә'HJD7r:tX V|+qc|W=,զ{|B܃~]|2 nez~ăMU rb <68Fg4,S )h?K<K6H;R @+K 4RZLR4i֨i 2ۥx'/Cс_@@3סYygT>!HT*xiaZgx`9y||ʎ̘Jmަ{ A^b4ϘLGpXP԰7ɴyOj| ^?'aB >hᒼMU'*֗9C 4X !:mu-7Dhmi_:-6łPp9AsrSd"ܹZ}ԜOr5"6Fu7 LE"@GZP`"*Õ1>@{ ao^B0U$\fc!Vjh_ 71Kn s{K@8e/$=,>0; 75n1aK'9yA`TkGWB8%`[ar 6T/UQfaH#k!1&lOB9W|hbIXʰLF JX)Œ2]ҥ ,_e-5$V]أ xR*~ဧїZZ")TX1$ƋڃQL֛ԸnG%KMP`۪dY5$Mb2,Ӕt(HQB#l -A~6dhW!x:?1<eb "6+Ժ4G(Vؑ)tx6-q칺Fg{:E1Γ\%V%hV8;A =Je$(#/ QŐhQ--Y^fGcsr"]aϲ>oaER1bY)'J4!-]T|qbu3Zp$ӽ:۬wzS^R溘 4RƓ>Ehp $y0dJVG欳='%rT+1e'px[< Oh5Xd-:djK)C(faE:Qa婞j="K7P/e~SOhTsMOf BItAS ͡'$<j>grD4$P^_V3d|ǰ[YN̥Xb>LQrk _r!LY|"n@A X\9+& *xgi#AF APy1\fߦu=^#1!!y@;p1)hdT+\*n^aYFx:x-Mȼ1TXAڡF( [۔Rz[++#t׿ GipPj,m &6FŬFk%F21KC YVezt=V\e}OmVtK')?3y1{H+O~D ``$ jOrC`8PM%mETi ҪP腆nEA^Nw $) MnvTPP cF6ԴnX>Ksl><- AqD/VcGDLeay+ o-zh#Hpq-Dm%EC "1֓Ĩ(%8>*qUb`4A QB} ba&][h)#/46ki0FlO B {]iW3݃R$2ӖEERa cك95 ѭO ;"0`09aqs,z"ESx"Mi4OGʐ_@,z\& )V7YpH? 3<e⃾/F8 NQ.80$Q1ر('8[HiHH6$ZmnD%ߴՙ!)M;<@z$ | #aIssjt}Pz`a p-Ӣc-Ega x85&G7"ϰ#ЦiR%uVb'%/n/:i##m)4Dž'А'En `tX/:N3>I͝Jk$a;7H軄Ng'IIIK t@-u1M('iX2 g$jI]aj/_8^yNg@i "zesʰ8UO0#1E (#+ӆ1L%":B kS+7VEU5R@w$F ,՟$l sI"h)2L :Ⲝ(G}~2h=oH/%F3 O~O><9/`$lMT+|Ȣc-]HZ~w ODG 0y=H;] DI,D%\IF$[jyQkDrU[! Lq-4:BH\97TJ,.'HDd Vϡ߹L~88\]{WoÞ%p(Y@l NlIIKrzk=3 E&4P g鮯ꭿ*OGU9bQ)]$Hl ԋp!3@aͭpݜ07$ǷoNK^@:{y_^Rf6OMF.kڒ]@]jܜݓȟ4lHc̬j:O ۄ|v/~ZYB30Yn=VHyp1UF:T$~G T 5r l2N5X9a{/-@ 2A4aodC;s>u.RM!~5yl` Lgd?خ={m%QpgY .޽dZ @ޔ 2):"ֿmke Q(^:v22<#g ~h~LɿOq~+OM/.32&3~~OX<<-"ՃMabE@斳\5e.I8t eMë7ӋhJ,b~_. ! phE)RD1H jҔpVg86s8ˆ#p3o 5RrG. &vU7NM$Qm*ΕzU&b\Z?bDrظU'+MuȜLz ,Död,$skAyL-#{= , 6}f[d طH2@*]Y@8xvx؍\E=zth$gM0\z/SqJ.$J}f̾l`7/vYND#UlP QT8AP,$8B&eҙTW1@mܙ1+(B |)ZY&(mS+"W̭YV`᪘jD 5we@@n̚DuqR;ݚ[Xd, =T3O9z^"hlA|g@:=Sɖ8L*'0\eؕ~\f;ᣵA'dA+ ѹ"p`vu;::6CHjNYq.59Qm35hPC\sL;0B  >!v h vpjW~<ĢJn9D䤯țfWDe_>%(HYD$ғj=csJ')kW"p~\LI"$ AXfB3t& nvMs:e8*nyY{ nD<ΪN-qel5թBIsdPϳ8p?0B@½`E@0i]~2j,R~H@ 9p0w?5"X$6' *=1. $ڜ.CWuX@FbGI^A> }g endstream endobj 46 0 obj <> endobj 47 0 obj <> endobj 48 0 obj <> endobj 49 0 obj <> endobj 51 0 obj <> /W [0 [777.832 0 0 250] 9 [833.0078] 10 18 333.0078 19 28 500 36 [722.168 666.9922 722.168 722.168 666.9922 610.8398 777.832 777.832 389.1602 0 0 666.9922 943.8477 0 777.832 610.8398 0 722.168 556.1523 666.9922 0 722.168 1000 0 722.168] 65 [581.0547 0 0 500 556.1523 443.8477 556.1523 443.8477 333.0078 500 556.1523 277.832 0 556.1523 277.832 833.0078 556.1523 500 556.1523 0 443.8477 389.1602 333.0078 556.1523 500 722.168] 91 178 500] >> endobj 60 0 obj <> stream x`?~^&;ۓf,!PNB &E@@QT SQ QPQbΝ݄_pܹss>s 0BH!͘0{٩EHuc5͏ <6m̼I!M}qc=B͞z軐Ɔ1DU!.ǿwȢ@9Ο0J* 3&N2 7w@=Msܴ13޽"d-GsGbyEpWґPV}Brj85*:YS`Gt9/+ g4%'A~2vg{8둋 gO2خG-Llh[~xG gxp#MӺD$oyl2??/%*_;ˏCo]ǑL&jOP! TL:3B12 pMK )u1PgE4Y)?&&M. s ܈\|^= DDRA0 {:h_|!|_! TԿӸF2{B R1롞ѺHP||:~y /A&1Yyck]c?_ub<^b}[XiLi@x~Q2 @ĉLxGd?X>&y71F/I}[9 aD̶wb[q}nQy".;λȏx'~;H)Ntƪ46RB |1X1'gB/qcqƎ|l$ȅoE6 h2ЄiuӴ+~41J݉y7<.ĭ@7viJt,1uwh"%AAd']):ߐGXhh$3PVi*?"s'r΂<(Pa({;5}Vvѣ(K l<TӒr?ԋ̃&&QE)ٸ%7ShJIx: 8+b0SmpL ć+Qt*$%PhKPJ1Tou;4F5?;hJ=J?=kul(6~;t&F[b5F]?K]N=~Lء1[D.8p9ո/!xã|o7[j|/~ 7 Y##d\RK^&'S2BFH9ǨcdIbRt&c*k9ff yyib^c^g20bUlˮaױa`[m .%uv{{'#ClOj{ڶնͶuöwmGm>}nk`N%p\?nWˍ긅ve5un/w{;;}˝r]av=n;^{^nqHu8Ssjwr;:XXX؆؉84sp.E=q{~x < x^V6-x7~O/ hg؈WC^"Go=#adbL k/ڛko1yyf0o1gXĪ4ZnzAaqymfw-n#+m=nh{ٶe^ۛl؎؎>}bk}a;c;gCs1q;Wn?w;}}ʝڹ39.k&^ֻuC[e#me>Vzw[^ⶬڪ*zh+m+i+n+j+loCklh5dm.{SzS SKN4`^S|r>u*?[ )gOŝj+nwbς\~ZV/iyw;ZΠ:t3z=~Dg9ϡzEh!9xH%C*FE:GdD&dFdE?m@NBnj^P2!?JA( qY6A(TP1*A(z2 TP/Tz>`kе?A AC0Tjp4Duh?x@P#e2 f*`: @ l@ <@M+)߀eN@*!w"|Eב!5? | H&Ry s9@ԄB mG;N tzq+Uڍ^;Q>fɛ0Wނrw`Й>:'>DÌ:>yvw "~c/bW ?U$CHR$;E?~v"G܁8BD@D[$1)y'̳}DA$1&%:'b$&b&b%$GAEC$$d#~B$II'?Ow3 6SOtם,G+4k;m6`c&`6yp NI#om,~wKR$<\x -'.&R\ND6/K!.6 } NM}q0<#H\G;ŷ x"=]N%[-fѠi5jUR!I%bP2 We[z]zk dQdK4bKKJ%C]%^) pނ]Z{S;ǧ H;Xnoʹ+*ˡ]!P4#7c6MWyEUc<c7PSQnu8j)M8&*k١h;2w_BcijȚ&fLmmMFWyqAegn7rLÊJ^zz5N3Ւ%5MxIm|ߣhpUМ&ոbr= i,1MM9d/)L7:@(==(/tn/o濿[Ɨ{ x]r%W5t>o}ͣ dDwlt-d0m YLBn!ˀZR`q-d 0e Y LBn&o![I["`r 0q ,&l!7-d0FZ<`8z*NVw݇Z|$"c!>{_]8oђt2BZ2j،š2[ZHYTxqbLޙRDf ?7goHx*W17WʥEK{xl jk*kCkZE^aΟo s#ȟ}YR')>JR @Xz)B(?ן z<~<>c;4n9eo6l5Wہ\:kA`/5W\ ( 9p+2nB_< 4ydGq=N6OG)4>ByYLB!0f5zC3})jӁ4Wz(TW6U=76Ck~mzH bT5{5{1cFSBVo=\Q=8}k9}U;d` Q74Wӱht_*t#9n O@gwQ4s?Z'dr2v_:Oʵ\U/ўP® x{c#nX5ȖP Cqs,j9z*t5r3Vswr+ |koD=xd[j*Ք+˯pLvKΩ5=P۔A>Mt VYH6e5,S1YP/_ m P 1NC'_l9-uՆ6E**zhM>hj QńRRZ @SP* R8+qYdPd[ Zt'wh#z0ȔȬ m5[h zBe&d:}GA8hC7COP'R̈<F؃2D2&-yD0W|  D׃-P#)~1s8 glcBl"ϑvFßG>xdFXz: }Yhɸ >wp_!d;S"v K0?dErx l~jn$HMoGKUQvzpΠ/f/w؅8rqWCO_k`x߃/*ރ|鹿@kdDup4L"s~6~¶ ,\p}BhGǷ#lodU7~SY000-B8(d3t|7I^+˾v0>)8zPZx\G ؘo x? ߈dm1v?ŧߣ?øu$G_~#-dK>8~bL3g3O1O3o3߀'+GWH z l|-t } Z ɿl\ ۀ^Fd.X[ aT7`-z>H/ 4a&[ 3S[9Q%KPX2m"f fr <ٹvoe` ^ȇ zvt59_R!t Fb[;#$IBH:!?&)ٸw#Y "=^ E2@}LжE%k9V> Eqۋ䚇{*BB'A|Pʢ( KP&2D)k7B9A< ~FB%)W \tUJW*]tUJW*]t/&~ FIP@X X$BY,i!k(FuuQJOT;ԉ@?)hu ^}rJ=cHJ;!´L[Fu")@P,9q5JuJ?==Co!zuo˴Mg0r\2܂]k!)gyS;AU1&8J4{nQDidgy]NP3InNn*b!d"ovVNfAp_21ۡWbpf7BL&s%V4lV?sgg ӆM(rM.LwjtI)s}+ִ,yYNWoM:ga+YT~gچk2ýK.Ɍ22N04A4L-q*讥!F$3EJ -+W0jVk4Z#fcP]Lgh(j1&7ǣ!UFQM2ށOf eUȺ93uf]?Ӹo;e-* X&wZ=C;BҬӟwԌlGE|GTHDJUlhPOfCo;˫*fϞӌÏo9'͹[G> I-df-8s:V(pz֡w ?HFZ9P^3?n~q)S6ŋk5֢28 :Vס:юJKP@PՀ T"=UWՍ=gɲnvssiA!:lSOܰ;u^;gܢ>wc\CsBM?L a)ȍpv!9Yɰtjr"ooX)ZklPƵ);&=؆Cu,/~:܌`d{2:CޱWМ'w(} oH-5dXup ".=u̓U;Mޑ8\j97N~G^x.Ǯ|IƿcCuƴ蘸ߙW Q*z64PLDIɦ99\e$wnk V.{#9@2@z~\o0JnK%Bu*i׆$Ij uUт I/+kp‹y$%eNA4Ⱥu`>)0xs ,X0? dB'ZE$P  u"O ]7z7eqtMURVH q3[?,n*5ezI?b)!wiBEkooډ?0Z^nH;| KRцt8UttFۂPX Ѫ4P&n3\tFBm(H>Tm0(>dB>H ֙yXdxtWq FVC+ zZ1`g~YSq Zlo?{m5f oV.#ooQy1:q?!au Fk i4$lCV*OBn%kWβԢS#!XR* U^ǺHx3v!#%,uçnKfκS3[cy|#C>s]7,_^"Er9 i?u#W iwh%bRcRZ U{.Mt8a./d=n̆׺2h†lLewiR KKR_ $. 3*(#a7)jZ@6_HOcr$j+؃r8T${Qmk?ݍ"yŨ 0F"cܣTܼ~`" ƨd4z=߱/tm=s3gV9 /#k5kBQbʊ['/?,gTm~LV8'-L}G;@T6}'lthr {9&%L[l22MS*5dZ@6Y!X)ß 娈> VU_{^A}?/t 4Tvc(3@+ HZP=z ~\3d:7srԎA|g-]Sg >1mP3W3khÙ9 w{ܧ&|(9#s6YwS^)L>!EۡQ47*n,NxH.a=EݶyP-ߌ/5atU{*-CMMsK ˌ l$ Vg4  фln(d4iA7/HEȔf"&pHXGF[ȄC~UZ`b~~=ێUx6FdjoZg8fDZHdca\bPջ;y.s4&qy#~Ima7ĪKl?k#ڻxKڹ7o>T~k_2!B"bn7_#Oyjf}&s%^MYbF[L1Z22tqww**+a]@$cL~,w v<~c>%oJ'oRe @̵FVK7|sxwJ/9(p"Jj}{[X8,qbD PSdպ[Ȉ@5`dD(fsꠌ8^$NbP(̞du.:^ "qH*98I]!dwYLT4 ptovŪ2IOJ4 b~:kه3_iT斛YYe[z6>PwY^qM,{wݲ:\^x(5dc\hMH:_kHJ+NHp$V*(%du-rB@&" yH[=Z[1+u/",5徭ցx:(n [|T`S2&&"9]~fx9,-ONŤPi H {{7tݴ>QI %  ;BY h H`)Hfd!lrQ=_yP>%Γgu:IV{ I\Anɨݑ6ݱ0LjjzpTA1%*wb@SvGvd,rttAT"5`ש>N%pRih]CX)}ŷ\I%ڿ3,#/7z|ѩ-z*fr%ho79|7A$'K{{hUh iKN_vR+ieIV%[tpq,iӪTHIqM`23)?%t9p]1Ch淏!X12]0uET@XF!lc߾5#FƅamaG0948[u/=K\|ÅfVz:}ه/0Je}hR.%%>[Z*Ib||)RKOꟴ猿%b$İPW u$~zvyEՋ*a( prEJ1ŒᜣF(%ґjUvpb-+Ww~לg,J>5xe~ae)K-raj:DX̻ [sm*'@vGυj4PTA\kjG_r9GYY&I?ZKױGɢ},oʞ*$;~UE>g-U/#=  =So_jid YՇP !|!oY 4OWyNQ8GNQHY j6*)` TVDXO3~KQ>JЉUao.,jdcAI]Mt_JAFSg2:,eE0

RJ?ÍjWF.ʒȵIW fyUw,  61k,"F$8bqЄEN4~0 vd&~_~:,vcw9d]]:A%O .}b|I"ѕ&&$3JیI{]xGҋi/X7L=Dp~1i#N2ɪӳe=z8}tӗd|*^G|-dLHS ; 1!߳ c:TgfvʜiC[4ݰ8Qˆ|=.wn/;W-&IG[%˕~>{9JVUfdC(|/5Ǧb_=gű7bX2'WIdcAie=wUNWkmC\)3) >W =z -4׿il8tn?axnpXb s.% u!`2ahx -R 5.ƚ hw-Gܱf^xf¿W"umI(6-ϰ玆sKH-]Ǿ}=h CYOCI/}3&ׅx~{nlxY}W\?15I';Wd  * Pɼ^G8mV$mh:?VIV0vAO o.E:>6aLWȨtG.r+} rKK>Me<.}ĥ[;|s̲b}@|dJ͡U0z^@CEZ$դIɏt<>7-RT& Mֻ!inRҔ;sM"ggYtV#4S\u/V{0DQ#.5Yx $O@7D4dHN"G|BnHʾ9ÄspΰU>뭃{wo5W64úvQՙߌc  ?3w.b&|pl CToNn6G?I4:6W6xx\pmHrnXhV6JEִuz<VnJPjm7ֺdf5'f7,>BD×ݶe^hqζh[Ri4{ ]`R~iŻD`O,azOb|p&3$;@/ۉ 3Abzfh IIFocO622TbNoMd 5XM\R9.2B.UcBhKJ fpOtD=a"j6.C9TEExϏ7ú|#/;t尹s\nT=3)7M+Ymq8cwA˟ *NoO}tDO,5}O"01sVrEnt٬3]FǗqP!xT˰̩ط>Z~0~cߞUǨ%tLtS;n+QWףnx+aH& |Dc$K aD ͻt\J bאb$sDO71%1*=ʔoCJC`k{m'$SRBTuHd#ΦdJM23MB&i ҤI>^5OhgǢ_YR[~C.s{NLFj;'Nj0kJ=e%;Mؙv 8+ætHRd\Xd9b䱎jN4Z+_UJQ]!qhp(I(c3AI(rx縤6%Y8"}Dcy~6BPUug,N>eL6dž^|1LvcssLvcsAƂ̕Ε+3t<|0Ly:ّLHάQY6199crL8%L6GcRTzS @AlcsxLV%đ diN n&e縊J@NIQN/]O{.M'j)N$,oI+2ly{!C<8I<=<O v,R^(ǞCrp0'-':gC n%u[zTN:\]̹ Dk5Bi) U6Qc,*.Ca&u'>'q13Y^w&Ñw3jXu*G]WuXŵ 4A*$H8.K5/$RyWMtrXcY4UbkFu2T5q5iҮ<Ӊ,MF3Ҍ{)?vϼx_2XgjKҬsԩ[}[;m/dR_4(*I';G uʪ Ϙ`8;t.C$xv4fSFD?l]]z#ߓ~cJZy>H,MG#6rb][ ?Xt-@/ݤv$a WI;5AQDNW펞_GFWoT)iih.ڎ0KŦڧCcTM>y^kKrwy+C+9lhO}C=9 _#>'{ jN?{h˝%_ꍫV4nz؁kmO t8,S==UǪm[Sw h~^VN'=|Ew}ʣvvQu[7= "wA)j7JjETIQym!SlcZ@3`N^,g7]zRD tXeI`Er۬AQחP`p^@_sr/UP*Ƞ>> v'{O;SE$LsYe3{Z͌@ Kx?:+淼bSpg3P32BN,/i%g|E:*](Ia5v]Wy@.LnlAe IEIBXё=dڿQrd=;s;V>V'64ִf>d5ѽS݈Խm\V׃\^@_P_@M~hiIEL+|1]'3zHOO~GDh;a*wx#f%B)ɼ=sQ0> U>৵Icw<- oz[2Yϫ{d k`C&6x%c=&Aet8N).Hӏݼs!QX1.[mrlфz>o9dD6RlҤ8Ü:`Q'.!D"-:~ tdHxCv"8Wxix -0JFd2[{#dzvM?GcH S%%R:jF#H$ 9]) W2ڐ| c8# cPO^2B&GヱB,eCb%' QY-閶t&zmVcJ,?x%!o'z!qCVL% IualƸc m; SOayV~R8Xr"-' l6fޅY:?$R;ґY-Ia잟Y^*-nNܒз\8y4+/L` %;%l4Me71n)- E,Oa a\g姅;37?U..Lƅx{i9 ZSr%ayN`;ea[ ߂-ֲKK. riS2<c3ri|ZX"e6l5<>]x4/LX"o-Ȱ)^@tqnBY*ŹGN؁Fk2l(/Ϟ(?- gTTl ;]D1lRMʳ?EM.fQl-aD]\řdq#q?߉+_v؋kuS0 S[M?w?rD endstream endobj 59 0 obj <> endobj 52 0 obj <> stream x]Rn0>2O:nb/Nk͕f/t`Fƃ> /W [0 [722.168 0 0 250 333.0078 408.2031 500 500 0 777.832 180.1758 333.0078 333.0078 0 563.9648 250 333.0078 250 277.832] 19 28 500 29 30 277.832 32 [563.9648 0 443.8477 0 722.168 666.9922 666.9922 722.168 610.8398 556.1523 722.168 722.168 333.0078 389.1602 722.168 610.8398 889.1602 722.168 722.168 556.1523 722.168 666.9922 556.1523 610.8398 722.168 722.168 943.8477 722.168 722.168 610.8398 333.0078 0 333.0078 0 500 0 443.8477 500 443.8477 500 443.8477 333.0078 500 500 277.832 277.832 500 277.832 777.832] 81 84 500 85 [333.0078 389.1602 277.832 500 500 722.168 500 500 443.8477 0 200.1953] 96 107 722.168 108 112 443.8477 118 [277.832] 124 137 500 138 [759.7656] 169 178 500 179 [1000 443.8477 443.8477 333.0078 333.0078]] >> endobj 62 0 obj <> stream x|T/>LId̤@B& "ETDT]XPbCEԄUb"̷I^{{}Y.k*g(ap7kΤY߾Nk$^6nƤoVR}-.3'Y> csܒ w5SLԦcHx:'H%\p;0fKf?F @ @X5a\}S+q:9aƸY[L{l掟3h":xpHP{6ս\;<ұgפ lae v$;Y}W{a%|8Ggjl]mkbgsuN Aj0sI ahJGHҷ[xQb*F׷eO0_.sLhZ L;Л惓.OzCn_cf KvY8~'<$NRsH/f33ݓ!M ⵫#N*g`=~KHBH.VNB2髍D]lB=lJxOcR~P͸>`װo''-b?( È"vq |{g0 +HhtfO*{ Ngekd?8OmSe}'9RALeb~={',i8b8ySef~LJ:dØm7rnE93x.6v6nxUNyhI]k^Jb qlVv=d&S$ Y)F^HHpnh Yp-Qrv ݇\wօя|XG㚇aձi8%ș(E~cJX4 ۏMq=,g+5aޕ02P/<4q\g&IjYr8-%QB!zbFqA\)x[$H}!41&KdI¹)Uؿm}$ICXwЯe"}`%]CP'D_C<վtX.Tbfl2-p|%ꢕebX&IjG{ȚY6-ڥGwmv,&;s&6~t8]>éG)ys_c{Ņ1Hh7\ ϳپ-鹜a\G>Sb y??D?8/ˇq/>ټm'(gmc]B2n=#ˑq:0~:.>^^lھ[){yᅣ6_L~L?w)%6Ū,6Rsa];g1gb/JSq%g I.KHriAbdxd8O8I&)9EGb?$x0I):$ak֓ "-#d$T&6H>";>sf\8qYFxHg$qhIq$xOIqH~J_Jmrl )=9ME]'6W؟|"H'(x0I):$a-7ƒbYbs$ r)"d7GF$a^r{Ywi1R'F)Σa)=:H8:ٸIxxl4w[vY~'/'W=;w>|s wp'_].555UZze^׻>v}:uuwFmu;Np;7{8ܓz*/Jk:!oOX$$ND"0l}C1|iBHQ-1'q/$$BH!)!HgRNn;!=Ioҗ "udFFd#ߨ_(P3ujoM_ҏQ{ sJQ{.\.'j/ڛ+jo9wj&Vaný½^;{~ ?A?____[|ssss9W{;t<<|y<.k@J{c\\ϸ^pqz}ק׿]']n6=2Q{Ov^glJ{ڛ(jH/M/۞tD(<+ 9ؐ8ѿC4}q&1!7R6 c7$AMb,+cq5=Xi: ğ?w7ĻmZ֟[O-a[X:}MrA@"MY1KW;㭉-D'4mjߴytcCiPs͵M6W5ܵMk;5-o.jGsMsn͕]+˛45whr564e4GMlfG}覣wEY]qNG>苟Ml!uCGnܣN~vH^:7Eg; JR*FZ@;gi:yZ{379Kcx,yk=CKG'Cţ?CP~CvrXA.;+nxគ#w*M#7`?< aLM_0}AlKp?-~5naL0.^τ0 p̅y0}"X6]j89B=>8,#W%_NV.h;HWM&r۸CAn7枃s8 g2N'{I#odQp*lQJTJNR n#TE5=j஦z w9E P TN5TMNs-ܵܧE9/jxV  yvBO%I9M('y'< F&6JHC hAz0!L` XvNp|V-YlȁB"< J:At.P*!ݠ C5@- zC}0 !0p#ahcaΟj"bj2"h e"ReY+-/K+ ) ߈안7DMhC""oE|w %({>5 !&7 Gh9Oq}\DH΋ًO H^@ B¿У:_@?j~_YM?D # : ac;ۑsЊHH2$'El뀄8J9 &܇haHxDM/ !p&bڑ@x2ϐ@2%w JE"#ȇ#r|L>!#3r|N _&Lȿrow{7`H ?!L"L~A$J~kC9wӤI%RT*=ʩ*iZ'͈}TETCQ=5P#MDBFAECG3h lCC4iFiͧ?{w p0H6HS_BiE;L$H%*fBbh $6ϵ $] W,$~ rhL=*NR r Tҝt=&JR~mB/\q ͤ7q2m1'" C8DFQ&cЛ'ג,cYqj12ҍNQҔ L*xȭՎu76_Ϟav:TmpcUmcf[ưsZƒ-c-]\wvHF } -bX^+UXxwyJu4Λfl5v+*WsѸ"~>n32֢ꫮikfĆT<}o|4>764 Ne&&cYiܨa ܸ t,Cj0LW6Ϝjt@&E3y:[g}.vcta d$Ĺ'W1WjNs7}U)Eàa۬1ko\0l%fO¹KxP(1/M^nQ}˅|p >k'v VO3*6Ȼ]w5~Ͻ'@EZ]3.U#kVdlG^O!+O# JMIbE#T;v=ƌAx7dbEK9Zu-ӛ{{uݵ)p/r0i(.aSX7nk/N>3bb?cR=L{ZQnnQ`UbQ$=5A)3Ed959hb3aնl$ [KU4B, ^ZXആWPy66BK qK.+.K.=;KI!C1:#ɺ: @xtGxr*dUkd|߭?;qe毁$K/Q-Xڂ% (#V Uq܌囱 egbL531x6G#بų<<ij<,E,*lŞ/Sze[4o|[ֵ 1"i ӶѮnrReu /l}MG 1dBxbsoK'n$d+g\\'#<i!<>iY8:QޮëuMoxy;l(ޘh6#]u//Ndwdc3뱛#d6לB65{5i\Rݮ9ɑzUg9q9?TnQۮ.ոm2v6WQrB<WN]ˋCX5`iX' HR!52L-K)d2D2cch,ĂbD|.ّZږ7fDFZ263U+]ue9MC8Dа1}߅IsM.LM sg S=|OE5c5STcX60Dz1f dKf'ElVkQY6fͶʮޚ-6"JlJ&(ѨJ@y)ĎbGؤk!kl-,/G\M^7ĕ~6A |\QB(HZ Qx HW\[ G 2\s5O8/%F\gYep9t-܁2OUJ^N|]FOp:.%JlIx5"`f_,{.,3[ V"%Zb9 o(Pfr-< ]MKw$JoQL1c.[1q;? 8q W^rL)RMWuC8+\>7[mo& ۅo'$JlNxp6LE8:dRj%vYnK|Rwi\%Gķ0ƬOw@a3,!Krr?~ p;&o6 TJG4Fw(~ t JgBwF\-7 =3W/|oYR&'yD]:]0lC?k 7XJ{>{}ޛ'jb NAI=-'7 ^2oҳpTeInf"I,VC=i=f(P Ʋhf:?O$)Ü$S$?(r;  Y ŹxM&6d98{ r ca p-eJI|-@fJO cwVjpu=;۷ E y>.FYFA ~}#&id9y*?ΡKT+ؘz,MBL4y@] t.@] t.@eXKHJz F2$f^ڋ8jKXdo<̡P}[iOնCeyk u]Gx7o;O۔D|%L%8{aJ!Gt&kL2GͰ8]zʭ3G*Z0[L *@qQIaAFQKJ p:SF:SŪfP.dn-h{nn%_h"g^)=ɷ>t%S+2cf'#C S2c,~G9N-.uO%49MM:MqU{(,EDI-ph+d@E<F| \(硒(s6>e/0x$+F\Ћ/؉h5*V^F9K'p(qڬ !#S(*ĠyJdbVXfP{4z9=hwt;+:wϸdRqG=W{+ZXrEW[M嬤~bOT]O >Sh 9>|hQ?uxap G`x6 HF&O/f;Xd<|wDpr*XRmECL\]$,,ZYf BQK#ǗɢmiFCϑLE1UVfZ,1Vo1Zc)*Kͪ HKXl<6On֊yoiSӭeE+f+Jx^p{EH!^P!Ôa(-.Lg9IQ*blҒ"Vx[.pܹ?vk.X_4mFgҾ}c5)NEz=>d~Eĕs-[&yMqwv4KdvlND 걽B[??K=tРPRރ_ ki`QpSlNA=zMZXV%)),*.TIB>R4O9knv&Jwqqt{dj==3 ԏn$#{MhZ߶eee#-Q&C<Ѣml(FVȴ(R&+I7fhI)E'?:nQ'1u%<=FCD/,(-)5a|^,rQAǂ#k/8u}Sg]RZwbW*S4\s]MTd埖YF;JT.i 7o ^5M@Ս>iÆGnnFe ?D<]6|Jp>=p>Fs)hSjV'}g{*X39ӉֳXӊSzv*)-ihViI=|hý=vxિ \qdq=Cma7*}jiNB3!|#mbOtΧ{HbjMx”扱>ms­1fMZC3pP,dNA>CEWl[H[ݮ4m'[[~^NH ƀoh|@eRb"u`I`IiQ1AnN葡>S+ʢ>/'a>Pz鴥%te6Uz/}W('U$[:rU\ë;Dz\+(?02`f2x%F:^v:^zgUb3uJ I(i^; ]A>\q ^eQY2΁HHi4aeuZZV0 &]dJz\L>KCl )ӧ۟M)J KEdcĚã5N)2/ bй5"J4r`t苉֣=@-=R&ŲXqFČjѤ,c3̘yy2ߍ3gvb7^"[68ӒM}X|w+/<3Ƭ{ =c.o|+kjޝ_5rXrV$r[Pj؀cc?YZ||JJ;mw7i~3p:"͖#ͬ &^jl}H0U#:ŅLd6ke ۧ/P3sQ@D 8LR ,24jZQ PRMJЊ;Ri[/oQ5+/}̸),3*ĢЦ-hEV߶_@|Çw_[tSMVzl1:kr|4{ cv#rLJ ˨rAP[̖W=j[gsB*f5y]cܚŢڪRNUYV=P wɭ s7ppXkMF2qXR='åtsܞK՞=;ہf;gMC:f>wcFU^-Yge@tf1|NIFJ$-VZd5ړmZ#2{:+N$K֬!Y#d%ԁ%0+ ,jYNoPIҜafGNKTV)ʱDDݳ /b֒|J`~,*[h+tXCvwʕq`bw vӟ Vy|}m7mӑAUh*^=;23ne ƶm *#XXPm皗ea}qhVF}SPD;Oٿ'MZ8 6B|+$r``n JvZյk5j׸ָoSݦMs^׽jUJe2 225,۸%ZFeS@ΰP3f-HrPPdNPeAM, ov\ cskԜMAȲ'.rFOX-cɧV/aU Ea!723Qh!Q +JdoJmT[L] Dۮ$R~$sēz0N[[O,y2H=[Qdnj+ړdQ5-j=o|\˩sK~\"cďBXp06: 'F:!ɇfUHi=*?>GV\&s pAi ]?=˲IaLoFbeܜ(O6RnkQ!rSɡ,bHf6a蝔^D;'3@){0LrxCO $}9$ B!f{ v߈ܓz˶'jh}eq^iד3l΍։kW>y)b_Zy9wn/'dQgcxlHa∠Ep9f~njAYIId6 VO@Taң,}>bgf֙^2k0lh\TN1^߄3T֊8kEيN9Gݫ9ή)ZR.G-Vf!9CwцMZ(R 쪥}>Yw]QI߈?J0]_`|s]tLzopKj=6Q(k*JdzƖ4xlOofŔF|L%;`vTlL Y dZ&/'w:\%8-x6EHX#BK|f< 7g6dG~߷{7ƿɏ/U]5e2jChn{̭dnw r7ϊ'~|Ls*LDy36}RּYuM jVB,a[0{lxfbF5gqe,L5ѐp:2bv6g1ϕQ̅ 4RΐޕsL01*Drfn+~U n~R@7g (6R.. / h_y)f;FIP|kA_GC%' UpzBVZ$/*K.糘3B|h5R~{.VA~*g BmJZ/&lz-fRtp@d+ŒW3^g󙮒3dM|q3r6Z}+ý2Ϧ,9uxGeOě㯨5fYF[^&5yswmв fQ&2ɦ6Wˠjc3}V<<Y*iԹ~3(P)-Vΰ*01-,:Ecǽă!ӡ|!p n(tݻnݿKmȓXJȪ.&7OϞU!IZl)>bV '}zʆa L̙!, #S{lzI=/IF$L 67>dH˰i:8sks ם퇝mٜݜSDvF^SqCQcJ%9X#4l#dsصju>c!;cM('ai1E2dmlƫM&G'dyz8!unLM缽b|$~e߬C*jʴr6f *$E<;YDIyy|Ы;5=1|bl}M˳rkW\͟ m xǯmm]%~Ӝ o!ovb u;,ZvZ$gvXL0*y1bcm`L`vq018\s}'G)uqBELk2ݞEOm;OYGG?%_Ze@Jݢ4ٳeZ%7J\Tm6B.;MJjKSg⌶ #/bv#=eA,r؁.X]6kVlcӕ\W\wM? VK8TuVVd\CI|D'mU=k;a=j&'쭭q_5!.6$pg^u )wÏ>BO__ QvoLYX>U~:.Qb1[fAZNdfeM RLSLhE|WК˃(sxyqq7m N36O`0RgX"BtAp%BP$pe%åxfϯ;=O}GJONSOz@ڈ 'B3T`H|-GcE4i6roiee?V}/c3]&5Elۄbٻ+&'[\ZU@'fe)pf j,K RxPJD/+=I=,yb6eR!v?1;/#IC/}eE ȉ|m3^O:Wdџ<* zf}N{klYmyc }z3I=gDGC0qbr봶<gc#[MeWv"Z-KE1TkIrJ3šTMҡL*b0L:jҫnaL.wIǀLgX ܫɹ0RƁ~.vccK,=VZh){\͟^-ߞk}VMCV>Eǔo_ѾfՇ2>؟i?P!o {}9vW69sPM4 3Ɵ*}^/ ^=SI߀̱224&;-hN̈a<0gk5 㹡3IoK2j [ϫ+7,іC PE3̼ t|u]D 6ڢ :w÷zhV|ټfҭ<կ"L<Ct_m|H\:EYiwy NJlōdjLVmR͊u;6m=3ԔڴyB*+m$mj:E_dG]b>a`o/2-XSO؛ Y^=GTG;,3 FUaXoh?"]6RH>cV Zcd~~]~3bQ"˥dˡȥYC/Wuis-Vl1HYȥ6EʻffVʥa3aY-rM҄nVdDHTe.sbXqGO zd^5$2zv2b !Y-'@hJO(?ӓo'䬗VH Bg y+wqťQDͷZ]h'f;|sCY_I碬;]D ZbMR _bjoY` 4봏£1Sv[[k^Q:RJy$//?9 h1''?=w13MJ0SSĜ isELnƅeFxђ)ZWB-;ra8}>2NeGrW g7nG =R{ I#Oq2=Z1Z*aJqN-Qh$ z 20{<AG./Z;XԋQ8#TWc?T@/N*OJ< *b鈕1SyN!VMM5r[? 5\0@ Ŝj#ݘ9`uC(0n jLd 7sn \f1TrE |ͤp j4d4|\G5x#`s_ `4.&4ovS^ [<ǷĒ+\!nQvQʦ՗Τhhʁ;蹺d j-A +jl7fV7ȧj WcW1yu*(ݭqAyt `EfG{=dYHHRXNpjXn<\:5!V;Pe-rVP^Vw.(ܙ.^rmg㿼喙^A`1(?bRu 2KңQ~>zyERXmXmfhL}34;X{lELI+HHO WfɡnF.%1`p1 wLi H'N 7s$kX֏q"ϩΛo޼"Qb_KT"tW/=ٯD:)lΝ%7C? Y195o%[$~J2i{RnD +e D+3'a֢&IY.lb(|EaJcm$ 7@Ub,p|#C1OTC36xV1ףj \tfuW|&ZWώ{TY#\_uٻr|ąĽpF84TͰjbkig:j%W)o#4 #,eʿ' f4z0wA`(>ՐS :h-.k2HՉni YH!uro&Eg)qdx_:UGo_pZpʙ':6}g 00_1vaQ[(W,hP0d@cN L0Zl[lYȊn% fe=𫺾޲s]CfѮDI,_$̈K8|J_-7+}md*8Ľ?Za^mToOepMpm;ŝ5ԳS2b8~sw hebh/{Oe3ʋ=v%'>. zO_N]tgP^EMh鱀ϭ@iuXqIzRL3bه7G$oݰ? zBLYT* =ǀ?@,أ&M )$Ms(9;?KkmCX? ` @o[nO ZXځ>[h0̺ZUfMJEx{z%+s%s3-;ڲǪcĉ4MUS|H4n|fުfsQm!Quj~qr+k;rFGc)>@%S" zyr+E\ۗJVf}a> ,2!\1o*hU^֪ԥTLVkY*=s{0h.5,)4{]MJBLrN=UkTIߛx)톐6jB/?4')hؘ󧛏ׅ nL4>&17M00/m|n,s8T+vGRlW-ڕ@fEA튁&&hwMyXگ7~fQ>4+X+Kq772ccN湂wi]jwi3o6}o6)sʔN>/w}07dˇ m-is'\̸.  RE)lToP"Y{&̫C20p8}j p2Q2gA% Y!1R[pKiUSq#?oS6Kk_u,<yY*ciɔӏvrj`YjL,ZQ ngwW1+U`ƈ.r gPETAO$3efg^wh-T`/.%]%6 lإ_ɖW:pā.&Yfezڬ6Ǫ-!z9R$*87_ 'jz|!q^H$t!TgJI:@ƘjCaBq˴qg]f?Lo8dS3/*pfGFQAeWգ`|Ȇ ! ^Gv-19jcYiԆ︐".hYvnwRwm@I\SbOm18*s@)vWg<5\:'xX^[#dͼܟYIk-rDQ1}%~+s.>˧̂Hu5}.#+)lY;N mWɠS׭Sf(ZaV :f蕐2Rb6XHahp3l!YlB9l.Zf/}֕6qcGy6`M|-x |wMAdH)bj(E⺼2~/wA˂x Ұٷ.6%Vф,$ٸGe۩hSN Ve3ZY Irn(D `Wa4J4g΍ L+p`CJ'9H D5ww._{,U e2Yg=|続~wׯOL4@ YMSnM9W0)lQL[D tNWMqGb'R|@*.gAӣh&DjL^F6:ACi5 @~6vWw>c+)-~^q~ojOkU]7؛=w2=Ė|ۃoFu$:#A@;L sF\2za8}uKőЎ9;dM\8 &ӌzB k ^ eru0F~Cw/T]s -UZݺze>K?3ݙ%q{ ,Mɼ3bw`u:ҧ_UrbRq̏-^TUV1 \ܛMMͮʽ݇-׸VihZ(jE1FzJ^A)0)?4YŷglYjll2EV- R\LDXyt4\{WA+iǸؙ3~cP$<i&eZuA "n)xHT\t%\I) ¤L|a^a2C,@39G*U4SN&3R2d9xD7)9KSZ+J}+LP(}̤$:ѣ<,Qk3JD)[T0J˱Ѳ+@gv>%j\V`軬]ZOK|K"6ƵE`Y2[mβ8n3;"ߙ6)U*(k""({yv@V[(-Rv;87Ǯ{<똶?Q:rf4OUX,SlvOqHFLYmJ5& 2P$Bg c8j $XӽA A,'cKК?>Iha]+ozq9die3$q(4W)Zi =Y"<϶VCYĔdQJ8!xU * a(@zphTzUQ/i\ar֫N P4A[mI0CJ&`:BY_O*PκoIμ7/縿vzKOA6GVke5n q9GE ju*IٛͲg!4}~=0\/u\lv^Ӄ>, x;aJ7q./úؽ ۥA6/{]A6_K}2xg`u~/KԾvxFkǞ;)w@Ձl:ed$G]4Cb.>k/}V'|p訶 H1?>ҙi%OhdMvm*.W A%/yl>&XJi u]h +X/] Ľ 6LhU5:F5c-TŢ;KY|t3)#Z xw Z^|9yFerY(pemVá/[vhy3; dr4RD~+4yl, 6@Mqq(,kЋE^.ˋJ{GG2'KNLJ$@$ )W{|'T]$]xGj]碔mkT+s{?,݁v(tZt[siv}-s;b{xYU7TyFڇmb™%*9dhk^i0a/ |RJIM/J)?~*yAe# c+ňh#p1+٬~'kQhFGPu4=hHO PT$xbsEZ+AQ2ktwX~ؠ։r~BUJݗp0SU\Kxz>b7QیEUbTc^uQ6;γ;56=;b4zwT/k! @KɛAa+QqxiĬӔ>Rkћ5 tNV[-Q0T]4T%uKBВ3>&xނdZD_Qtա;pDz+ zf?4J/ԺY7ݏ&[T=CJs{cqS+HLsSsBW_FUΞd ?j* .]6' [N"A$Hw`~4:Loɾ}S45$*]?QXZ;PBc!22IQa 1fW uiT*S>XNj}FhM4;Ok묗G}Ck]^G&_׫V9FSꏂ4KΜJXa@_/jihiY+k߿fp+gc~p/BBK 4d؞ j3Mw<ޠeAa5h K`ia7hgϙcJlDxn$=A+X*Ѩb |]IH  Ѩc])t.$ $Hh j&VJߑh>VKD3R4Z +F?HI:omA44CmG"t]VHId>ޘIKxul߾eڶu#; mMsKFǶ޷}i\Դjۖ=G?w׻ml7׹gi[GvٴgNVn:OranȦ]QG6rwo:2n% c6޾yt;2>.ޤnշno5k7glW߱gxmn# |4il92dt1x/w#ͣosx6n݃48nddFnƑ-@}vr#w7=۹gu;/.9%jx6{_|D֧wn d]jث9Nl#6 Zmۭ0#&0o }_rƑ#ORz?Qj>izdF!u]ФL> Ņ{.lvUga xY endstream endobj 61 0 obj <> endobj 54 0 obj <> stream x]n@ E|,E#E*0)R@D*X<_ƦEtlU5$tze%6C=YNnBC)ъc!O΋MڎgzwՒElJsu_H.±DJV$lNۖ9ψDcvqfliT-/dŎ~X$ 5?2 w1a@'&Z͇@P3wLy3>S DL B耚8=JRҌ[^{>'K:!8;hG; e1{83L*3yY' 5{FsUJ"ل^c]q2YL3 endstream endobj 55 0 obj <> /W [0 [750] 1 15 316.4063 16 [578.6133 316.4063 524.4141] 19 57 632.3242 58 [855.4688] 68 [552.2461 0 512.207 629.3945 557.1289 367.6758 623.5352 620.6055 289.0625 0 584.4727 289.0625 933.5938 620.6055 614.2578 629.3945 0 409.1797 509.7656 374.0234 0 0 770.5078 0 522.4609] 2152 [409.668] 2190 [550.293] 2280 [180.6641] 2412 [430.6641]] >> endobj 64 0 obj <> stream x xSE7|9'$it$-e X,-" ZPʢ hY↢H(#ZD nVP\Q%~s|}ޏsf;3 h j9ac$ }̓nq]ۈVO&JyAǺT8f v/&$:CD]Q;ޟik4h~'jQB$ɱkLs^17֫}q>7n gf\-BxFێvEk?6vrdZ$"uO\.xŴh>{):RȮ.lhS|N7knݬpRl.e UCLLV&"v[`n4]*fRk-&hkPxjeaEx^+V*e4+\䍰܄rH/@ʤDlL&߱DQ4:/k_up+iw)r Tr_~zH>;EuA4D(w>0àG:C K(Mʫ)|@6Pxn2eV-eqDg'eC0R_J}mʖ ɵ<0,ulzv[覺FRQH?ؘDRN>Xݐ߯$MYiFiv@i &QPꀴ :Hz*BE2`Ysm,mTg'k.g 5B{R%6˴FLyy>īc\Iq[G#\uSRB̆cέL]ڇ4d]N Oz3H~CfRͼyt؊ѕЕJZ>TIs5zJ1P/ 1nk<Bj"fE+bp|>ֺA"~KS?Li 䱼:ٔCL؝}Q呖(MyʕE=:17<2sNGZ#NZ* ꫷PJoֿR!.s /t;"6AB luk^0}'6Mk'4| U%{1oؙjM{@/Neg#3 Mli=2m0^6[00 #yfd#gҒH}ˣmwm5nW hG}07wh:59n DjS  )>x%%ȴژ&2o>y `džrck>rKJP>n>-}MyԫF F7Ӱv!#IrDyx(tڔ1J(@$I4'h4[BrciHc&Kw-D!玖_1zVXs (PQT!vM}ur$yNϭ;JO?A`oƨ!MH|AvJ'pAy#!͒@i:)l荴RX{uo {&o7m>C0!,˾}lW}r # @Sh)f 8yHRY={q9Z?'+* @1i@HȞ[ hE`2>_Dm,W61دE/ K#q?ژ⯨=2,:xt4*#42 kD\) {ޏuY#fwP`?UCۑHWʲhP9(]n(F!2-)P mb4VzVĆX>gRGPw>f90K.$ X*Gqo "b2+8#0^}ènE6GEFt);?цJ?=X kg7Ho~矁zүIt \a)1ymV \EG΅<ٛ:xDTE =@4G|_ۉzetiC MwF*¿vu x(Ll;٥@b 5qeƸ~F~XY4 @W~Ћۥϴfa?v*O^}zcm>0~{\fo~m pX^Uڛ`tz# i'#6;]̉K௦F}Qv<'oRQ(Gx:wҿGN>^c1å{)y$Q&e,\?#fuڙP#JE.ďcKhQfMޭV??{#P%'IƢ8\1Ca]O3,݌5HQB!E [6k+IG}"\g0Da^oImǫgǕV4.g8O3 ;Iݴ*Te\=15Tb}!|J?[`3ߩ=Kg@gu+cgQvMugU;H~Ax3PF@?? U;g>??ñg'֟>$Gd(c`ATcY"ri9~+ρ ~7+0&@ax@ ^W;'r9\ \x19ˍbY g#oq#/6.0@c/[791GQ.K_Ld`Gg@}@?5ρ#V a'0ٯ#~;ay;# Cljjrq[&]\l8 G?ixg}Ӧ]F yzєXe<}|XEAs(朓GwqZFon>c>%U|c݅yd-СEuhCvJSih`<m#+hQO=^%*\ eO 5@kh__(/s˩FOb&M0y_ 9]V455v94KיU$iɊ|@a|'`,qʾPX#/y)XMRK!sSzG?Бj:!z¯X!KcI{3nSgsek+=Kd\ 8< [4;B.OG@b9(.6O)lG{j{=[wGyO?Ak0 z>f2RNj -Gȣli;,fu3i"s=M>甝e&S$4_y\qf߮HgC{j^ޠ6{u{G1>h(A$bn'a?u&!>NN;rWF3+O~1aӷkn1ݰ9CagԀ^l_ | t-lZ!?Ͻ tK@`!?C`2HGeUcڲ@h 3C#|^+ȃv?X aЏLI^m$wBj~ҞeuL+s'ڪH v|?J)c@IȭC 佌3o>l~b/4g-SaW7G)AK3ȳ3et'MB^%p+(0uhl/|Mw+f>[~3=U{%wvXޥ2 މ 1r dЩ#avrRN:?S|O|&JH=h?ws#e~W'S9)ISND!-EGǿ3ZOs%!sVA7FoEE|4i"WhRCr;PMP9mi+}D!N0b*0cNf ,.f}ؕ d6Maٽl&{=el {^d +g_[b_oa#;Žpx.o>Jޟ>|,';>O3l>_̟K ^_ky% |#o.ÿGЄ] H)/<'Fh&Zh'D"Rq,ƈ N1E!xZ,kuQ%NW,~qqBa]WP(+nTQ))()+ ǔS3s ʋreVT)Uʛ[JEyWy_@H١||||UjwoC}ZT7C/x,cq$OOO:O.jFχ<{z{~+VoMz{z{syoe>ޫxKO{}ѻ»k 3|) _k]+ MM{Q2*߫ߡLiLLάY9,KdYYYeݕ5+kNֆY ~){ufˮ~3wf8qBM;tl DNt^6Bޗ_ŋ>7>;$>3yB?ΟO _ _W7[˷c)70IPE p$*E"[4M_\ Z\XtX M?_,gċ%N!֋ b!>?_$)P+At^H|2Wܯ<,PQU)+K(*+Ke+jeRWy]YlP6*;{mNeG٧|RW>>nSRm@xXCTSi5M힝/=<]] Oxէ!^_)$ $JߵiMєQ2Ii?4* ?Vd/3%”7 9CS Dy'G%~ [NG_J7͒px, ?#< |48<]~k%K3O,j;Ȳ, '>jC{\Qp|qr-Ӣܔ\ǵ_@RjԯHݍWf8#ͣu1OItt Bw g *g呲PI UX7<߆!K7OC!."]{,}7}هǼT+YCPTx4'HU@?Rf+)g%~4Ocj*EF%Rzb'O>qq˚}o/&?˚k>fG5lfk5$jޭywj6׼-" {JC Aʻ}#`մ09hu߬r |Sofgg{|/{޻e潛=rϊ=y|{kOhO_={nݎmu7^G qi0>6>2P_Ҏ>?÷t=+7Ö==OQ3Ζ(qy7 oxR,G,5˸eyH5f42#W RJEs?v(gb#[qH|/~Ej `qeNj+V NPVzg(Ϟ5rЦh!v>a/#>7s&;}5e*E(cd;*9=Z\"KLZ?YF_󷭃g=2b{^GAi=A/ҳP2z4q}XIZF/trz+44a6c?pQVZA7`_%f(I&nFL5Xn 4n[Hw4TAOT7e|Oe ւ^cn>Y`\O3.2:}>UݢOWzb\O׷]nv@Geqq~>C[lCS=/0Z>̨61}~__EƵN:#?ч#Dc~TI?f4Fc˸MŸ]ĘhxImdC' PqC3, jOSz`NJv#)OK#͈7ta$FdT#p.~=lBJ@]mu#Vr kV߯[w_h5wl=ŖBVEXdf.+&)'ăCֱ[mBo%[G\"lIKu-zC:zszSoXMT:ui*Uaw5_OCX5JDW!*/LJ:+Vxr\J$JJ4GT `gLʢM15ԌԜZC-\jCm<ʧt!uuu+uNPzR/ztRKWUt5k?ѵt]O4R % +26c,(؈a%n !-xi!&: 0b aEA3iL,\Gu/, ,zn5YDFyOSBOæ=CMy-%\F/aVPVJ2IVU5Z'7J )aH؛ޢ`7&C{nMVcvA/?. ) $#^k$c_c|}} ;ľc߳]+;(;` vb!) ELΈ$ܫ{Gސ .1dl)o>Mx#O27e_f<,Ix}l7o.cr`a8X9\*׸Vnv㹃;yOI<`t@ΰ g8$lSh0SJØ ̠S !:A'i,<,+Yc6zRζ ,Ue欱ք2m^֎W)Y֖ubYG) "# ]إdg֝b+M`76kև5} +6 `h;̆lkn=fM@=:^!?}mr[jsAfM4n0+d4^/-5%9)1r:6[4UQsLڿ`T0kIО- /wN,77EVP)W0J 5U.l/>4-*s`}YΝEh׵ϗ{) РHΥA*/QޗLڷ0#v[TtAVן5Ys=k %$ %j?w 6c N(' K!lO9xP0tTVБВ<)Q[ַ+Isн_iv:̊ 23hՆ@cW2{'f&JNz- )A"%sR TdAkwd00(Hs+/[%~ЬPa%W*YxP2+y--ý[kV7g1D +2kA7hAơZܝEYAH-+ .pBdi`rIrj͔K36xTD͍ɿ`}leВQrȣifNu95ȫ`T7 OW Fd~ &>Lˆeerb!N5$txb`PhVµ,)VT䋬;-3 eGKv0mD{_-ݜ}wH*fSs8=£^WfKU+f罹t%eeݳJU :Vec J3wNzܢdEֽo`Bt1|y>WQN_G zVc"{KRtI5H=bʬy~\ӥ삑WFi {}:4"@q֧K)ڕȒeɴXIm,Uj+As+1oۡWaz^t(eIuãcVrx F;p"78<1vnyq]", ` uY:at"7,_sf&1v.oho19<'8|9<c]rs ..w''i*FwCNc9&C p6* T٫%"'{|gF VZ^cSWlr`8jOrJw2irrcgR$1irڇۑu427OGSVU233?O7*=3ykL8wscՅKV]=mVYjcok2l -[ϟ'̾edϛsJûUz7i&?B\`>>AV)?uk~Wwf zYzf83J5=Z-VʫJYR\EPrq.WDortQ_4 ppEm ڔY(6dSGW?c~%I_"5P*h堕h ʶ|:)|*]jwO5EAͣn<&;,\օwS@rQg`* BЛ4MǘghƑ88@uDݎuVr M;.űP^ @[zX nfcn<~xyb6:S@$v+ZMVށn `М kڄ5x֌qMeiq\YrY *pL R>vOJm6&ɑdodJZ&$ Q&̬Y Op8]vjk.NIeHh^< Q%BIg R-⒜)qn%1tּcM:6ذcfGonjS;&utwtt4:jEGX˂^몋 ʋ^7+h^ x]TfUpwk_dqizwWI鼢.Y, YN+9({sY 8T%LkPl-7(^}Yj^zf& t[@\  `<[ϕ)' *O9w7 qyA UE:Vwo 3 gqBtp㣍[L=rO.:=:EYE~05!5tV&# vWL2-BrO!2#BfFȬ-ItfX \1B.N9B%B.-B "{\b>B+ )}/ef3nƞu1i~J#R7SvZt"kܼ~K&߅ } 5|a~49NOh;mմn k7!,=A;H[/i?}X3nBօz?U]`oΑ;Ii2Y_6( |_K{zʑ3Kkl'"Rw?/'Y-Nw>цYcV=rjO)W[\[׉ZQ;54Br͢?^FnLSi7t:?xJ5i_]X6+[t35}|ArV^&KDsT?uKdYOd_U_,ߏG :%B ӈRz "HE~j( ADY*G<<<<<<<<<<<<<<ϕ #NĴT:Ae1͡*VEGɠ ۋ{yExϪF-z n7 QOMR{M {ʵ2{(+-$fAZjVlgR;)[(.ڶeMrn.)9꙾9Z9\S(*rC m}I D-X rq,({̀GmrG.1N݌6Ww\tO1 qpWELl/N淋*~?ˣΝ[ϼk#X\L ,ɓ.KmϕaQ- XaM΄Ͷ:kdEǜ/::4[Sici#O42%U~Ckc8fu0lŚž w$y$Gm2y: D ϸqj&Z^{]N` d B_fwCe:6- =z|j)ތ!RzbX4h?bm.[aLKv5,e6/zöS_v8߳|a&Ͽ=96AHn+j.%KI/J|!ԶM>?|j=k/CZ—Н\dJ$/}7inq˾K9k-,FQCy>kWM}ވq͌ TT:fMxmDigS,HF%k41Kn +%xݹbqz3tڒTf \"S}6ӏgcM~OL2%ʝoTOTy0)/)IL,-)Q۹hg1{>, θkK.ᵡNVs7[rK/{ךk7hGC~m&]/ꕺx!EE#Eڈtt/dNWmT.lWu,2fKIni-N=J"4=-85de(dk~<2M 6?ϧz)ln:ewXIqOo8,fƬ]Ʈgsfb]yVq M'?;\l`T^XI7وm8!/hx!of`JЌB$[lF=bVW&zGbՒؑ<׫3=j?CMLCM3-yswB*rؕ/% 8)~Vܪe4YYm"+jQufGx4f|_CT,+m-wӬIɷ_쌻hp?Yl}YBM>:8֧ ~/|E;eN.vuvvfB^ Ѭk$an!jExW 쑯d(ϰ>bV/4{S-s1D4;x4w[C s#vH=tA\yzxyQDS M6>G2s4`6ϗ)bLb#UN?Wl6󸓿v{3>q9X|;ą3CWYEQ~okȅˡ5흌9==*mBUVM"1R{G;4^`a-,"!9+k27ͽoQWTn<=쉬y'LTx+˱R(¡xս0.'-ԝΓq .[:6QhRq^-'v#8EZvY|n_kDN(M~ }l۪?aIN}ՑueC߆B`9RO ̶кarIEK Kmta[h_h07/2%5*U 6OYX|9V-)pebKU̗Ă-bL)-t se'e {}7-pAd-CJ7،4bj kbN.5Ol ;syG̶oRa;[! `4ƱFqljV=VٞcfJEh>Ejs2]4oaXsYB!܎tgzZ:-dbajtU6OLx.y#xC'G*H%=ŵksw>1KDQbu%j͗\lIyy?iܳw~z[?6kщB _[0o쭙Zljw&hn`X5u;pv ̐\1/ ߤh+屿%c~ywX v yfWO##Jgo6[NEK4\h3.g&Mx3供+5Rn!Oݣ>r>ND6B-z(m,6ȥ_o'ϝn9X.Q V/YljAwYN+w^b3r+<^+̙C X]51tr^=Y,bzKAL`H$]GcxeNAq?R˰ biFHAXܙ(/N=p'; "gEeMm_e/x6s*1Hc\Uz4?ׇw\G#>!~ٗ ^5rkEN[P/ <;G40K,3%JH2=Kx' _a\EW+UĄ..>'ީhGX0q`̓묯ƃ\h!Ml_CzkwۍGx8';~OӿO?74v;ص}Z:"SxRsI/DnRCmհ RM O)k1#eSŜ6Ζ1tԎNԺ/,[>Yl> ~c?bc6=z>Z׆t m-n!כrڅ6+ƴ8bY|D(L| X[9./[yA?hc|$F0 ZA;Ha[P*!"Ôu`wy_%mWide).NkA$B'JT%*6+s߰Sd2i\cspjyt7 >Z`Pp .š?x>؏'?wn~nj,_8C|oYIC9| D%4ܝn[ Òאu՗; 3o0 lCYb$$e5[Gv;ց}i=@B,t#4ާGpqC Cc};򚰯K,ųCfyC[H xΥTS?#kxFT ki˰(7i؟Xmji6HJd`,U$|R:-oIHK?$Ue- nQ S.&q'>O7)l;`7)nDb04dPݧUW'Uq+xpވx'L,I2^-QY DN V$L,R1 EXpƤY"!?+ۈF:T=\vVa ^^f^;2jpw\7׿pA34>𭰱 :jH.Uw(BF|R_?0|ARV?5Tk##dxӥf7=输"OA CB͗ȇ>B6/9|07|2(MPB%e'<t)Y8{:rVO^`>CqK+~/;7}0MϷKSRz!Yu|K' gϐ)T6>qϥCC=?N)]SvY'kjaWLJoɪy/7!)nR1 pizt콱oMwRN^2vaK.#ŗoxq1{#f+MP W*\'p r|3EP)!KVo ܀| zs@h\+PT/hM~UfL 9|Z(&ARhGEO V3\pKm_hpL+c+ pFkfW~B5h^gG[e_6{3cpWh uYϘu%1M9o)ކ,y'_{VlR.,qZ;i[uŬc2MLlc0|NAF\^g.<_ebPV@fP5 K¢I{>z0Ҏ0޷-ܳL_?B,=wpF&ʝ)ҩך|Psfm/ b|əvJZiDMxW9_gu+ endstream endobj 63 0 obj <> endobj 56 0 obj <> stream x]n0?| endstream endobj 57 0 obj <> /W [0 [610.8398 0 0 250 333.0078 419.9219] 10 [213.8672 333.0078 333.0078 0 0 250 333.0078 250 277.832] 19 28 500 29 30 333.0078 34 [500 0 610.8398 610.8398 666.9922 722.168 610.8398 610.8398 722.168 722.168 333.0078 443.8477 666.9922 556.1523 833.0078 666.9922 722.168 610.8398 0 610.8398 500 556.1523 722.168 610.8398 833.0078 610.8398 556.1523] 68 69 500 70 [443.8477 500 443.8477 277.832 500 500 277.832 0 443.8477 277.832 722.168] 81 84 500 85 86 389.1602 87 [277.832 500 443.8477 666.9922 443.8477 443.8477 389.1602] 98 [610.8398] 108 [500] 113 [443.8477] 118 [277.832] 124 178 500] >> endobj 66 0 obj <> stream x `U7zK[[zM:Igdiƪ0, TQ#(*GTtWQ4"0eDdA-qp .wnu'ͼ{t{T{ֽro`p?{GcqƅSgM_'@dԹ51E>] .:g*y; &Y[+%ex}S[.D]vH%^0oƕ sg>w֒/(~klWCΙ5uϽ hK&THr4#Oi) l2{@}0#amn~pT=?Aw)*[kd尊l& |0W9=9Hf$E#U!BA =됮>#CZij 0ڄ&3~tl'И5hߍ4z C15TA3]GPVkiC=8W4ŲH+sp1>K2뭴 ~6-] w|0盗( o_uOnH6>cGa8X 16"kp 33ݓuAqmHpn{yj,[D |0@J]6f髏$=v;V uBR{ˈ ; Fq/3fs 23"*!2ҥHM؆N7wl͑8~|[6{^B9~/~?b$Yxj2n9q,k:&.7Ƶccٱ<>6yc"WQ{,O$HƸMI^*KyOΙ9F'ƿDnq'_"&K q88Ȼ05$7'y̆x_whoۣqykе'ϓ ~l~$ _S'?'oGw?9 KbS| XVro ݾ.3O11bʼn5Y c}cY,F?9/i]gP巡^M.bSq}? Y,fsk,.Ksّ։]:`n+ R) 0d{w ˒t"%iÀZ$ճ_a)I2m-Cxے;qۗG~x~ֹRsNj1MsP)|Ƴ=-g5 \+FK`Np %lx)ށ[U,C_Vq鿞{/_!12_+OAļD>HB,\s;>& l#ag`c #JXo 'ѭ'I 9M1* ƻdG bN$_XΚ)ʤ_>__oߥKI}zOO)\ ߇>yF N#qD␚;/\y0ձO'ސƌO#1}GnV/_ @jQڭW34Xw5<`;o;o0"'q \&r2T(BH#FF3șd GZd2\N摫?dyt>|Fz ĩS'->KwнC s2N)95T̥s>.q\!WssqWpWsMܭܣ\< ^LJ 6N= I)~7#W/?8v4#pZ{Dw'05|IR36ǵgx,+⫱;,<|xcFVcc?` /YS/570eƗ"U̸%qO<{덏={"o={k]]tOZ=wuo]+wu-t]]]={Pwyw@Rtgt{=~|~>FY|Hs?nd'U>>p2<)hߊ:?H&(}JBZF(ch]HУ%=7Z%1" c m3PycBA'+ţF!oPcٳ78쑓v' aVnep< qa)ƽ67Ýp#?|:!p,iLU; sOhOBzXL8fp!b s .vc\0Nx-jkkO- ,qd=B BA2vq[BK{{އOc|m$seV? r!UpF<-zĨHQ n󨆦wWjr*먖06ឥOpspO/sܧ`,h#ya%pHX뒊h?$dȥFjZHFH40,`CO81!}!GO$$pH8_$? _E Hփ R328 G SyɑՏ ~$t#!? dqHx֗',GlJ9 F0$HL y'? H $Gc |FH7 _/WkC!ߒr!؇r(  0I^#q PJ9Sʨx"* cܟ )TGvS=5P#MiDBFiSuRuL^~AEi\G4BD?OYt p0>/@bԝMD  Ѳ_x`1b"O &kHOB\'ϒ;RiwK2$22(H%OZR}O 5!adp2&rd'#`7H"dkr=sI ? >'r:6lJK5FR*L9J 0=CkT,:`J m"5s2-g2hoItJq{\y\d:OG:%~Bn7᪷չ:W}GC{)uߖ|́-zSzjo c-;lN|Su41.n xϐPvCV]f 2ד9[ԩ<Ӧ5u=u+,G/oJVvce5LElhr z^8Ӆ}E-;"ņˆtO=+rCi[zmi`E?[kix&2.TX:zS֨zlMnՃ(-s3>m:~}*/5pTaz,= ᧅfv(j,U0 >ǵG@5zz>djD)0Xw udgQ?QM8j87#3[HGQAqjKEv3-xѱkPKjդa5 joA4n\nZ_do R=]œh$!tNoAME 3pxl7P (RaӖ\mh'q%S?sL<6wwD[OT`?%^gi p^^lT3ouѺT*VVAK *)3-1˲"˙vѲti Quם/& :Am,hxqX-uAh?}.᪁VI/I8{$cȇnpwm=9XACԬ<y` &ÉnjXfugOE'%nGNXTrlZS'mX|o|yTp@DծN9w.ovU+;fcW_w^X+={VĻvFyZt.vda;R 8Yz9$(=8*jWnKWENeRa81\bN4Mqzssc b3z\`-'WG//.?O\>V\>T\^&.gb ɵr\)er^N.R;G.U&j$ّuo JХ 7"݀:HW#]t\vbwoKUuӫ+ ɳ\.?Ş|p\:1 w`wKdj u'9LICG?4y7(z'y^:>Yt!:$J-F K K 'X++wteDGշZU_V32~ 5Y9kffkT0:pf8uRs\5kb %5s˥f]ٓs*랬MddYI$&6x2)U{V7'yRu`1Q݈աgu3UNV$?x \T6a Y`E:}lB<&D 7. u7om0u #dZD!nX|4x cp'|~Ŷ`DO+΁0]<_=A ̂pKO Ux%h,׭ 쎿A)Oz(D"t8|hv[hэ9x"^PnU8a/D/Dy$r0.' a BRm %#7%xO8sg>o|NdL"rAޡ\.gg@p68R> &T|1~ xIԑf"~s2+4Q8j1(a)hGlP3AG$H0d?R!#.Uso]B}gpYlLN}i8v6{}pv_goÇkCJF2#(jj9r=YHn1C6gȫ(F:F,}Cke O$h• YIXil\lR=z\kNw$fHl4]Vi~v:QiψWqvo?qf?aG(g+9qFd Ob{adi#+ȕ8e܊|Jduz^? <ibnxNc %p=-C)d_J*"k }F~gf沿26 Şy=RxZpW`&\; =Kd}\p,/ +@19# @6@I7&H0ǒjw؟.9i ǂuVC2Fw8\.tsIE5@ {@VUPTLPи` ؋ѣӿ;俓\'PoPIhOgaTV3uN):EStN):EStN)BlO[ʤ_vDXN5r(eb=rߢm!%;X[yGVBM/Pѻ$pP˝G@գ)~Ƃ 0 7o0ԯ4qQI/,, vAoTvTNGxKzCy]N zl5 VBSZ̓/'b$ʛYP,iŁkW Tʟwa,{bq~mv֠ PR/-d?g𷤩h4izˢJwZeQXO/[}Uam DH 'RpnDFCw{ʗW!0ww06ܕod-E_yyVJ6ֆr&p ްFKV㍗YBQ]]PStEd"~iZ,iq@ek|1M ՛%%Vci0 *  F^Zr~6ԧ!aЀ% i"%IUŅg΋1I^{_CNx VQ̶F)? Cq k[ R?a>?*Ý #dETqTEWrbgXB6Gka[~CzO}C4~E8R-0mdb AqΌ[_Jݓѝ&EuPyFB\RV5K5r"{t{z!JZF>{29TfS_茆":yEr){6;|ls۾ H=:fn!'W C6"e컞p>٤ Qhf C]K/-CՌ:sOw9FYy+hXx^0 .w] K~ͻ3TWItZC!Vkbm^hn{{BGe\=,5KR$DG,.&5Ԍh 349uM)&yg2/rՄ:(7޿fyz,U"'Բår<9PfýO̍dFl4;ΉIɭ9zXDvn :#Q;jœɩT==|lݡ} :~U=[}[kzZ'_ll$aZL4̒ KJ͜L:.rْg7F=,<$DQ4V~2r2 Ē ܷ¹?Gt_~ac^S^(ї"EnC<Ez4i+S3ŴDMA?/O[0R^ݞj$ɕ9()eC;%'# p(ݦl[x' n%76nuǮXqÈ;hAiE/va%۪琽#;bbž[w/}bGb0/-F!g  yeJ,(=RRk`/sil5;NH53)cgaƨN{CH'`7MML =r6dx;2(9YclQoh;y+-quQOگ.Vs`[:^rUl6#Zg(U/2dPmA Xh` ֒=fD'`ehgu&̞t+&+圉)CbϘ tN̴)YWɪ#uwOm3)gY1ֺP`)Tb S]WU.h]2~dس o gX+Df* _{,C)e= J/kq2?@;J&A!WX(&UVlL4[2ljrs9ή͔2Er&Sz;w0YƕʚJØ . A^(TVĐNw!B DEFSVs08߄؛ptl_11qbzҬX.'ԟ . MjEJ?7u3slo \NH;CR˔)i4Kg[rnڹ3J ,-Q`Z#9 UC(Ce0.3Z>ԙn/i)ݢ*R:iFtUCyP fe-XVV.6MsI gEЭKq:PLyc!eQ=PaKO2\_.aӀ' ,RLDmK^acI?9=cR%B$Q/&,Br׵3kÊEO?{aû Y:FgVڳ~oɱ=WsFzd+m}"'׻K-4"~ {qaG0|^a:i2aEtx~0^6C{LXAm~vf{}oO.m )#{&J?-j"L؝58tP>yMa:ID[Y|mU_,{CU,q(2{W:}s{޹QWgǺ!Mar0:b /GG]ne {qI>aXXA6z&3~{5֙fnШl)n7)LT(.CZIt8TP̖sM}/'-4RҜi4-- ~tM5IhDPDrz2T!X."6>cm"v@Η4D J*MDm!=#)&y"929$z 9rUwv~sbre@yE|e*ޢR008L, \>P' T8 ՄH'pI C6;({fr1ISPɷ^u2QH=s';(-M:2(פf<›1Ҧy.]F&H;}*C*uu~ 2IqWVywǘkTpvPZz`2;H;1K[?5iZ"= >mo(F]ᤷcmͭf: Nf 3M;ZY/@c^%\9bYño2vL(?,^kոJ]DFvEr%7Vџ^z4wmȚʧr^utS=@wr[mE)ejr*a#':Wm/kFDŽ5J-fN]L 4X O:~*UY8s"M`enf8l4:BmuUO_U~YV=݆%bϫmA )f3t+/n pzۙp8I&X1EHEj)ʡ'ݪpj]a&r=8G,Z>Qo1Ѧ4tK OaKOw49MiTt**zuUju_yﴃz9@X,]JO,;֗ h+0r[uDPM士lDxeeYEr6d4-@'ͻWeE#sJOכTj֘D4UiNNe)T:KΪ!+j,. 6rpt`$df% wofnLF d׃rloSإ'8(5pHɄqX쭲]d?mQPn "Gٻ9=Tّ6aUi $ tO7\+ ٱ Aՙ%-dt:MU 碞pSYeDX <\kW4hW ZL2~Cj$*G ^9}t6~ؑ2*&'ߍvrmZeRss n(-*>3Rr^F2hjѦd {2Pդ[R+hnJA<{Q+< sy 55\U}cv=뫬 [H_W06,{Oo}ۤ pi ^I[p{ޝ \sU[ >zzץ5GuكnzW[K +j/xL1;JLEGhopo4]h0U5ߨ yT|25U6T J&HT[BaA0 B#/()r#[: Fho]^^hƒ c^9e?U59Yyr)0{$5wUb^+,@Y;hLL\\""RF%Q(JF9C%͞jFg_Wج.`SlJRi6X,tF!F7ѯ+]rM$7+[h{K#Uwy!kW,-ɍJsyb9ʙ3\:9M.IZ>Jt"J=ΌpE*';sen>2HYD_).n–{g?7RN[+g|Y.<+>GU؃*hpM"gYkUfONmXe|KوpR>!z(iT#.̷+EW:nͰAA%~W"WZ2UQXlK >I!+y;d˪:?H^~$i\YPIǘvRdJ+L_n _!%\BIE%dF'4eKboӳmCb΋^A}{} hěH(N9U;މc{rsJ}NΌ=V`KQgsX@va[E&`ÀYvF.Pv~2(EHW*1rv#˞h Yۃ7Z7Ч64IMDoL4k Jp}"3yxH^)N%bsaPޮ|T) 6MimXĨ*m|%wz+Yҗ3J/I$ގ@24*k7L>k<5w,pȠڲXtV9'0=p ?.WRJD;1|z0=wcD@a+a.ڵ9T>ũEk5턚l%o7*r){cagJ?g n JnbM[<==|y~q uԪEڈVd؊벆 ivpuum)t\rb,M+(.5JKf@u5J *v1,&jtC\s\jT66IXQeiaRr},xi(7K~TH1e/VBN짧bNMdfRZRD^sӕEw޴ /d&^X!i"-E/OV[ڮ.ek cFX]}5 LJdڑш M61LZZhڢɑwq㒒EW#;?"\ ^(Qocxl\%ޥ:oqDυӼI£ B5<-iz!BV0R+2EA>H)O%;Il ~^h/IH1frfo^\[L)\sR׈xYPj.E{sǎɎUFWwՅ%&Zc)<\I,rWbwr?Oƺ~Z3?46[r=zhT$ݮ 6SU]o"}Qi%VhN r*POzumU;rlOEMN~N睭kv`L)Øy ~+tg>3eʨ[D{Kna?-0e*kl+/hZ|Ϟ G_"}r>!ojg/8Q2 XY,U}Pq$TZY7j[ۨ|=FҌh%Y/[$?ƖGy7$ ay( fKy{ J/$KhmB4[] %{`ɶdK9sf9KҝNwQ cԱsG?>.}Bvr|  h Fh0m#.64pZs@r H1TW{c&]˵F-v31oIS.%`n9H,{.D MIU]Wrא/>r |QS84yI9mnS\ʤfGmƠR][מ_o!OƦ j)^06$V!)*͆/f~sX-~jA]>Uԛ9kؿcJ P%=od*;kC. NB v?#DDf9J[tνo^֦f {VF+FWq=!-/+b1C7<?wD=ョ5wliݯE&: 3~Vڄ@u!5-LGxSx.EjNlj=qXO㰶f8Єod]ʒ^ !X0; D5k./1g]R9p1`d%b2bd𫓤Ueֹ CwN2|[r'vSt\ZӺwB>R8_I1G]zIΎ/&^w'C@P8rcv|G˨0CuXȆ }Gv-Df4kpʹ2^^c4Y/dAgud]%v2;/[|s f M>5/{Eϟ,2gKE)*uEZ *N)ȴt4Tka%IE׷boJ*W[xιa=䡅oƭ-ΌKmPWio^¿zTochF ;p[˚\ۯ/+?B-&ar-ϧPs-df4 XMPVYW89 xӷVeАxZE6 kM[:F3G^ߪ0 dps[DS/r*%RUqXS_߀* WXw@?#V|<"&y$^L~(sZ\}wƮU󄌡k#ek b J3XQX@^IN&z+6M%^5my4) 5V[_N)J+ i(CW>Bt8]0?i~ٌH8<=@[^Շ<^7hCp!N qMC5''2GA|o)ώ K4Ksf4>^d./"b:wRt^Guո|#0ϐ.Ul0? \jcVE!ȨQ~nol3V"P[[|)hJsc]]q6RhxE>l:s ;x:{4`9:/lu7hm /8p\x%׻qmSoldq>Bd>h_by{/^\K_Ӻv2?v->/CGZ4m[Ju"XxkH3 z}u$M$|[mm t:֚ $"n^{7z{811SH&q!A&z̡hW7JƗ]axߊ^Xn$Y㊬y=R3v3bWs _=ߕCI'b|h@jm%)t]8~Zt5[}A l~MKE6;Q_]iܴʦ5]f͌v8zýlkn{9A)?YWeYOX#|;Z~o6Oohz'_y?GcMXߞjoltc^R66a a nԄMdZ"d݁6G2Ѣ '>T76Ҥ8'!ve=v;0Gg`6@m]ѹfQCS쏚 | [U$$sZñxUWXn˄*AYcJ5|@T٤݆)Ԍ\-8v@AT~M}Srz^(R(H?_lsx:$MC։Ho$}5[k@{~E/`Ձu)U7/V=|G"}xrOVNɕv~; :{6DVFjq%Z'{WQ8?v8KxHW^ynsj\|gȋ/;sc_wŸma( 5AWɬ#8Fڽ&;s?QUVz˝Ly<0 BhROb'*.e4X 噘9*R^oMYMMOs@LzJJ2\尓<$DraJdVIFh] xRWx0~oDp_]uPYv~I]RB@fET{fdb'>SQֺ@0ܦIH )025;/]n$]l6|$a5_\lkLIN3jMý:8?Wh!u \b'L֟-~K9RHyݺD,imwmxj{j۰Q~(M7]z&xLܽ8\WT)UYTԹ3 ׅ0PUI$2)pT))jSJ6I[H 7>^J8jUZby9 x5J~"<:ߗ/=[QДBϓ_D+Q(0OԽ7s[<|S/Q {ƒQj_ قP qp \DȈ,")B_)Y! hRZ/%zœC QPrqZA7JF17$ESMd $Q!DjϠ&hyw^uS`cJ m_XtVU8ஃ/D1A/;߿Z ^&׹k~)\=@}N2[}c|p7ݢVGu0g'1} /~oyģg8o~#& <2IUژAY\ƹs#вf%z_sesp!te9]+ĕuWT`?,mQsW7. CVg5a\osF@,ټA7- *-'BM_N`ve:XL|zioTa/\2J9^N> >%>wK.ھ)ݧr?K^#o(=Σr윴 ?oB^F_̤YCdR&G_V h o˻WYO ҟsƚa4悡\&2 DXzF QP`BKwQ; k@4$q}  m"7iQ*bAT3[8%ey-#'maSyޗ=n.{vͣ<͡{xfGdDxH.q^@ %Ik2pu|cyEn8Ñ'Kۆ^\:Yx[阸I!A\5+c¡ @*x`czqpee[ =SSʎ10C`R$ F!KWÞ &0" d~P<&a+ƤsLB)\\V0 % 1?}Q R EL+ ˥<%돈0 +QT¤i %l00%pZ jS& k'KX'^笄RE0+y\23"V*ϧjCbIg*N8gNjNPHs>եf;,#EPّ8H28UW9Z=vN( J 9:6QS3ΡqXxqxZEPHrQ85*SDxYY.L9<^5KW.S T}PJ͇&vU[]-(ZX{t̃2Jxln3P)h 4xgUxf^puo~(f3]8JJ)c֊5` +`ůn?U endstream endobj 65 0 obj <> endobj 58 0 obj <> stream x]n0E|"6$BJHX~ː_3C:{3EuL7  \;t78A_vßAx7uʞs}z0A3 צmSiM{ȂQ6 \cdܯe_yF;lV}7ns%9ҁh$%Q1Qt& QJt&"e"HYH%"bת5>z$G/Z9T2/Ī[IęSs1#EdH$cRLIS ٓn[vE},X^}期2>-2.R endstream endobj 3 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 5 0 R >> endobj 6 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 7 0 R >> endobj 8 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 9 0 R >> endobj 10 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 11 0 R >> endobj 12 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 13 0 R >> endobj 14 0 obj <> /XObject <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 16 0 R >> endobj 17 0 obj <> /XObject <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 19 0 R >> endobj 20 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 22 0 R >> endobj 23 0 obj <> /XObject <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 25 0 R >> endobj 26 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 27 0 R >> endobj 28 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 29 0 R >> endobj 30 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 31 0 R >> endobj 32 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 33 0 R >> endobj 34 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 35 0 R >> endobj 36 0 obj <> /Font <> /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] >> /MediaBox [0 0 595 841] /Contents 37 0 R >> endobj 2 0 obj <> endobj 21 0 obj <> endobj 1 0 obj <> endobj 5 0 obj <> stream x}e9~|ْ"tc6 ~clV%U(ny]/u/o׏?R_o]ǿmeg؏ۚt]內?gm?_O@YmDg:_]^۫w~vuq6톿n/o~^>~dV[όqOx/Ioy]94voO~y>yK%^?OE~,nNjO?~^<~Gّf+b$o照)?n|P4_xr {kBcS~|5>?ďc g[NN~MrU\;me( ZvJP~-A}Oڠ;Uwōz/VAkQm˫[~ &֌C8_efݝ;סmV QgyFcܔZ)Kld-m6d/-VȽ@fq`(D~T,SZZMB[9Yr9ӷLn^>l F\mK8Ѧχ~gYM6Qh@Wiz-2w0w&i02k8 .ߧ4w$W_IWA0~rW_[װ/0T8l+ ~hc$47CpӔ>G}W7up -GZwdr_Lr3%[w ate>p1#8X{Iw>r$^%p1ŖZPգ~<@kf%] w ^=v6lp77S0p??4سui6 f[$rΦׁ4k`:Ŋ\dgɾ]o`ˆ(ͮD^C6X5Cvꐽ ]W:.z 9Y$hyP?0h~r oU3hM&^V)Ҿg4MVrǁѪ З;~Â<8w{i_~?wW_Is4Ku]4w: T~9'f&'>)2M܀p=z.OJNB]k_PiXׯkh;&MЈ~EXQni멀O9Y.͠}$}iィLe_g#?_Ї񫳻Ї_R@WN 'A{hf3[v.lw3[|' 6~߱_t98aJޙIq(zuC?!sÇTjT*HWUмdͦRrj p{*SC?x9muDFn`!?S^r-Î&7ߋCߏS`Ѻ7hMkWt*|LN}UZ)J[>G[.ٯk֊wp kۀTe:UeIέG}O\k4p`y_,BR)Ń"u;(tD9k'@J',&D8pAO)=!a0mܠosk[9G5@#JFwh̽s6h mД 9Mief K5G&{MRz48)@QmJ'@b̭m][^RX XZ+`h x Ie.czGQLjG-(Cq}:Y 8wEpFtkCpZ(""7|hDp^_Yy}W8deR;4/CKAn~&j9hUܽAi|Yc[웆{{o,K;.C~tK7E $dZ& |":J* O Rv@-z)]KҚTڀ0$83it0k 5T:J1Mա[]U!oVSےMk)Mm(uh C&- 4#G_Lо֥٘gjIcp<ٶyN*GReERn谳X"&uZ)m1m 8Rͤ KlXbS--1ɺS9_M%ݯ༮g)>R2:cRVZlM $Nʜ5ξ?餌g}ϡ,FyIUTrgU֥,)37yb`.fw^ߺy/̟)RMөuI0X+[žGe?eS ^y~Iʓ@s=dXKy&b)nQT"*j &925 3-1CSDbKۖ_#5i^g)':G%5ew|dc:k hq(dcV$0 P-qz>khBNK1Z<rbпnTv(E ms@|3Y234ĜW'@7lK'a )RܺW[Q Gv =tHK%1|I%PN:ʛ֡*%s٘\&V*MǏϤTWT{@c#/ԅL͋dj+@97ٺIL3ihPFzw̵dt6fI)lKz&g"=U9nKۚ4ACӛ˘nnJ̗RDr/) м^c葉44`" =2%WSE;ssMdlrn]}tLK(o^>_JMs!OIݓ:-d4;(ʾ{Vқ&Qd?ǟ]#;Oh m)@-("`Λ&C3Ft%K@s^]h]LsO%ivɾ eIeR{qCZ!) LNj^M:|T] VyΰkQ2[Bץ}/H+JbզܥԎf@äSGMS|]xPޔax~OOz[29 ƕ5m߆WTmH ق9Pq܎yR{J`qo,)@)-Wٷ/IyIS}{ tnICFҖ{fqX/JCJTvS1sQv("ء m0)kFЖm:@隈{'}:سAs߀p+WSXt)≃ )o,Ƶ؍aO>fg |?gUg$Yq-v#-R}"5-2paxu)e(rgDcq}CTwțn L=}SJʈ"?QrܑJIgrԒ}gʓ}ٝ!:+=$ 2=2e!)Ŗqо) .]"ܪHk#ǭYѷ e3;ΫCFCSG$SeIy9#:xMTپ;wĸE%eMjjT(Rd((PT%{rfwMm͛o`L:nEZI+CyKPbϢ}<:j"8ʏn<UZ :G6lhߴ\w :S'f`E{JsI\>;MkWE6S+kJ+17VC=Abdz"-j 'DROtJ i 9ݰߓifɾC.As1djagjK 40m4QT:tTf[(oZt Y7{Rj=LPNiz;gTfҜ@[:n mK6^~4*1Y)Ž>Ms~WQMyA[~q1|kMO$ChϠ"eyjX C)u3̎L+>$P|&:e;-gs֫)gky3L56O!mHLZ_M@*uhO3%i%TIP>djw1$Ӓ@^B ,Mj.Ŗ{st稊QW츢|tzJMmsŔ9NE{iVHL[% &IsMy>ՒGSY Ȇ9ϐmʔ"B -C8 ?EUx4A]'Iԥt- ?ЧuGXI0U`G亪(DiDS*گKwo2>}*;3r-?e=rZ2O/OҝJ-"(̴ m颔j%3:`FޥUq2)PKBMQyY5[% %3e%,ܷ۫D]=8*XAYrإT+9li \R\Δs g%jUi9!v@Nɹw/JkGҝr|jS#_rhY$™JR&G3q;$r$ojJqlDqluI3hץT1O]xCN=V% mXsrr~i*UlC*SCe)JbQ^t{$yèY (I&E,hf(P>:.C%}VȧfFٯōs$%ɳoqH-J#0R)ӢKvC#ituIC:Mr3צ)u ԷLfS}="N^.6.JJk;z~߇6hv(-"'Hk|cױ!auJ5p9I&֥I1ISBȧf2%;KԴ&sh{O{}x}z}zikX}xiXᱦ3U] G{[딑&559 /yLEpQNϤKMS&hIޔ7˾aszr5:۔ĢFciICuelr:\zI~!rZ!ֵ?&ClN9kj}`tS1#@dttVū)ϹT܈WgתGfamyO+o\TҢ%M{ %{Rrf\-RlžPUi'u-n}T-'ޣ|{\0eJQRU$D]INUiŖ'#R*Yϣ|Ym3-4%ЖMUQtft(d9ŽtCuȫ䝳o7\l쮘j(mr)JߙqOpmNIڪJ].>Vs$>Vsc#M&]'ZZޮI޴r/XTK[]~Ɂ2K"O!Jz{91X_^@^@^>}xj>utӐDx_[@U~ggQ4OoZ =@*xSϠʘ&*)7Sx 7\wҸv1dK^yL?JE񡞪]03i≨:{灡qr_@=[ё$v?v;?2?`*&!mt;"z]{[(N"RЧ S|(bTpjΠ"m)=srg]3t<5pֱ#OI޹t-QP _wQ$^_s4`:(Oym) iyaC,Wcǔ@bCc͗=C,'9F༻agj;?Ӂ"!;(!r6 ,̝;&?25X%P> m|||>06Jo~FD:mIx;N:5y͢x5|Kqa4(p9J 7:@5ٺxIPnz^&IӴj0.dZVI&yܚMdOՅMWi\qSa%rC{D8ڄgU] 4qff0f $6puBAuKy\k-X{aᬣqXrP:q c"~ ohO:S__nܨ8@}5EVܒ&lPZJR);Q%j$-OlL#wDNNɖ*[i{gbgM"Y|ay;g-wEmYT"EGӢ#qJ SRް5]@ugl*Ewb`MWUPAʹh$`6On| e5Q,ZqV!7*Dn7 +=9*W {XգM<$Kh++KgRK^߯9>bht*SRѺCCl iy`)<:ǹx ܷ_2x2hq"[W9no;gIޙ-.x,PGu)ِ"NcglA%4QycT9FnTHݠtU|Pg2@i";j;*sܠRT8޼o|R:ǩ!U2%rT>mՕꕤOU:ǩ[It]6{JU[pmM58,.=Z<\s♴(zwmf^:T8\ihW|aI+6֮χW㎜W@bMO Ɲ7@"= 4o`T$Cu^מ.3GzCw? [S3t 1xfZOWN^O%и,/K*(1>sǨF5;$ 8 >5"ON_үuT իFi ]Fd7 vT}Ͼe_W/(\hyP@V# #ss[GtR tOp9TG)>M/D* SAy-s=ոsA9Ԡ+i5k.!(_JUb|#-'ץxY\YQIn:XUW+F˦ U)SEyScJOʳ.e%=78Rފt<4ڙ-JWۚqalͤ6@yd譹 9ioaușQdkLZսvFy9`6I[Hw▌6Úy;_70 ~5n]a:2,-#WU^.11PөgiKJ^@;{ط_KEBWs2y,ζ MF2w3.#}`Щ%u-ʅy1kE֫ Q=lDQ=&3Ui} Z/vܡ<~i`fhΆ8s}Tunۆ.sO.(&)RrExIZWΔ)Y+D8arjm㞚+Tyqy{~p<޻?*wFސrw9n7:Y>]^r9#St&זt,Qc7i\kI HkU֤44aңM81n Ǽ74%*GӒ35XUFdht6_mѴHLP$| EB\=dٖ%DBzep'G3p̔\oLkg#,(C{Uk)݋rg>*bs2ܓM jO2+4zWe;_hNx`3\;IfZJ^n:E>pDR7y54$=2 g~ĎեT6OO9y嶔4w FysɁM ]Hy8v")ԂlU)bE{,L.HC@S\EQyY$qWPJ\[MbZugi% MЀih.}K^5tNUԒˤޚHOw zż\_ШG}KV ]C䣹-Hܕ VY MhGe"ҥo_✉ԇ6\E+mv)xÑ#ݟ\8 vg*5fMpg?/)T(PUCmA6G4iۀOĜ3s4gwieMF3/Kq5;?Ȱ8+u@̳!ȖDøiN5$ unΩT,Wga]R54MbKEr`遙FnSnErKC zU˻&yӹ$F~t}HVLtо)Fy$)xkUpV<=[䊩LZ<<>m[r9a%"R 6]kM7S3״[}6E}bz9_[yo.շ+8ʛ)tH(P>g;{-> stream x}-7?_`zJڝN@  2y f*Ӈ["Kue]矮uJ___?~)/_j֯^_uyJ?뗿,keu s;_Pן_?k>o7f?~߭{Yoxos9u|g86Oy|k8q^Aw^ׯ?__ޤ'ow_:_?__NZk àL7c꡴'.KA{Py"`l_73Njj 5λ+UZ0QK-]ɵ[SD6%jj5\HԆɵg)'_Eo@)-1Mww$lHeSO.Ŗ &bjC/aC7LwfCBTCgKԡ)6Rņ{D-?kK00(P Sc 1QM 8ZͱV'*2}\_8:x;O7*Zt V$%b'!IE!20)&*r*ޱOpsj1D{k^Mj瓷yt ӘK,y@i DMpٸTF=N(;X7JW('4E}(Z[j5\2My n*"H5F &/IQa2Q1:%JNTK0*D->j[$죄90vJDAM>Ө*d1|2 $aԡ( >ݗ.UzO!n0L?}N> jKb L]Mr -1P|#j1 Kz^yoCYIRzg* 3ڨ4ɖ%àϡ 9?[f[(+Z^њ` ա4т#XnV<{RlnIj[dK]IrnK{h=^]p cO%_*Qa(@)V; @*n9nnƬ' (VΦp]ZMFI?O53 (ov;rm۹nҩ8$`DR i^VN)_;~^c2ؐ'?'|ϽJ7?$O}.FG!tS>T" QCLFըp?DE06vB93T3{@ݒZhd>71-xAh &AU7p>\ȇ\ԡ8E跢s26bjTlHeEPK46bNAU"Y8~)MfJkyz`X0W{Ql` pTQ%(p݋ e!x: /$~/Cz.`ՅtTvHri)j(mKdUx XGMڸMJ7Sm+=-&Ј0`D3P)mBcattvIڅR=(֪]5鸬hJbEm zrh lpKrC|}ǯEy (S?=*@ 0q |gT@]hkɉJ j݁3G7.{Z⼀T/ ZNkRK.(D:@)r ECֻD8-%/HR9%jI 4.J#tzRZZmڇ"F2"1KOQZJ`Os @1lM*SCW? 60eL(=ogKҖ;5H_KѧRX(}Gr]J͓b%y!J!îJ#t;Ok+eSgXZw*c!krUNޑiٔkL©M;~^s[]o[nTq˸#D\1InoT*#LP_Rzq)1Y06(83}˝Jf @_s}{֍  ? LqjVl0~˫r\[ɥȸSTn.; .Ni{cÑ* T[mɖמ(-OaGԎҔ=c<ʟ,'$j|$j\G\DR+լhD>.ʝ%zij{XŶZa x^RLm}ȵ J`jLJ]qr:XJ3y3:;/IQ&K=4w\0;hG·C2Uo]u>2"FεZ:&)tS>`ɚ:t`jܑͤ:X0`J=RE;U?#[:MCr =rd(RFa<Ѐ7DL'P@ߢJ%/qB5DE8]wEuPRl@Q V LW4WTL]|tCxzד[tN4M&5S'0 ܲ4a[];N~ZMJ%\kwENZ._Ш D.BU$aAt]:IC~V|;ZQliAzַ}&Q-*kLCSKu)&~ؽHwq`?lqtPwT;JSrG9ž~SiPRcpxIKS[ؒi)>%FZdd`bZhG>L\{0ߕ ,:~/r N7-vVw5 @ejDET(AT$sK&ljl@PFR[sJxߔ:xǜno!&oKُW ">^t`N D I84UC8L%EG˽2DBUCK rV\ HvkvS{{Z&=#tӍ)n]V%lK yx.[Ur -0rH"Ffa(oVM{r>ɨņk 0d`sG9a7VS'A;b/Jk=^vYIy=@6 Pj }BDR(@_>Aj%ƟϯI;QD77Ʊ1\KR\ t`)<\JГO<6&9PZPt>wu)~진Վ\r/ ӷ~ꛦ|Tߴ˹k[)Ƽ'7`/9UICCRBj.%[Pf,:j~I,{ = %rG{S|\𽨵GMir !48SNn˭Mj-?B'yHJs;{~"Dz;7:Y;6Mv\'\:ȭ^ϹG?(^Rs·>)R=8FPRj-$C&C'>kfbH-rכ`=xO\9B쯅<9 <7NtTI506'j9y۷gdz}1J9^u8W}T3˯;út[Q4/n H!^+\=`"OyM>y c̿IQyM*+ N{rQ흇=)˸p,AS ht!G-He]5]5O^-T ߤl Kq)V>>#J+@A19_F/ Esjl,uy7eD~+LLskA>jJTܰ'0X-Z:n|E@O&Hy _nT=X("~M26T` =׬H:pY,7h$ֶxnh|`IMܠmi6F7eDTF>u hٜC})@j'! rC+Fę+L9~*n*W*׵^Hk Wq-{1Bu_(p?oqu,c氡ӱR|V4Tx[SWT+2{p6|uSݩFΗw'ob۬*݉׍HWN\7R,3'n 6';MR0;6) c2s~U_Ljb ?(PHWNǰ!F}ʕ>H.=N 2?UևmKNErPZܫ GScZйK#-.rp%Ъ%VQbht^{;HEov\h08N5r*2Dmӕ*ѼcztRP"c@_hCLAuꍰhDu9(rؒ-ITfMq_L;vOqMN?nUySyzדHlAb稝ȡ s}vv!|Jk06(ם]pZȍEW|@Ο5hN(E?|wlPP*1n0sL߀^JLS.}}D,}M( 'o9ޥ"èlu*Mbo,9z"7cZ$j{+7MrV4K>_Cc~)A ϡ jIJ'.&{\ DFAY.T]:jJ7[4ԏ'o+rG'OZ[ޯ?lޠ Wu67M~zף}ܭvܙ)ɪ!3븓kЃGq|Mٙ`1^Os[g{ o]o292KYBФ)BO&$C0Njy;3%mҶ xG ġd0AFNvEX0){D@f+绲1T( /7rW}v+-`X2~4έf^Ȼ9jj@r9K|R , )Jߊ{M6-25K>PiF螣IPQ`N*12$j];INmr†:ߙO0szf- ހye.~QbrނLj]rr4` 2DL*r3[UC ֞Cҍ)̯96E?o~9y"kMB[^ļNm!W{]>$VWЃ?'Rt՞5 ~[pTRSTsuЂ-&j~7b!QU6dBԨ|QG{e5ț3refDBO~gFyciOniGXڳc)8(=|YƺKa5p?chiպ)[Fvԧh:#E9ڭ)c4yTXQ*&WjS6|.9zUitv~.͑7}n?`춢,J?tIGԖdr]҉xw\U[6ligX/=>^:^oOy4e (] 6P$| Q%~s5'?rRHrJ::6Hg͔&W\]zd?=4`zQ׵$j*U54Q~jET_9rY4XQ_>sCʭMRadx0.i#S-J̾)r5{4H ;B]Nh)xy !HՃVQe`ݷ+鞙wF,3/9l9f[\.:&%H;4Lo!s4P.~?jؒ >%(rWU]ΦB.?/k ܅R07OۓyXc)U5[O~)׍df(xF .zLB,^#jׄ+Ĥt)ZWފ10YpcJ F23T1&\AqmI)7yCI, (`ڲ`vn{- 6'yǤbza a%9[P`hhPYwfʇ֪݆A(A mH3i(`J F?'/6FQj^֝Oeݹb*7<`;ޤӜ罛#[mdK9LI;&`έO{-)kS+w7٫8F#V$Md/= @R;S0KdQs͵-|`h(.vرcۺK69=y}LyԒ%4mKTq,uu@&)2Iѐ3O9z_ ڴafS{_.NT}cǷ +>U|1L҆\~cv4zMi8צ$a%kt&2%|2\btJ-XӳDm1H& 9a 0\.Gץr-Okhinh,9K65[&|%[vzkۣ T5 #kJyՇ#㹤ѢMs~HiPK}=h\zH"1I(:ؙh5Sjʵw s9 <2y\sXW99:rnk"1[arl^%&K)M)Pv]0m+Ǐ-~{*?/|FI{`ÓsJ+RoD#,=A-M"yZ`]\kyZ!&G/i :8R$hc|R8.vEKLv8I7 RL)\a?E 1'o9ZzuV( c q{Nb ]SIF?TQ Η8?&6)gvL  0/ڜJ2B,H :sBaxmF^ۆ7My^gQx7%a" ~*B)!uխ,,DҺ& Ձ9ؖt%wK}$M#.%==,ܯBU1I۔w5G:oHb@E!t7ԓ/ĶܝLA 9|Ǎ0舤J= Q<Ԗx"c%.X2^W?KyIJ4})'J_@J(eF VlV! DKIJ5'7 zrͣ&קW^=з~p}zoQ̟\V?f{Jj݁3#9:#>f™޷n"wLMч@cL^K?A}+aiV-7]%ah*0 Bb0? k臹y! 76 7wsKv}þ{p7nESC΍țI ͥ j F\$~"g=y4mT&Cys)TiP}?FDOmIwoh*K[M3V <4G@}l&*D r7c +q =gPvj_ct5, O-O>Tghú|b`\je/ʗ.e+@$Ou}>%憲m Sku`jCLG0hhT~xʫr֎G#Rj) 1:p-^M/%P/ ݊$ ؋„.UiuliU2{XD`Z(݂@Z(s mkn[.xy ŽMYҽ7ɵq?p㦵qgxx|7.Hu.bӍ*bU0FDK&f(obXʈ'_yVuv䣷)MaWy@GV2Q-&>*I:}LOG\C#Hh/<ү.Pz0ط5]n.^"7Ñ5X(Ua5WA&*ڊuyΔt\)ܺӥ1/c5>%6nj7\q;j`tRaH&_sאLM ta_PӥLcZM+25eRSd+p-ꖨ '=M>MOA[b%&IP^\Rt{ۮ܈MDoi%jMP6/eD~+{n~6eWO(P.N(7gUT;L<%pnD}UἁXA9ZÊI]$G ~\@0KkG-.x\>`*;]ҽs vS`#sq{!:SDB[IE m).U@_%قe`D:,͖EHp[jkW9r.-=S3<zrZͷT[Isw#B; S:IkQV7Ŗ 1*@H-ar:%}j_&'GEZ۴Hge+Eޕ"[S5NNI z\"W{?ޕ2(})uru\pZ[`rs_Gs hMr#{s2ܼ.*=| ЗτJm@Uút27al o>⎻KJV3oX{ ˱s{Dz8tt)f/=[&'G2.G)4L6JTx@0B{g-MS@<:wZ?=`*Ee*ྡྷޒL;Rhnc JM[hB]GCo4Jy%ڱVaT)q{uI7ʛ-ȋѽHMJZS֤xئ}SEUu $(o~fx( A1ҧ`߱ w1s"ě2 vC¨;NW<\ϽB6df^qٞJb^eKl:px>B}[ղ oG-da3:*d0!XN(h8sXMՒXm9]Kˮk)es?{u)̜G~5S!G eɶyIMDN4ْS4/׮NN']bt݊-wn׸hzGG]ŊcK)شTS~/fgXCӱU0ӡp1~1.cN0vRFִ?o[N/9E/@iD:^T3D){Qޯs*iAؐbA*yarj¦ |+Z}mF)&KZEFX2XSLEA JKc&d'!5^_vec-XC-^{!t. 3;xF= 05嗚zgڨTPnEG&p1M7tzǵc-IUC*rݕMʍNRUan- ˝d@FBwSL7=y^P# &9 '8-n=.9prA9[zd qfCRAW#[[$S,ŝ']@)۩~^'홄7i8];`]f[UILRNn({E"{b -J_dIb)[yFMG1;{RjT&l|o%Ǣ!-ۂ**ʛBW˝Ru˝RrHZ{'قyTŊO mvSϯEJal,x+ji/4<dƂnIuI*E70u#\sn'7\rBc>P*UɺqR|oE?Tp)74 覡 }?TI<3\=7ERѲc;h/-熹zw d˞0O'hnc~&{e?:֎e &'Jl]nVGۖlc~6KVɹj9[%ͿCB ::tP8M wS2@͆5?k- JNB;Z(7K (TbUm8Q1\/J-P$|K 8~R<˷XrٮB P"7-tQ$޶Sw =?H> endstream endobj 9 0 obj <> stream xkm9%?GlhFn;-uӈgS_"%*ݥ??T?ǮokeO???R~?_@Y2[)Ͽ~g?\ߟ:=?~qI9ZF?:G[[7wzϿ??4>{9s?./__>Sjg?~!6zU|e~t6>t F`7tK|1+B÷wZP"t.6h|kvaF!PT0Z04{ Q+ʂU-(AuIJD5[/ukl`+?.3yF)@^Z756G4CҢUv&ʛAƉ#gȱVKSkQu{ڛt#iY$-p=w{JvICo"mJD?P[X4Q @]huybx>l(r cR䱫V2~'vba@SDrbĬihJb6O-)%ق>zdm!-ۖq,rEFb{j=4B5aĆԖbEOf%? ͻ m7bcSs'-_h\Ϧwo# ܈ʊfѮRDG|$Z5,pWeL9/_P`^CcX} ICQ8vVeJ[7(J\꩒#JS_2QJg*p-ɉ=ONZLmXrlkrbȉMiOJ45v/E短[)gZKQbIC܃/yDƼ훐/=-->S{\Tc^}RT[&'Ikb:ПS026 %Nl$mɖ%[p%hy#mE:yX+ʀxnC!'6⹙2 ׸@6 ;t^1Լ[kH۝i5~ݶwI3s̵E_vC q=`PD9P53h:}f] @Z5 !jۏm{}ԸqRbw㎩yInSt줳$is iѱXS*9ˉM94_; 1ZVR!Zx-n(_`$-c cN"r f꒾.}2vleA!͠Wiɉ.'ֻFσSFQ޼/@K?1e(};v7i#4&) V$r[JǔkΦq"-4{D4+MY%6fu)3Xۑcw[>*ƱQcڊAV6A[үY"(P::CY9m-%Y2ͦcv-K0G!׷ePZ+@c cznAo2)w~. F"hLi@cb; y8MѼ;%H"[ŗfJ4b|E 2$ )!sAڃ:$mDNMI)UG"I DrPcu$[ y-N5I|'nS"EI$Ry74kn5@^%>y :~~~aEG"RGkNHU+}m߮(Rcorbˉu9՚ږ\'$ Mr[.Ch"t ˎUrm=BO:K1[>j!0OBy29\&?JQ((BV ͟&x!Tې>Ն1m=%ϟJs(,s`H 򎉊geG=1Zm-31 6-d ;_ݽ<1? LZ7 };v z v PS̘0ctFK0{WR-WjWsz_ฝVɯz.5su:*[(?Wؾf|r< [_yӞHUռ[C= HCR44-І▜٘.~^!2J>G?Ǫ%r:֋Ҙځ[y\Iϔ5M * -ZS* %\\SO#Cs\ F@Gb[[-~ &wC CM,țzH{mH!'9$<閤}b"yEgG-UMk.ʃ(o..CU`9Q\S"zbKOlkd5GSF8^Tz] ~2C#WlLN 9]-U tl=1\v׬Hu"h 9hrb_f(kJOʽ /(M6G̱spehkD_h(:Kq^zrj Scȩhk.ӟ̏nrayM`Bkr]*jF-F PN=DjV$r4G}47g{l?MH+~ԫ|SQ2noo^L >մ^ů7.zJZQϢ{A)Kۗ**h v:`VKQT} `ަs w\)¼~QJvL)MU"T r5%{_ec.'vX+y^v_DvAѫ%nL _S=wv;lj'- s;Uw'oyE*W]Q[{AΖPQ/wڏC?OcGO;cLoE]%7r-~{utmTyALMG(8o=k-=P\\,{%ƥKއۛKoEZ:3-RϽ W&߇X~[f`C=frEܾ|J,țUy:p  #&W?/mSNԁn1necd=gyaL{V[@꣸n(G8pP.=SPݒRPHs|>+|KϮwRӪp{+ [+#^ݜ rRn즔ʊye1\{T/K%ﮒ#T$O⑲Vcug׏<[^iq'4+-T\%][ %RS}J-~1|[)Sl˗C Rb["`ԏr[#["|@_S a4L^S>Oҧӫc(5UR*kKj?A"#,TljbVj8pk(0L96 E^j2OvdMHrmVļcSBL"o0vU)" ZO؟"goUק/U_[.vDyr󥊊jԸC chz+.fѩ0UZdOdIkGQ(_`:U[Uӫ rbg+KoQ&PԜJݫpGG^=>AD1 lIrxPv-I;oړ)8tz eQWG>ЂTBѤk+WUߌ өDlA ֋C)Ƶ?^+@-΅'%Pk8oj7Irކ;DϱC+`4FϚrK=2ZU:?ecQ$(иM /[<2ֻ7beǥMyLE5OFwA8G9Bc&Y/b!T~*qrnx+_`NTCT%3kJN^,Hk["Rjn4/cr=5%Tȱ?oWļQVW8n./ʩ!|UK/ d?%=I&R%-PdjȲR&OjPӻb7 U)ec bu%P?bv ^]׶-Wܾ_G.4 Eqm/lm3p٦oIÕtsOZi-GRZD\IEM +vq7dl-o5÷o$ m K|g}^wSum1(*; k, 6vh~S G$"g\w9U Dw#A|HH^#ou+["j w*ShAqlhZ[\ H]r|^c,“V9E+u˙PG1[c.MO)$mLTTe%'mX[Q6P\ %a{\#|]{j'"r&/95g:׫blр'6*Z($m뉇HZM= 70Ģ#|bɰflb4cԜII|V6zC)N7BÀL^o so7ͭbi*}J.0%(j{܌qGE!ȑvQJm˱gS<}2LMdj ΛBd_=1vJ[œ6 ŵ6|N%|mrާIIV*rg>,w^tDJݤo鶔)=_ܯPO9ZhNh(颃^F#S?L~m]ؔ %;ete|LVuLQ%׬e;Fd].s\L:KĖ-SqOSda[5Ðü!EB*?J$S Qs,ujM{æ;&-(o.y dⰲ 17_bTzи&{uﷱ 1G1O g [|[zi*^2^P;(v]j(! X!B>mղA7\\GA*t@DS3M9i O zh\Qdk W7G{k$潋FRh\<ۛ3]_nrQMjjSfcPNǔ"?ʛ[vn6'mOܚ]5eW90?rl-t\""(SM,uz0zI_oטy@3Tǖ&8@)akVu8S6XpXU sCOA1;~%WAIpsN-Yw#Ǎ{xۿJ >{I'B>nLŚ'[QeSA'R+o`~76!jVN>w3}S%9S<@IU=aZѐVۼ nzUʻo) nh]MƔȭK,HekOЗv5-Ў}$ֵ;ʛX=jbKּ9zjJkLj_cO=zz7E|I׾ ۫1\|sT+i)nJ=rLL]@ZL Қ#+}sw,!UV&B)UR>LI#S mz*-I;q"\Ϲ6u;1y&-^0PjFVzF7R e[TT`vN @s]3WJ(H7o(o).:9zШ8nLZjFdKR5 e)^Vjq=͏>l% otBZZPl(U=IH&Jg(H&o 3-ؓ3BDv%] ΄IWI*kEM瓣9]E_29GMGhY/\̛ j(S~YBLtt&K{64B|tL+7"6G7蔳I~ t~i CksDg|$Zl[3`M`QV4(Rԡ)GUIyx4\sKG*߆SܷLP*B 8Bͷ:"; s?@c2շ|T DC @~ΛRuyh0ǻqGb>9:\"Zyή ňQ)SJ-0CNln|59-W\ `z*+"JWDTd3 "@;R uhʈƖ+@ |4ฏ`EsL$,Xh SӰe4Mw!5xma~{!7",R͇KbCCMyE ln7B1 L]SI˧=/R P`3 P/V|K>h3D6U!_{qP 1z%3mT{1MCsw&%71ĂE]z%sԼR>ߢAn/J=|b ʧ86bRi*z H&A'֙jb=X(=S-|=R:rJ+w]`G]=l cR&tkǔļT+{ RjPbjb--Ð/cܷ xz-|W9-eҧBL/Xфk(mc(D]u@3dD\)c[2-2_Ȱf5tV)oj߳ɥf7$޻ uKз.h$ 0 ˕G鸾`4zZ }Bw5aEaQsBԁr%i(,J;=$(/R$FA$vpй鴥O!rMyS1!r 2 =$NX1ZB'z 1]\.*zR|IU.-&JrSR`Bz0:Mڥ{L>oΐsͷ/p/5`|*Aޱį:22 -`bmOů#l-v{Qw1!}_;VEh: ,{CrTCM(ࡩPyXʦY WL;]Lf@H,5oW/ht79Xۗ' ri z'@7Nqܼo_/h\y*PE%G<s`#/ s\PP=wE/JK>L  .<BU-aR\;]v`lv94q׭\/0=,m;^FM]㖤LRM:DҖ(^$&R&H;h\y Pz T}35x:ZkܫuhCS#_<. -/,DTuG""ި9 4|4[Z(#%Hĵ5kɓ8/<%c˥_w筰(w&]F1TXF\ Mk~#!w{!רǔOTC`JuGGMlbɹKUF4o}ɮHиҦ`&Зklr%PEBU ż`ܸ9|wSR8;%)w:\"G Bs79eOpSfGrmjCwK_0=鼕 MMPql(P@7 R6vMƯ侪ԡ@__f_ܟW)=ɹ>g"9&]? V~cɱ*4$i[.Oz1ŵa:Pq|&8Pfۓr._ӫpҧt|"5uhuSXf0P[D(uDS"9pII"bUZw M}J"?'mJ[̤BHפ$mIפ#jN%Ď<&H=d!*gw_ZS]Pj P J]o(Pj;׷/GT-757>x}|ۃ܈AlRv&O׭i9JuTPe%xVg%by$[V"Y[ҊQh(4T R\Jq"<=;׼R+D~"gj BV *IR$~MZ:7KЎTv2bjb~sN#]ߢ"I5GcwxDN/uɵu4*%R%{}(OlwOפls`ߔH3pXG IFU HʭH-;FRU.sdha(oi0;SN̤2b&/͖c v_*l".=yW/&וe-s5$zv3lfw9=!TxPd\77za7jOxQ^,jjZ, /7cX M={]ʠkw0uzdc*'LNly-GOYZ:^J9U+5@+F; b~!VIhbF=QT!;$9ϭrjZ+ļR܉ F'Jn>/(ih̝$f=,K?|\O&68$h9?a7@t{0mڦ8{9iԩ罺o*7wהo ʍow4PӤ] ΆܹZ'Uto 9^pK{{hﻹ@M.P/>%o]_GL:nG\C:PVB,kYJ@u pmjb^r\ji7w`R6榪O]N.N9%C4A|_e&7ml!ICB`QS6( 5C**ٲۚG3Mtoě/T38숼I+'rzI!?RccHGL}۸ Зq @醻|c]<̗ dT0_z)o?%[ ;" 8>pfr&#c*U"U],УKc #]ƔX2c{yV$ϟT ]2vzmb,D` jL L2}WK~C]2@_A~ln-eЗF9Ux\WMEĀ&N5v-ieTJ%חڀE aW.IͅUQfqݚS_Joh4jӑw5-4iS4x#{os5=;]F_[W 1;_(3U_ƴ]hUQ 2]AjЗ򢊞qb(@5-(b5A@^%(Y\e2OSmEڒkP:rm/I'ҹZ_rE*;&H )Fe.1./[g@ʛhAڙ^Pר[,Ҁ;:RڦkU791?wʍϝr3CjKMtI,-s)rϭlsϜ\CLJ+Kk eR>󿉫ܿF2<^7k~%ӕjZk9i;@A3ۋtRI.d7TtC84r`DJczEìRf&e01VUdx>1:]{s.2Y\:P n(G4nh"U/} ChtF 0ʺ[(~6ts~9xk-VUmLxAN/fӬ/Q jOuC5ݼnrl6߼RV L[Oȉ=9Sə`zzIm0o=J5[ $Q )6h׼ߞw%Er5f.3}@9?P=$7%'Ic+S=YO*1?׮3|FM9l9u/ >!*'z+|b5nR D{_j}TedՅ"hT׫8WSAk(d${-PjЕHF^/|q/\`8) (zK?5:(dʨ|+_LH}RT)G;MQ"Db!ks闡/")K ^}W6 Rfb317oP:i7/lM}~a e]a0#z<76(*~ TSO}CN򂕋%qC'^r}MwwW[&4loIڑ 9[Z $pYɯ[Sz4 5!y4)@77]@?JOĝ -='2Dz+ymv+Zk~ Ԯ7騟;:Z\Hb<ߐ-& FYҴ߬vG䉸S ` h`r@T6/HteO_OcO`2__&ꏫkRwlj4w*G7 rw D_Qp 8E{hU) $hh+,rbS: a16ؿ~V+{sz 4w4/LhJ 1Ɔҳ̀K%Z7e(bnn5oplXdI-R'кTΩ0+-e*mrJNW`oKk/(?7K ; 0r v HAJ@ے-[Z[|?y͞n(?ps@k ;``tsp+^jܧ=C9jNOMiU=J%UVz"]*Dc[I,s!HQD)[鈜rS~rҼwS_S>hRϛm*ksHжPNY~3ԩ>5~ܧ&Q={A4q)~: 6m(Ŋo^> stream x}.9ny.̌g;<@q]k7>sIxY"ΟJ__S9(_7\۵~{ٿgZ_?]Wԯ׿gϿScYG'2(-1VS$9/,S{#l)jqZ (wUC#K9M=&_˻}.bV׵'Nۡ6X7zG %EYպ~3G\jcyzpj555&GעR=:z &\zǏVh{yN좘f%{yʰy &/P/u׎jEfVŒ ƾ"'G#k:F~fAqKy :Fӷ\M7E֧܊|q6@ WpºQ`Tm :9'78)TNIҫbUKm] I*I%dDzU8ɹ%B~Nj")K5(^cwmRj7%U3jVЪZ5C딂\tsjpᾃ"|~Z6ߊ"KkK SC e{ !F\LJ76rUƖɍmM5T`Q?҃G7VTRrTؑcG`tQ(8X}II=>tjj*Y<Cp0[{BC({(٠L>ՎFhQ~Eb̻mS;ڒBrPh+^'rxI? ȡ=w_rmєx#]RcKOMj5jluZv9y+rt`-'CcۊBa>JƵCqQohWB ,A.IO!ԿUi?y9j`cnL0pEԆ)~7mxwVoTW}] EXIxdM%â<,zXǖTMw(Ԗ)~-'(%1"in'}jc0ԺMnLrjtg.9zM6j(yR<>u CtFn5%Cc^Nbz'>q=1Laf(j] ҙ݁1)X@ *^pp JҁRbP 2/妊KRo3,5;]AFG\/+%~ ]n5kr6GPr^F-)LǿGPkn,~ľ7 hoN/rt3%L!L͊Z3H:zZԝ8~2kK4KQZ^J[W$ͻwG~stS^XrP*FE-ǔgIMxQ: J$*2|ͫ}[)Jlo$ےkLzbўST4'7ŁꃷԱqslց M.6{A $`h<`f&hH3u qwKW KyKaS&pހyR`n+*[b&I[8^y2ף啑´2qbe0 Zjj cczd)fuc[cjexeeXJq)Ŭ482b(((_i7ДOdxQ/SEV v%NLl 5SHTmvi0 c*m oW苝oW폱EPJzB9uRWX/]0s vUXOa6QV\EҀt7$K $(jA(( Q˄ɣXH0`^*.m9VKQdMS݊j.}>AnRO'}P`OEfFӧBi mZCЦ5i mZC4454~ZC;jh]r큓]Q-оqrJ51COKՂkcT`X lLۆVѦ0TC:M\q-(MQm/kWj0`S ==ןD J`bJ|(:}Bƒ(Ohd X>L  } Ԉ2djV03yWX^DTw8,H/Kf2i6l i=uSb>5զڒZWT*_Ozը sS0:NK?2Dh( [!764j0PK1jq(TR[]vM 6Œ 'z}%(-QRO8N^EV!MC!x[B|'Io<)y݅=y5ډߠP/X!Dcy&=G|H4޺KOM8&Ie+:\*-VCXp1h"z JRMo E$*; "G{BOڨ:|$=!a׋xKTp}ɍxl?rn %=u c_/(5jk(wUX=U`Wt\'V(Ք]3R)?B7wn}P>FM]M¹y%O /sZm*YRz<:'ꉁn^/in,xQjɧ~Op STSFm/vCSuTJa륈R:wҍ5j&L{Rm=},\$ mh]4BRPx[ڹJ֛<]{C;tɱnݤ t:`($r _YZ>wsS̿`:q#}_?H L둾g[JN'8~÷'zZ<C) n0ZX).4s>Q>f1slXX3ϙcn[cnŤ"ќ V9yc+ւ|L)9F)BNbcPLMϮ Npt-/)T1,|-4WKw$jmѽILj%Z;C8 .P4)%lY_Ȓ!аI9w76߷ύ!㒨|J5"Zd9uTdq?UUHnr?]O{}{Jޞ,AlY<(["xӷMCJ ! %Oh%7}}xʹʝσAj`!NĒ/ Oqb}{&'_f _з$(}}(}R3k+,#E[j݉c]3UaE ;"©# KQ{*oM 6MMr(M䆗YDjLGɈy\4ʂB[i%;^v!u۔\~8keb)7=QܨIYp/A#[Y].uKA/침zFWi(C;J=p4}B7%=-pږ 5'G&g?{}*l0<|GG&ӎ"N%mJ%zGγi]sAV= ]pGnK=-O:푶 88l .41GA{=C쑻 2Ze /xB/ɢ=^8 j^vO(I<$`z hzpTTOܒO>4&Uo(^K1PG})aj]3bwz4Dڊ8Ɗh-BFZs_JjItϋ<:'}֥zD~ T&7}ު!ruo&͵0y3=W'i "`o֘KrbuP3Is۔d}H6Pj/7 %96.X^ȱs"#vk1cdw/.mمTI@#/5gٌlȗ; f@bڑSs\rt '_Ǹ$@Xd֣MJY?MUMN>2$J|O,|#ͣ/xUz %,HI;JĞc~B{y[r>0||m" q]ǔkD{]/E[dSiF9YZ0ԉR5 .3d[ Z||m'_0coJ`~yV㽧 +q8{Jކ1oԞqsL SMzaɒ*ўs:T8=S%,Cr:4؆|6(Z/rt7.͞ux;Cwr =|SJ'1Zo㒨 zEsmTi786AzDKu7@Yc&eo؛/n%&<Mn,?(­O(XzF鞀3e SX}}> Ŝ պku72Ȇb=G `})VJ J7%ۏhCX xW9lKT@O QIYR/x%WRKQoZ4CkPyY'1KNn]N>| Kj/)uO,V.- Ł. n,z(UJvC;Mw%$җ|p©jIm%78^6d(`S 6*QҨ~3r?SR>;\E侵h2K\(^چ FxR? d{fQc&O` &.CM5T63j:4UnN0)YF}S"5t  4]v/ZW:n++7s P"PhXjWKIy@w)J~NǺэ$эޯw)b|)?^>89&1GvҾDE2ɍ;B)y=PZd(eB!Citzc'TjݭAyY%{=9oL{ycmeB L(ihj krr֡ 5j,P-mӒZ}bG?LExYR$J;2˫ KoCľ &/!W:`Sc`2 ~*`t#瘡M%v%Vu0_XH{nxr.F.xRҔ?<+&g(hw瓻w/@4tQ17F<lH0Spk?$y)r'9`,y(u ƾͷk]ln,5z9vRwhoɣk { O7"E[Zbtj_~pcrH*_P4?%*ʯlnS٩ .9}*Miwɯ,Σz)"(&7zM bSbnoy|(SC$5h;^${*mWSd4k/0Q/SzSdioA GOD M)^kѨږ֡#zA"OA)$“,[teqIԐi(QWi[! w,pT.׶*,:PQͤS3T.9fA>DZORN2OHdJM6Хߓq޷ryCおc0VUy>\fC!}{l :@c[.MaU.9vHQJSKbm+rS(%!XelrQ:B_$K6` k;GId"5RD c*I|偢H=տ$ɱvXjhC;M{W> =LN^[ ͱ68hK rR;2ߺɩ>%5~~ k9GƼaosi=.Œ44i a%5Ƹfk$;4=Z]~HS b:#dVs呏,CuhJhjr퍮lH8.Iq!YCd k&mhԲ 0H[V}ߏRxBZVWa,?v)V&g]a5ah&$7Z 3S6Ԇu-?~cm~75-?LMl?~w-Jyyšo0}VMKD%s(I<`* yܣw"vҥ_o򵝃7]<,B^ l)>7 9Y&;LK/iuN䑓e_JRn/|Ĭ2վ"'75 -k1D.k^].-mHAfb 5^[.&Gj1_/cG.^oWd E콪>%j&%ՋSJtnrMi[BYÇYs?o= !k0E(AyeMh TCUϽ(ݞP S{K4 GԺ}um`Vw~-9v㚊džhoKnKK`1%su9yld1."׈M+s{;\@m.I5V]r^o+'e)jr}:OAOQ,\@\`n6]!Q( G*w]zSLd vK~| ÿ29jt0x?!kU cCSlouN:'^ho.L7.H`5%^!)Ҥ禤fUbS6[%OYNLa>ԁ 2)a?Aϗ $FzXr$Jo ʽ==jQѺn{xQ8SS,Su1O]Hɠa):o*p_qq 462n$g )^:cݕ>z[|b179d&G݊yzY$K IPNneC19 K'jɲ5jLa5"^#UVoM+ߥ4- ŁF6ѮM4=mݯ>b#_Z"S.6ƃN\OW;GJW1T1kJKf&wj%Uy~{C3d(jP L34CmIQ4}>ǵEcrz%Q 7dr߁??^1yqgx'j&aYo7-} ֍quNFp`;bv|b{49EʋۅN}HU,+ CRm9Da&Р)-o(j,n\9:׮/%L=7EM`G'8pm *QiAq }Kl<$M{CqȪN\R͚Lz7w<2M%.i=KL%C9\NT' !J??nH<FV_GGycUWK=$a](ƚܑ 9ę*ƂRq_5L|SS&SY%Ge1Y@n<|t%B^/2ackS"=7Yj`0<,8)}F'H܆TC~_r#KO%RkT]i;GwUJ?N. fib޴~Bh c"/?f.06ƭr4~KspVS;Z@(L̬jSN}?R 9Kd2wwJfzߦEwƮ1#cvNǍZ,MAv3x(?T7V},< }G*L##U]PllHaS*ѩGǍ,EB m11~BR3ZSd99j][wX*1CXӶn{ɽWnWNoƛxsIu)Mmk"Q3 ``iTA߂r-Q{oI57Kw oMŨ㒆k _{T{7 v@)E|U~D.GVT@XqxR!Pٓ| ;%u ԛ ²g~m2$IŘSnlujT9Y%ӿ[EmViMٓU~EKkVѨD)Eӿ[h+rxaInSVNsWK/EݐTaENnf6dh qrD{CϞr$]XAR d%/^$IYÜc*PʵpaJ5VVb:F7)v 6nJ(z no)X3dh=\rMT~U{R%|4 rU37XCQ`W?tΦ4H>~ ߻j |~ߵ3H^윺# TEDPaz=Qcwko}9Y}Mɚ[Ue jԱɲ &>gdA/׻MIUyi IPC%@3쒨R %~74}TEZPP⁷k7/jߴRX[}HrdMdI;`NO_4S9K[ m= Utw#jS;'yI4ۣS<Z@@Ǜ!LkJz;5B}J%eq~sbG.PkO(}f0R_J٧ +uG^CT39>1РRrVX< fXպW޿UE* q)Z 9*_cWU;ZSa/=ta19N׍5Zdxfjӥ_j^Cc mANVj%;d=P% hc[S_JW/צ d(eZRYFWd97)ͽB6➨"N.(.%5cÖA~C9"℔*'ӹH֫bQC`5Ŵ*Xڭ-wR6IRp3؟עP''j0?{ QFd\`4v0Ԭu)<7T8!mworo̪<LQRY,Di9pLJ ؐg #r,.4M:$υm~aҋhR*8uoDaZ ~9'n@6f O(O(U-#w0v{ՔX-*^l9Vpl 8Zl=`*xt|tӘwy̹c*:J7cHB95JWBՠ[_MDjZ1k6i1%fT[Np$C 6&wodi *0_,K%jNS*- 1WM_j֦ 11K?֑ߔ';2둡7?'&&EEj:瓣REƮr-É RsbTZNl'4zIT*C05am4c Jy!) B-95% (Ҡ'MdTv+ה짚Xo}MZ$jI4M=TxJj}+U#62dAeki,)Q{Z;ذјAub^'!LijM8Z&czn.j`ưe$ =|]$CԚ =[?|cƨμuEӑ:ЁzZCIKC##) .]s1 BɣȋsjUʽ?8'|/h&=(ϔʉjfJ ZnB6qr%1ӂP[6mlKus{Vy d(zk`cVVJd"|Fs'A7&*X*-Z] jcCWiObރw -5jrr4TƃKarT“FOyxc1٦hnPr)Gi6KY4dks'dIK%Tpn+cIe϶DmNMxP=^IfzmU<߈ݘu}@)\7 BvDz03vkB>cXmR ֍=Sx\B̹X>crj BAS}JXY E.v =&K|i*QCM/{+s~{#GVs jÜ 5;Hc V7Mxl!6Dm/' CտIāZd=$߷?k(0h6<%(xEIjÓ6ց/j\{ IQmoűj^J&Go 7/,K"3Ip=ΔkO5[O%Q)EHbm Lu{Sd1yjV%Նr X|cY.tUIب%s (tjti$˂}R4?AJk}*ӬrA2ܔ$j&ِd]e5O|<֎w9[STJ4\{Q  BQ;0̔G|L6;~Kw3Ӭ 24`hS)#J5*/DX& }3^iFwCLn 1G[k59j v0JfGUp{Qot}Sş,'~K羧^w{BٻzBcJ7pPnPz] "P 3SLf8shh~CK25nsl뒗zɤ V&/LŦkb66'8^|nCi Mqmjs}\tm/4>w<6gU]U3jPz'GR92QdM ,G] ]zsY65+rm[^IZC~sB4=ކ1:~_Rqږ{o58xF/إJ&q1J r i OPeo[ɹ]R-_S,9Ӎ b&EbޕIQ<}+F4\4N[bi*׾"@iꎮp#%[>Ij ջg=ӷe2HPr(LLN>a,ɄjGEy~{`fzu|uOXr#/ +!Y\ENo%$;yT7j&k%'w0OP/%aNs+DtVӊZF!PRmPYgkO<~:9Y6iD%hC;X0 X  j߂j[Z65~R;CJ9D=CJ5/i~"xzr}uZQ-`փ ƪ)mj_ kԵ&sCH̽!\XܒJW4W Fp8==wz@Uж=4ŢV")z%ms0952Rⓦ*JےZsŒ|c[zqwGQ Tʓʔns&=>>(/`rb/9+nG, gltѨJQAޒR Q"˄Q(7No4ѷ"1h%itdhn%^slM?lu endstream endobj 13 0 obj <> stream x}ۮ-ݻb@K4n$@:dM>48c\9K]DRu_t]׏Uڏv?EcmO_e?߿_|S}_aȲ]W_g_|w5uןu a=G{oc9wu3d~8[ ]Inw?^Awc?o>?z_qoxcA֞?> o_~13\ˇ^Q ЊЊ߮]' FFZvj8rʶy4(`5Ɨ n,9༐WAF) գ(.(]\PҦg0sKYqJM꒰v<ݵuA]\*RdKmRb%bn@X_kӭvH,$SI7z(= /Օ"2 (En ޡ@i(P"Lq4MT8*r%Mm͵-֯h#CȽ:7k/9I+ r)/k KMG1hPb0+Zfk1 +p#);R-M,9#x+6\i%HC^`{xm56V3p`S+IP5ݒ;F84 d~:. sx0oT \5pW1ȪJ> ):.U%u=msawPD9J=5%~jdmh>L1i*kH6x ^p-!zX>k-Zt4&=zG%0ћ )@&NKGG'l-{"C gog-"pEMCJh2ᖅ59AӔm|YG(K&E}4i٠|ER?ڰO oSIs;_g_[@kq茢כ_P: :|n-")-),)!wUN[sZ:pL6Dm?eUe IɾZ7e9 J||/%XQZJΣmrYz)u.Оx8XœR\k%T ԖV4t@_ ;MZ,q-v=)h@a-닒`o>& /Ehjh+#* C/J&"Il(`O=VtPQ-Vnс}n& 쇒ͮE¥>N`Р> ,x7w?L(V3GUjS\4҆-}j}*}c&cE ES6WUJZ݅>N u+Bk3%;pCyCs<^XU4)Q%QkFGe*m+CO4*>Foͅbm5|0)6IBc쵕ԨaZJw&IZGI_|CI D(xMz>Њ>zɯ) v\3K xkoD-42U`t\=imiY*W|H[XT_,l5~5ı4kL{lm֡ PSvK}Q[xշk·Qp\=,N(r/G1w-w#,Rہq/YP?u5M25(MBu6I$R_Ps߹bWPK_5RtGRjUQo=2s/ٕ] p(-h+KF?L7sPz\z[\DE1z[?_S4;#؝o2;]po;t\Zki}a0%̇h%Swe%vԖMA3Pn NU*4ɚ x \^L܂? CuTTxBeK.TK.5wn4[Fno{4rGa`t陆>u,N(%n6.#r @iQP8 F#7ŌԼF~qEѩ.*LA HjV1H !32N"J6O\_>OHs..6$rYfO\[UvAqulM8\sV/DR $:*hg&I1t,qaK6L)SMb>Dme۟r5(}rP"zN9(k @X<hMQhς"y8!2} 6V?f\zq+zu+ryUojntؑ-qc`zp?acMMy#a(oKH/E]]an\?587YЧQ}W<7tJM2BE4-1ih!b 钪hcRDž'9bL<q7)!וh@n&@cK+NXh`oü˨5շ*jO9v KS,X(%QC13x8bQ(ǹ}-IJODERg sZPxpajw7 jQPyiQ\9Mr6"e+I(AnǐM`,?(`ڗLn~sv!OvGr ИSAM]:?I )A&IC<+X>o9{|%wE)r r$;:C%vɹTx8oM~=.>L)rGy k<ވajEpB_OhyR7ƢHx9A0KgNoR%Qš.}QO ^q55Cp\˪$7oP)z)k`2UIɨY%%KwULnA5PRL _S!Mw樭2JˡO`w*r KA?C7'459v' Ka4DRoRL9vA!sus.{ʯ;F[]K†!ULcener%[6(isQy-Pe+VԖ5h mQ$Tӝ--cHK~={.LQaX 6GEZlDžɜ [R51T(_3R5Sсk^H(S0y7 FP$Xu|g;o\U^yNz/v %HU_`k5 &ݱ i"Bk&WC;hsq6uotӳ׹x2|2HOt ا1GU҂FJgj^WJ'/y'L5[ͻWr4V} 5X]t "n88%%;YO;bQ,?evU$a׻*ˠwp?SkҦR\)jzǔ6ZPMj'ozs'uK[Ն=U+VZ{ѲzCp)^M6M)߈4S|j݃K5(/4dK̐j[~5a 3=/wd\5tɄZ,o L n9.ouPkh45t-KWd]FfЃsε@i۲5(o~ϱ(۵5t4xj 8E)uMyc[^$S_OM҄-=)K:OXZGNieE?O yޛƼk̻djDžƊaKeTJST]6RŶiVCXP%;b\xwjNkI ] aDݤg@yDŽ>2"**Z\""&hCN.ݞS ؂So][lJbht[b>@c2Z(]H)Q/%d; qG12fh~[m>._'TO'+ȟ8X~8w@k9(DYOs*E׌ $ڍD҃cܚZnϢ}N.$p)ݧL J"_,r:| @iG|(n,dD;*-(PbN(FkVX2*?MJqHo|Lw2c5Q:?yLMk[-0@CouL^(z>CJ; Bהl z?Bu|7[XUp5lrF(2EZ#Fۥ++Z/7EM^ úRZ0% %Vu)s5I\t"eԶLνA,$uEJ"roRwU9xmGZ)z5L$CQr1DtumIroL(v5iJ쵢=_M/edq2`lJ~瀥>1Q.w>E9' З`h4P*RXP~TR{yB*(͉'V@Wū 8sCӥP,ש0; 5 t) (/ϳUhI5a9m,{K]($F/E\Uht{Fs8PpoA!n0"qfTd0S~=FY‰*ٲqD凜ɯKrm\blEwMA4MCCuxI yɹQt^Vȵ(j"GG]pр)j+DZ:b[mMmLf`ꂫ1_s)913.s QP҈ -R\zdѐ \"jSÀqnz]J/,d[uvWg6y^} w(\(|cHue{^x"{8^EyPt^Z+sz+]T==1WQHbJ 6Fjڔ Y EbB{Pxa훎Ѿi_ҤzűjI h$loӋH!-bܗxwZ|yc҈!blJ+1 r &u}B+)t2%FDx[Fȣk̻)zu65_ 1'S:O(h-OܗSk@ot}pd'2gg4@{dlp<((aNgRPN|V`zb*Qy*17Wɖ1˼0Ce6JkHM5ν}ʻݒGU{)-Fp-[3ZBOh܌)U'0rm+ryCy wCƇ#ݳ*U:s i708՘Jgz]c};OWnB=昛nP$Us5iJdt[)gqTU@ۥ&W"CZ;nrs†))ݽ9 .d tXzg\F=vB_#O.K: }ӑRXy<ӛeޔu(D(r'"o4)gvusl^+4 Ӫqsw3%092ӳqSi8}`NUe_-zҤ=ry{1xzƹ:B!,}siRyW[r"T)2djn-%JEm!֝j~P[!PLm\-¹4uQ[(SW>_އUG0t0Ԗi{\z:bы$znkwߵ]c=5OFDI Vh?~}vA"8w~ޯo?1Q?Pڍ_ ./@)F~9|sgz@c()e֮>`v~_a4*/m/<~Kpo~Hc])QE2y jtH:yD.}Oq ^R+kyLɠj V ͻ#VTQъsoojG B݅|CVxYw|6 Yuy7<\vsM*M C*NJBNe7R{Z hMeT͊b7:DZk|Jg'V[g*̼-~bE'5uN"W@+groK1d߻=~%07 A֥T-XTo(.j =ꟅtS3#Io[7I-;G{,A[lJS=/7Cu#-mh# 8w4j:Bq{}Q.Ѥbn\~G,t/Q:o-(THy J=[*y:K}Ȫ@6U #///_`D/橑BzWbkj-pIǔv05aD灪Իپ>J1kZʌ=PKKm*TT.VZJR?&FlU%K$,xoN)pM^$ݒ38מV[߭ɭJWvRuiܦ$؀M϶ MmJmNb];Tz[=.';~AߺЗmԄ P~vBZR(YV(귦 Լ~}]丒=7 OL't'.mI=L|Eɼsнr l܅53!qaCu3ϪP(.%H4mkh^R׼Cэ -w @u5GEQySsoKiB7hJ|0Mi@L3s긤ck.:Q31;ހ<lHᴄ MH`dSDuj8go9R.s|}VXqjjFP+FD IC75'ppJ2CJBAa;kHԶ&lK=OZE Ts{| Pz[Zm y5=Е:ܛ|p^ZʈI̽bjČ"UP9j,9wc<毨w-TH fh‚2yF=6<4ZGys+WSqݥסTzUO9ݽŢ@i[$F5SZܖ6_:.ϔy$ ͟8 lJ,5FZKu ULZKnKj-T!Aƽޞ-wH:|tԤ>uDOWUCD6F!vB40rFNێW=R$[M(au8wRS mGSW7Q59xSfOٖs! $lj&E5Z͕0ڐ|y*]}޶ICZrMux6HߴHej[{˓e'_wQ"f+::C6<tl1M@U78dQ'=اc-8}t \ %Ƈ֥f҈lH#<2Yh{ʥƖ\j W"Lǂ wꙊ#W[@N(zP뛲ګx{@I'N(A1 y 76o_ [ΜeH!p ӤV G9w ښGAY>#>AM ^!Sv RW7_g܅(¼M:#{g)Mj)tT{3uQQ/sT|kOyZ.3֚R;*?-7q)mcT:kOԖx5Mؿ<@ߞ_{oϯP_&wV~q^_;/ϯ#<@# SK5})z7~Oz.dG\C}Nu) |<I){[1wZ5a&%};"W|9(m^z{LKT4o]_rc\.9wsaUu~0=1~hO5 Bm.e%%Į-tRHo1w"E1wG(Iz;Pb/ҼViscjO6RnpWN%ܢ㵢0700nZC1ӛnEI[:Sh).(P[ /s>.ޑkƢN nZeي⚇ ng\ Q-B Ue9Ͻ0<#Υ}1wR[<<yS:PfSfЖ4"B<0k&6.1bFR̋凜}s.MJD\:BO;5kb H ].'J6(6^VӁLbCt`ᖫbnTJŐ Qybp\fS<9V۾Wu8=tNS# eu[x%y)VM5adzq~W<߾>-NLlE)1۝k2\}E9?%-UP%j+[R&toT~_RbMbOs+=!FywRcɦԢ VP#*EtRԨ)K[1ջQ֌*)(zl됉VD*TѾ] ޵RN789)C͔"(ozN M):|ӣz]iy1`TJ*ڛd?3Z羊Rg*+J׼{ePTCya..0 l% Z/Jކ?1J6baA޴mYr%c"RS ͟oSu}%S[E`K.zo[R3S@xL\4WyoUoMJF~Fp1p"WüEqr-z*)"R);!ȷ)M.zΠ~q$FQwߦY\[&fҮ^K2轧y0("k$Rtc0%:C k,r{k[Z }8E^ZkRb t?[|ptԴ*4˼Zt-[G1B}Wp1T[b/]KJXyt4jCm2N\\BaT&{]k*U("^`n~xJSYbq nj$p 1SL3\ L P{Iy{ Mr)u(?cߪ+~'RBR~(5# ^ZA=[Y\q4cgwP,u<1U T7wG0 v] 0ϡ.HUNgzTAm4.d[Skt5}% NO=!1Fq:.KPK&TsUTasǯ9`okwnT- 47?ʈ:wyP ކw@:a*j}Ǜy)]k̹֖wþ:9SR\P ٥vGz9eRIUrcȾO%nreA[:+]|_]uݔ[^?RM<$BGfK6L)@L 4[:DB[\ &nX 6tI] ,T%K+ٺq2[Kj @cG9ZE1/4qp1%kRs 7T< Q¯.,6u 64a ?o祜? ޾:xh~ʟ·cDƴx ;* C5w%=rRt,:э%Q5y,䚷?ХvF!菩g$FI\M9-QU۔{7I/ɖ^jB;H-6yż"Q9՝vL[i_xm ՁvLiSjV%~e!^[FC ) =b%yKCI\;'w=ĕTM)by6"6*.0BF["4?ii}ȱPh(\>P^aeA}K3o]&? 3/H6`YgJ:U)] T~󯴮qskV{1/(j.MiRa\b\<˫@EXNSL㙛2>(07jIݫr) 0y!Q2:{HTgOhYEDt輦zo[R'}o[r%I ^:OE_+0ң64jvkKӸCeJ]k|7=9St\ ՁSYgy<︦g+{r]3S%D [P#KzJpmko *jnłZS-4ZMThҸ0{frE?ck-PJ<ׯ?$ endstream endobj 16 0 obj <> stream x}ۮm9n{}W~j?;h yb Y^(W;)JeX?K"oZjZ?~cz?_^?巿Ôy_g֞R{Mg߽j}w~e}-??~ ?;k6_½5{ݿ>n95];|26<_'~ラ8J?__/z^z҉?_~<&c4VWm}󇿮y} 0&ywyB~dX^V|InS 9_~B2EF%GJj?G!鿥?^O{['u/Fy۰%{yp]|#C?f %cı^g@4Z~V+)n,7RA…}0KbfBh^Jƹ4=EuCNފdI/ɖמ9aXOn>|hTÒwCX͊9+.m)uxY=.Ҕ$&bN}HѺ QCyFq*ee5mEw͒ZPt*Wmq.Uv<M(WzJfLMGdDs d l h?Ķ7WN ,~6O([;DrQcGMG>9[]?+o x |N0,'/dT<N^dZ5yGѶ}GęglY0ߘdYbi /'cSPh[6}b]c|G.>s5޾K -==C#z"xHw-RkXkW7U4` =K}oL,JSJaӍL)ٖit>m աW`fO,lSc鬪Pɷ#~ܠ)PZ0.ku7p8>?C8u6;ez>Υ(2ڵ)amm*qPҗP) ^'R?V>oՀn2w.szW46KeY\޻LSNޚb`&o9"眒4|]GTZQUM]{.$VƔlY6OS濸x>~?TƔ.& !Zya-CtQ"-ɝzzC%†KЀ QŖ@h Pa}?# uN#{o %ㅉ fOPf&f6KKh 3nf+йPN#ܠ)ȖŨUTFFۖܖd j  .cwU$n . \s(1hX:zfްw2$8g˧y+rJN3!`m*hC.H<>I atg(y c&.y$ 7z%uyI[V2sirt͊vEX[iKAys!ڦ@XK8QgSM^SSDŽ>;JQ~.;׽=n>|Rя;X[βbz@ouURWꪎJ S2@03Q(9g(87LnxYIl=>2 Sp?Т)MP 1}Ɂ U%)stV,(KLr퉚^Cn VL-*)hQ޴=Hv'K̝aҥ2j`U6FНym9yMUYhP:=Ph✸'Jt㼹TUg>F %+0#̾ᆱmPX ߝ03EZHsEqcon6+ (C#-9yYy19.f%mI̝ޱ]T]LksK֐oiyKxd8N6R'ca5$ﰚdjA"4*u6Abmt |kv,ƚj_@5 ?Q0s;CաɉW+ʛo %ڥV(u 9}EyӔxD: SJ0>5!RO^o]@/o m"gT3o RoRC%bدB+bNmA\Mvor4}FLJlU<#y61{)۰[GoSZB?`lRc 14~4jQרHyN7ZA1JM"xm7'~־tT4k63ļ*QCr9oWRV<(P dGyh49ZQ뻢4n0ř}+_%Ίϻ+ri)^>?d N~+=&$d {CE߯eL 6UM [q*pcmxޱ9]RĂCXyQ,ҪS6Іhy"G/͵]$[I55eթu贪SKKP妫t=RP'$0M1viOb=O%KonOwzyξ@/^w|Taf"'TܘMqcnEj 2(ڇw! sSMھˋ .׽:&7n√p@ѽ)o>JG[VTQ ˋ/l)(1./`;fcrj2Dmu"IVQb7X5վ+Fyoo;XupLW-*~"ϴ*ۚ0rio>j }* ұrWE,um8^z~BoeǺܲ[`f*x{wXr~/?STc:}&@flqIIeGϊI.ڡ瑥*Q]*q0\Ғ[>lObr4L5ͷ̫tɵ%v=lAdYR_/ڗKgiVmc*Z0 QPEs=ْkwՓ :o>*rٕjVgLH(o$nYb}}D{~i%Su؏2b.U M3ɵ'z*ztx{(v!M5y2CN:v(a7  *<21F!QTϵ[`Zxt U3[ߢO Q^Xo[b5+턙}F xƾ!BϪ}'e6ϻ't['{|ZKP"0oHafz_ɏ3>W9(DH9֥.C*ds\.x?ۯP}2}nE;qgUxvg1[ާ%Ҽ*Wt%ހP[JB-<]m)z"jg1av*M]XO5 8"Rf+9߸:B4d!ْP-ڬepnvw^zlXvVPyҦtv0NZWi;6m  U̪DdeB)2K1zjr$̶V&~\>j?bU8(u6u骠dwWUzx.z}J[eldVvYK1⍬媌:ńZILI+KI YH;h 퀮eOC6$lI+jovhsV\Yƽ1$y d࿞w`!IHG{/TC$!- TZ @Nפ<:{dev>f5uߪeɵgپţQbLgS\+h|in(==1詐9:X6!&9}ɪk%v0;l~K[W< =۳V(ޜRBR&X ʛοt<\V#)&Kk 5V;+) {5•n19fD-& nҚ$m)Y.:fRWFQsYk&1_K[qZs+Ny^rV@CAIV5k5-k0Jk f@mH )Tkm7Es!Ϯ#t=V?**] ҄5$7rOynb =ϵ7gThD>u7mi0@S<Ϯ#I.#pErmHw:Bkגj+M'`||ͧ %F[!7UK 9>?̗>?K]cˮ1pJ݀3]I"* 0I SS+BV|=ZV2,I s l%'3 5ԌbmoEbkVԖ:< Vtt\kPjM{+uhi*ZLNĚSj$ dMb{>@Xs1ے1ODz .1S֖b/Ia -v 0npSKPb)hﴱA{;:]66r *'9r}KO(ޙ.$7EiU7P8[6A`ے-(b:z#;[%j9F7To+8FZkT&IJVJ+wF] ֔<5zXLCq@G"S];O9Q[P) bm-t$$a}ȵTdL-Se5Z-.Ǯ&ó4⹝ηС8:.EBmVlƪTƥhBE%a;PUwG(o:@k3?@ky(Xׁ ϩʫTjblm++i]65%tZP[|w-tVs͎cbu4k3xm;ki~@ׅP*^.chREncc >:KqL, QeZ3:PccTEO; U [c;JTԺm);PDḱ }uYwc@'@};e[4s2[-Q`:+a<'\c1o7Q^\]龵>FӉռhaW PDm#洡-ַM IO^hqNc^՘kPu\VR[3+}" ز'-[R{{Qly*65FCƍ &{9v&\ֽZ:be'{h*L,P\ i@D2{M 0'+l f$~&_k.ӖIӽ9rCq(ՠ%ŵ2*uz*b&-N=-N=[ݛȷ`re䶥@~c"}ڟtuX` .Bg'Rλ+pjvRd]r? AtUz)ptxj/)g'3ueԵ%S8NVP:7vzDBF(g[E!ГÀxgJ19=$ג72inrxNc`-bWO}+zNcBWzgn@R@ -ʳwU?-![~ bԚꊩ ]0)mN>Y}JI[$MJP +ULg5@הMyQ"akOeeZUAYɻl jv;m9&NN5d;hPk0]OJ*uWs~KYm[Qtڲu]}IV&Fֶ㔘VҖVRd۶6fsl@,Q(5yg!EMԶ,+v9QdN~lv^;kHu t3|IW5cE[d$IHr"3 0㢝D%E}+P*8\4}Boe\ g&;A~ >y!E<>A[9`MBj\|^frybʓY*=r P\|%F6w*ܮē۝Y5ۡLjuHmMbECD uT25a(oN5*PMd!a5ue%IvjѾ49Ձkǘ7i%vDosk|Z,2H 5Ytev$2-IlWv^æb)ȷSvC=\Kys:SAM[9:6۴)DH+y);fJ|DJ; zf ԆL:K#SqVlwdLr B}NJ?ɭ5Uy)!ԵfM)vrr4lH;RWsO:Ә[wV| {&5KmyؕTd*I*<1yqtŔKYj:@goKVQ}h/ŵ6ft .bCx=2CAjw!].GeoCIݓLgo@;hZfvE EisͺC †tϷx)OnϞMUq$ydz$) Q_q$R:l~{+~SMx;󒖢|A'#z-}gfb:@/o8~R2E>\k(z7w(.D~}]wēשі|/%gk_~{oT45yDOP=e?HJ6T}P1(P>Vn=X$}*=B<7ZҥXoc4V(·W yN*Cb"})uJ[!؛>_CECjKuJפ0׶쭋>ʽ˄-AP|BVts7agr 3ӳ:7|=qػ `$A>@&Uco3coϏ eIAp@eRN 3_l~ٴ̾j*^mť(ʩ+,R2=nSe51;߿܉jh ` >SvԽf+ g=H"1 2?w򦒊51SgNU2*.b`%:]S•$[Եաlic5۾TQx?OS VXIPTg$l,%:qrh%7L7m idy$PobN}jkJ 4F0ggbUkKFOoiDְ?fU\`_;a45/+Z؀{YEz:ʛT%*05Ոs1[BݕYW͘1 |DY/(5|(=2Ko7JEg5 ڨb̝Ÿ"wx[>zR\skZV:cbИ,:Qh\%6;4X}猙jʗ<̩8@-ɓT]$a(oNX57=[rsjIJZǘgLVϭiKsu 4z}) "׵֍S_0oNtMП@c*Bys:"Z^Y$x݂1'͹\ꐀ*e ҳwGK%E?9$jm(uroMcaSr}+t;unE1 YcjBB"FmjAO(}<'n(}(]PZ^ѽX%7K'pt>w(T+iѲL'(H^@2y] ݞXf>K+03J,a+Uʛ'%'h4SY *IJ[3dY@KZrTy %E@:4`:b5*RjVR$ۦ۩4#B㖄sÑ$Vth[ڻ 7(n}^-r;6]aPYvZO/8 ڜ͂XCJNbKB,uc-zENtڻ%phYFK cTҝ}{"'B]ISˌk}{5D2H[FV2mm9{hT$R!]*r?7?D^8ofͅҪb&ڸA߇6;>I&Z8mk>¹dxо?E$@UO(R)L4DDm+MJt$7pf0C cic}(KlW)GKRUʘ]3֣%fسs5zR<{61#ɵǔkϬLnZ`8".%E6 g0U -ŵ6}Xl,ѬH7JbHm9)0Uҽ2;"TxZQ]jj7]Iuڣ<1>)Spu $S떁@zRfl Bmi)RHGJn1[A+6P:sXJڔz, #H65jXH 6SSkwdEf,v!PkI c }H$֧̈N:SlԖ[gI2wy+zd>sH*kkhC ^Ke%^º\ 3B{X3my^SwWܰ gϐT g!M 1kS Bk{PJ̤X9/ϥTZwRQfV25_ebJVk7}c{7wpYrQ7wpUK=X* YrРH+ĹlU&JQA٤ }Ik}5e-vM] b,=l5բ0oUf Q}(nCzg򦵧V;{PVdK`|wמ:%EXW6FNPTf%w4;]SQQ+7Kѽ%HnJUwE]:()JQQW2u5dJJGN޾ij,%2o_<L/:ʁҫjɱE-jgw@9Y024!/-+?o=YZ/rب@^'ZSdgMAiӣO!1++ –l`tR(Ǔ?)x(' +̱Jg$V$R펲jjus.P$)=)xjlW byWgT$=sEyS7]b>yELn)rrB7b˓DpMŚkErU5{%!暕#\Fbr4LNaӍxPn \~X];YBek!6*M8}B6}ʬlo4PlDC,dFp7LLFɨ2 &ynFĿ ? endstream endobj 19 0 obj <> stream x}ۮm9n{}.0T~v@$A9{ipLU%Nju]c~_+o]߽ׯؿ^ou<}0eYirf_qB_^^?l?Ο?u]O7uoi'"_{Gq:4<-78 7cZ׏ۏ}Mnq?K_߿ro_Z/k?_#ʯ95||KٿSdZdnkOX#AtdRݔ#6ڣ>(F*2҅2 ,4eRDT/D]˘~/QYJ?T36:uB#{ж#Vx>cΆcmfɘL2cD7J!(LW&K8@+J~fV8:)G/pTsEpL_8di@X6%a#aIDGz擌c(hF-){p;{H'o/9"Z{ bQZw@6l*%At4 <_yWQЯ<^@BHX][B)BLkIP%]5+k~k0ك܊DͤoĘk(P>g57}[@XLT%(|4QhCi/:[ v%l﯃t-/0~;<0-*jv!m=gCo e m+Lx?ar%'G?"-MnJU; 6(`f- ^M}`J= ͩl^xz)/0;;Mr}]/@Z]ZyjZ̒?NK;XDQޱ ?}(]&ؖdЄ%rԂ%tN5R߷F[#BbV/-ށZ1:<NM,fU)i큣$X=D-țRmH4.5jtHZ:&ܯ}eEhAqJ/=ܯFN<~ 0.XRGZR|tXr"UK֪D!1`{.C$<C(s9rΩtZ{\B\0bQc^1钩|mzxSi}HZɝ /ڮjm.5Z}g)jyZ˂O&]cnBͤ"߇(7Y@ySEԶ#7] 66о97BF ;k^];\ F8_A]&w6QLXy &["Ra˂Ypc-rmt/-q.Su("RfЫ7SQO=a(o>H@!Mͦ U"B{4"z#a+?oѯRLjU<&>}Oj(2pawYNgM,. pj?xQ!{6Pq:B 5W)+QuhLpbjxBZ3-[.%;(iZ:yLūNX7uҜk)>BD I;+טwZ^pFLM=*l*G:PjU%H+N'8N^2ۊ S;]m.Eo54:к͐lYHX\{K= :yIw+]ꪊMy뚰W,Q@,-AqwQ0MT!z]O|[]駿vH;(1:GQ \Pb~]ZQMAULN&4D%ņLw :TTߔqWA3H%֊tR_r֔&77as%(oM ojj3%ՁwNSԵFZ磃YT;Oݽ%cIh@i zdz<1z-k{(U|m 9XLeIw^GmF)ق}TlA"j8@ʛȺk~k"Qe}*#AqtHbO=+O |+@cVO(?,bWPgmE|p3]N\7HNM+p|HK~¬-!?vj҆ʵ}O$M":|Z"w<p~(S`"қzcxX>ʛXM_ī2"!2}?PX@mM58m2XZo8:n#\b~P*PøLwh@!P;nic#3u騳I7u 2s?icߤ ÅwcDJ'wF׍c?1@ eCB.EzqYBN}pϔS'zf> 4?`5V!1=8y,sl\-%G/`$oKTQcgQZ8nVRU~3וOJU)CqrXuo,B{c#.74jQu)x=HPZ:a̵li(P1!Q]Y**Mm!`*2!-~c2*c.ɠp̠G~Eijva`Oӽdӗ 0K٘P৕{7Of&!yԨ̱Qr2hypR.e(K_[fRzUrs{S}v$lhiU(Af-f^)&SVL] L(`rF\n4$j}++23\[3k+MRݬ\bC!"1[Z54|5H5B1LFGSrma0oxentBn} m@QY9t8s3S  gLϼ`2ʅ 2Lȕ1HK~.Z:pUI%]̪L_4IoMFZU*엧sxԞ@nPs:늦/_NIJ$!/kǷ|k97 7ݪD- (:CC2O`{{ҫRqO((5Auɔ`lP97wmū3Qn)(* LeE8avIj">˻-'oMKׅKRUS!nk[n.}F P:BmTV"*ծvT&[ɽmtuN lQ<&rh g(FSX Vt=Kһ03o9_:?]@&"ȑf(yF.傂k_x ': <P{ `J ܋xm(x eI|g57Gr5SeW7qh^Cޗ2A*ksf@o/V^d-&]1իfJϽN#󆮚nNi^E-P[UyCyHvSIAojp)UaO=ھ{ozU(t4`ַs~&Pvla0W^j?i:\; ^mEF"+Hw8yXӓ7,?YcBd`C2զ4 F/D*7`W5?+2"-kszg5ܧ/ иYƱtfvFN(N( v_cn5uNnXD4JBHsLcoo&0U(,ڗ? &p6sJۛ|ow[ꨟ=V4KNߥ@+-e vo5$j!_<ʂɵt*Syo` ?^*CP~?35o  CN=y3)Nޡ=z46t=nD7ޛZy>Yɵw=Pށ1)]WWUڂC8KfEfE繿*m/h ]ۂ?M-~A3N]9})&o(UIc4#$GТ,CAAa = okJP|r Ձ4ViH;k7L@K 7JꔺVIa[W4Jbs/9zJ[˩J9Zpd;{ȷwS΃H[Rct<[m `j= zɩt- =v7ijAXqN38Zpo>f c9k{~ '_^e |3%|zO=hPpPovBSP]Pz@.f3M}p}@)VZCѻ/sN׳$yQ.%/w=ʥ s1.3C a1^PҏAI!jɵQ(/$wjm?ȕV)P?54%jV$jf~OL>Ձ9BD >h: sڽSls8vs0BbNŲ"<>Cy\2-mS%=(cWyUNAh@QIy'7;f|ǯ*@[hy/u zn:['"jyUF]F35Lo ;^2XwK|[6BFTd*B!vT tt {e5''w-# <\|C14zzBceuo|t' x3mؠ ֥;z0@c}]Yޤ2V)S*z_*ZӣM-.ܐkt HԆIMt/tIOA<\.L~I/G:[ADJ)&ߟ?kӭK)ߕ8~܇Dr>,E7->WRkhS0zy~^BCPC=y߂oඔ! QǼ ]?ϢG(tOGD,6KxN#DLĤjvC-tAf}c] ly]jmCC}ԇr(@Q CI/ʕP p(](=x}T>@"5uT9C>"3-<1Їn*47v5]c9C:P*BZ'7b4CM:ޕ2L PK{cxPtf519hLe{hJDmI=eReRy'H-E8Sބ .Y!PRS*1z %1=繷 rJQt߇CgRЂQnAmɵ$[xꭏs" [%ZCm;%ՁwTP)3:t7f!}lc>ۻhgфF/7t%o}':X*Xb(Pqkc S-P"ORp*"R$~f!DŁ9e$.:&#@ ge*}OO0vw0KRJn"Ycޡ?&(& f~Gy(1~黼 #S)[qԩ.AOq$rl){crf%asK@iaAtid+մMiwŔHʖyCEOMf\;X6uI"yN{\;nvUp(}FCS_,EwCҋZ J};M slAq!1_sj4(LQk] ZRl?t۸hpgEB)l_ut=qBK&1ߘļvڍޚEU|!J }(Z(ݲF F[6e$2-P[2}ɵ7櫥JƅVB_ڨ2)?G)Q(J; c~g"/2-6w ͟n_-mb㖧l|m꒣=vmji"̹:\aGOx$% #-G>hh.SvYxBɝoJ L74`5a,/X7~z +e|4O9ڐqmd 7[.X ri5)[ lM槳p9D:tRзk:t PXQ9"@f@o9xrKErF^K'K\އ1/re?K''Ji \~$P3` =2|6 FiyВmdKJn9׼,̎t+yە-_!J$[W1yhܦ@y2@?G3E j0?ɕɻk[ @i P:B m Jl5\[V|k#?>!%71rU)Pou{o %1?B%&]_GY}wK[ZdFžN.Ia.+_0{ߎ+VArtڼK'4|kdom'KR3X P*`iMT̼3o|M *lļohΨ_iJ^a#P[TΨQUSFu)+ ŏ]>TtǹH* 2*ӓK@w%%"W4`^x5EX+xaNw [sa*xcR1UI %P>,),&;l57?0/Kn?~ڐ}S.%փĞ(k^؟On!SDb"5ǬK s)mRlTGp =2W a}*NsLw5%6θFգL-F: S 5&1K'6)Fg`ЇGh|1B_PWmԎ&U}\@4턒@t_4%;c;F,B4*x!_НO]BI{Z]5fOY `0%(N W&S^Z׃j[*݃:pd)7\DDr{Vs=zt צCBtl2ӁN-KToR֥ oEomqX%Ĵ u\L7]r7<48ĵldM^~PgHgt_;@x{kww|F*8FSMmߡ)i~F*6ay*'R$t#%O9`ڛ"s5"M^*PR|p-6dsHT@ye H"eЂM*WC K4[iϝRl[2-CC @t/-`1黪ܟg<&z> stream x}.~~.xO$kz}O~]/oׯ~R_?v_߽__Z_?OU)O뗿,kiuu_׷_^>߾u/7 a~u?wjo7"zۿ4zɸ~//w7|},q;Z?/}x_҉z?wM~}N֞OקZ_}}o>|p[B냤\$QorB п!\#Dpp &O ^$j`bda6x\l'b/ lkJQ=^0‹}(V2zڔ juJ_;MJ_x %:$l.I2]Q5%A5!1Jq;*;7"E|i$Sa)J,gՕ2UՁL |7_T?W|Aھ~yH RFƻ=`ZMG~]}x^yoIʃZTׁGe9lk(NZYs~KDM˻TP曩S@yuh#G֖ Ӟ7#cH98q쎝$ݟil=dgw$jF_0O7^j v.d-br-KG /fs x`]*)q-D)t"G[-FVUZQC {Gۖޕ4+xBe`kJm-FJ[hGFzmJz.R P(Z7͖scHԂKQ҄-}I7ʛ }l;e+ V$j-7ţSfo!6lvDMOOכӞ? c0Wwt AW(eSߎGk3, {,:b&;VF)( [@X+t+p yJW"ǎʛ Ő#S82e}JGsJ&;_SVo .)1ϡQ\b ­*uͷ\j [P/x ^m"jFzKI1k=)h#Ʈ[ɻۖxޭ(=6"{L[7lȖHwP\Q(=mKp6ȱ5M%o֑紧:%aCzEOOmI]3-[%dp.%xš;݆ыMO@͈tݳ#Nh$#~L@7(Cw=Kwu[1vYȔl5%\:a ƌ;)Rwxݳ7I87;KQLmS*j^P˷mC3Oǩ6.uZl76Pb?|Q4][(mhMP"zPdAsJZyuTlhJ[ak=Wgc/=A|K=_4_SstnOΠ-q:ilQ173ec}󱕶t49ƥ-]RE݂}ǰKUS+1(o()= e/e%.lהd(Ȗغ(/2h"d*G=`#n'sbm9`l9YFأHǐM`*_P.B`~ildKLn"30mתx~Ǹ)=V݋ڧRNngMS"cLvDmOԆ樴e+37SMm)*5 Ʊcݥywhv:1v6ښrmݞw RzQ(R{^eS.aSh~R6Tӧj<-[@a'Nt~ %{8Udr.1-PC^q {7u\6L٘ͦxn!ޢD9ZbnF<2kC[:BJt.lzBA ^Sp 8~ZSl{)9j풨VYp?ӣ9_8^"Si35[q??ykG|(?ؓ6wZ~J":5n1/PZ-~JI8e/~@k8Vq%qclʱ[XVPV67m ,plR~e)u\Kpk n`\p@O $kO0Z.17lM]v .6gM$xVlـg_ϚH݂bKm/z##\U<鎘T؂@#ahc=sȗ)&Zoy$Hm+>;B[: ,zQFԵ>R$F˖%ٲ9J-M1~i؟mjh F Mui!Ԉ9&42OT2X h'[pmT9|R[#Ls$Bs]Cl ߕ>\<$^SmGh9l'4w"V=ɶ[Ἓ¹\JYR]n,C>6 e: @q1RƒMUi5Dm.P ZZEq-.%FSvn]ZEFP琨@DfIm˜`CY@%yP|Ҕ2a9EֆҖ,4v)a~)q_J~(Ke.)#a&vNg ; ƦD):oҵ޺2LqtrlCUSqIw(q&%1]о㉌]r60ZZ݊wM2l+mTlݦmI%Pã%ɦ=Pb"b)>mrz]Mpcp[޲':4O(S mj7qBcy:4@mD y{G)ŧxԀG\ȱ;pRm ˕s*RJ*I;eKqsT~ƦȖ.6: کkwwI瘰~p ~P_@vΣ7 9ךN# Ui5i 4=0$ |錩 s|U:t~<Es'-g@PQQ@M yRr|4ż^HX\_q\.E} ˯Ԯa`?vuFoc pJ~\Jɏ`10;8v̕i#U%&* mS`E3>'%0%j(n(`NgDeV2@V˖n_b}}2R ɽi KvnEN{\8YG¦4L25̪qm}mEnK[̚td֖iYVb&نrl/ €m9T$@+*}6w^Bm5ʛPkFGU#%@rT3_'sb/K04y8"^ZW( :0J%35Zno%K{t3h*mR~nt7a)Tۊ5;6us+=ɱ7p^A[}g 8RkUpK}<=uCWMȵ`D? 57%ț O9ʛ6џS0Jٛ¼"ݱ~jn7<[:eDC%'I[-L˹d\rڀG՝WLd4q)vEb:Y ;b'xЖ\P/6̈́* gשƐRa>OpVJ5hKLhQea/vzyԖsm_sXL&+ a]Qt߇uT-JMjlku)Q[&yQ[byd*-e%H=|a13,4K&q6<:[9j[z{~5Z@tIKaޫt%F.mԡTVq;yRQ.AGFm11 QbV@-H"07(śAs  ]G=)C)t!s(_FARա=QIW&eO(,kxB9L--ٟ{ ~]ҺR\hp )甆!(B4i}+c~sV}k%怇^f-Z=PCՠd),6g9o9wP:)9a.Z-1@Q> .4e~ 1[)-5X?]9Jڅ~(ƫTE %Zr[SV_{bMbns[sP,mЃC hbklQU:^f{-FMDLTk45B>Q[^jǞBwjU?݅J$nCMk ԂHpMȡ?Ì:P 5RG߿-{`y@[r ݲ~ݓ=mbA(B^yĩlJ| s8ZPqnUqͷtI˱,ē [I߷0V)5jJvZ-*E)'{{]Eu DiUم7LK8f,$Iؾ{*`ti[v4E yXLK?k|<L7d(̔PZq98Jy<B߹k,2;h\k {>q?|;:)m_g3~Ϟyuѳs}ٳ>??ÌfoRy={S r֯3|>={!at G FSJ|O)ϜMm39W[Mu l/t=8ӈzTcmiO7v7@civBKJ޳G긧z(ж u$Z޷gzZho/>\9QL,^=|~n>"S]/0[/Mu߮ฏPb ށk4{h]:DUKj Vk֜~3ǼAȖSES|ߪpApQ{Emn5$(Q. F.lJE(۞MƻT.z0HqIT]dDDiaҥTdCE*Wfˡˡ9\d&"T*1ϽpkpZ| ʗCѹė}K6:ʛk:SdԢD9"fEmk PNh̝7@7=BV1Pz`?Wszl[X٣WV!w J:pcJ&tN[ Zma ֎=!?UUޗR uBVH Ws݂\6xhUbYkjS,AToyX̫sC*;;sZ!S#גmRd^,o5?`.n֕2U5~#Wڥ\=UJ6DmJuT\*Kεnih}R-5ؘR M wIB 3}CNUkc56LyCqR }*]J]@JUMZBV%~r(oNvl >5x@ث)E[A)r/-M,m)e2-1R[ -lњVq(]|Iْu9K3*NqY׋[e'ju7֢hkvB}њV=jPQ._Yy6~8c=.'FAʩ#Ffː8ZN_oj5/“8;|&Q3hLZ2k^2:4[fQrg"n.k~đ۷q\c V~s4`:'k]%Pinh}SM#S 8#"աצhnO?!{ >#Sϯߞ{}oϽЗ^s1{V/S9V* -O:?:@bg$,){+uCJ~{!i7L ]8nLI[Y:f_C9 9aHj7qצ eQҐ}u<(o~jIS$S%cYy J4CCysBU?C357]  =~-ܻyo輚b˽S` TFԵ}.PQ%@,R; =țjj _9+cn{1X{>a¥dd肹B4`/wt(Pwi%[̤ ܐ>fNM9YxY 9{O%jw~JJt 9/hs%yCϜS[5x(v 4✰гva5I%B]I gSst>GYj}<;1 =`z.EW%F)fJ^x#׵:/5X/U/ڞ ;L.4:RUMݚ-4jƸ̦St5M=אot{[4X1c/ٺNk]KYkȘz>49ΣOt";_tyӓ(s_q -v% ]nv MNǹ$Ja@{CĂ˥xT$ɗ׊DmI{q.2r.(o&ȁO״:?dx}jP.}BJ1k4w;*]s~F+ W}UlWw0]C|ߑ}(%kjXNj,V*J_\=vm csR7e{we>6Zk 24צ4;<.0&l_R$+eK݊4;OnUOuZ7i%p>vGjJ"bmď@ 3P3~\C>}o@fJĝP c|AqbpV'6"v`2KSDvA{9!~y2T yH^Jx~u@6kt waqm-z55ygNhP)&v`rI]⢭yvpZ&(X{8s]'TKJȇ@Jk.2onhzׅ;&2PB iAvt1!jL^y--)̓ܨzCu'L ԋ%"ͬ:ʛ:Gա}xu:t~4JU]]RS `XSC)bdMapE`3L~<+gP[hht~KFs[쨩=Sdoܭ'd4 1΄q)njUո[g:iad@ОxtNUNMlp̉8=Ǽ/5^TB.v!jT:۫$ͭ"(A )Ry桔wpv;?4ġB=ùCL;0| hK@cv,g #=C0P{ETk+Bnוw^bҽrdה;m$f ZSBFm^ "J]sl)¼њ.7Vk/EoӋ9yNs.k/e%^ Ժ4[}oо)6k9&r)QER-d4]vlhq۶2\R+TQ6& 7O^7iEݤ!bH}g` 2XSeGZm8}`osNn:&%w!s{|2a9S-8O1s:ZzukuT&L2*'an(07TЌw^;+^JJ8SAgU=Y*žcɓrBg@m_lh#WX=ǵgǝpGa෼t Ȗ)Y^t^R-^$Ǽ۫+bV*&-Otz2,)_|"@HKrapXHGqe"Ax*gꍕ_7:8 ˟6|Gq ]Yн% _Kcuu\⩶@-̡t[\1CJwiAהup\m&yѠИ9x\,73?#;U]2 akK1Kc-IE-H'Y^J93{=~*YjtB98 m). 4{k 椲PҳȞbeñv B#|o=<зhpxhBx x{ЌR+yh`ƺq`zPm5i5l\AgW`P5h_=cPڶhR?֔c9|^`ޤH e~R`J}2aBCbPd,Lm(ow1%:/텰<%VCC{FY9zcZj 6 ﺥմ-^Ȩ4i{\j&J |^$R;m\-2lї-is@-.QU9Xhfl>Ce4'c*u0=ڔ`I6ZDmKo1렲ꨲʰr@R/'l=P  mO# /GKJ =FXIļ! )@S˺T!7xع7%x2cMiZLZ[ħN6|q4^oJɱzIJ{jQӚ"*QkCIzPܧ[c)A{"[P{@u69DUlTN7rԼ2JO[w Pp %7ԮySH7z)]gS(] =7?`) 5(2"vrl J)"}m(y芩\)kN3v#D`f F`\ WT#%lԭns1B s~X84^4'2'V{Ǹsq{ EG)wtX|;,jnrz{9N2t+Wd?/T~d7 ?Я?ş~ÔJP?\ ayFm ( $FΫ^";G CH0_8TKFϱέ+aQ/ZJQns|Q<|,)ʼ bHy[Ve-.BOJFwvΣh//.P* y;;yU|SьDd9L}]=BRT+|*wVդ yxeqyi2$[vy<pë(Kڂz0vd3{t 1vo% CN*/f/U{mKii9.&f"ȯ|RqRI1vcw9vt4D-8,:Z0 ݕw-o x./ 哉 tnv9A!a1M$ߪj ¶I6F' 6D]/ :KMfs^D-Ehc']MNVWl(1˱%EP,c}- }y hƥ5%gNh4mS@)?mcMKh~VΏb #+kj9vֱzʹ%0O;P=ckMb>A [\z4lhMWSU4ZD$_W%KA?JO(={:VpЖ&ҫsf6UV[⼗/S)x1vñ(?wI˱?#1eΧ J*<)0U0ixe΃:z+=-^YmU)wVWCc>uK|!aUSt|l_a޵ʾ} KI| hh\oIP*۔_'E?SYQpM F R7oZ5CL]<<MţANY"曪`SE$ה`(^ ֝W6KSū1=˥E^w#W#b5+٠-v(C=Ty7^:@QyjA?z~vK3*-K{EBdѪ'l%Tbn(L׎|KmVI9ne)#|LOR5-Ϟ@ͦ\vfEsMǸ{~+M߄-hfTߍ7AXkh\q+Y$asKVK К\;Ϋ<7^a.W&.:7悅bƎqlR:z両s=J̷f˖l{Bz\ mֈѐ]k^[|*%I+)Чhqݲ-^~mzܯ-'?D={h@>{t{T zT񀾜*>̶=G#ܰ4,&})whcOQ$u$KSXQ]u֔>Wl)Q9Տ\ ŽA;nsM9Vg=>s/e^j_~qGeK|Vɵ)m۳ػ(UkG뷧zyUխ݂ n玸OŖf&1y4(=lt ស%'$/}qiA%NޤA{%à$1G}9t0oۋb%L>?fh?$pڛ f;mufsZ3Hl})t@_J3Xf:/_J3=зL9Ϋqq7`=``ە]PeU% p@ ~ܔNQx[FqJ`Ƈ -1 STbQ\KP#*2%'*ֽ٠W=Vd*mVŖ;|:~D;$uav3; 8e5Iؾ$a[:l` ,p-"]բVw35QC W򧔘 =jhxK6-Kx܉zͨ܁KzQ"ߌ K5֖?@@@\6@uQ~hTJOz 5n` 5shjhbJ?)3~\cmH[]ҎB1^ƥUq{)EߺDUq^?f hg?eh1n'0p\_@o<Eaxr! ~!^oCO8 KB`4gCpњOA'2.Wڒ-W㺣ؔɳk+ܛ=1ޚbrZ2k=&-(1͗cRݭRd1^I6|풭 >=2H+E5Z)HAÒޡ RYk(uε^8Wr* iʹoKAAؐgO9tT[lKA!-yOIL:5}F=$Mŗ@-x"*n=[nz:`;Ęo$CG];6P,?-$ endstream endobj 25 0 obj <> stream x}f7rݽ_'0|`> H8۵[jm-M׷uf,ӯ3-}twf/__KM?}my}go5/R>?}y_GkK>>ϟ@_wp?:nCpk{ o|7&{0YV[TwOO ywzk]IEnff4gI}oǿido?f>&?=}+ɾ?Jƿ`W[6?-R "iy\cpw;ۛ>.sK-m1/5}߿d?> L~C٪^=^6[Vvp _EƶF_;}2G";L*ZdJ='%Ş`4B^$ꯃtïՒ'(t!uGT:[;S4նڂ{4Uv`D)<@,tr?:(C<~HG`?_o ߆P%*13Pv, |t3@U;A ,l稲LYbT[?]e{,|%=hwHLMy@8Ǽ%Ǽ́h >TZQPY)7(]ƍ20pĜxJ;/$+˯WS\RZZiKj XPkW2`=4yLS{VP-˙DB8W $%#BtF){<qU 6郆 zux&E]a;&CyTܠۗuY-) Ǎk@J WwhGm/Z^EQY6@䛭IHQҳ-Ʈqk, ~(^EtԔJ 0U:DF )e1Z2yGĄknQ&j0xxҙ`]CǜH{lE%*=폁P)mi**ĬH^ =IJwEڄ23nb(-X5Vk&Ub!V| KH)DRSO<&i!C)ʋB7FJ77hL chV*wUU.)Ҡ>eb򸳘@|JGB(6>d%:ͻH7~E]Rjl?<;s.Ėe|(EhbE"`H&UV5ERlj_6tg)!,SE#"G Qʬd@CTTk99љ2{OS#2 7e5)s0o+1?^KwSy]26|NI96dP\pwsTh-Y},ZkQb }#:TWt:hkTTԆ \3v&N_%k) ?.,9qEg#v*ٝo>jh[ YIѺ& L{f߿8 /R+fZ{YmFD֪l9CKIwUa.Ca0nXUqA:<]MvKU%y5APc[]=w0L]PO UOM}CRYMB2^lnIy3XA!k2\IjlT(rBߪ)^Xt`IxhkIZW&U'`EYjE{ۏQTr\S v h-4*GѲ<-5Ǎ+_nrzJ*m w)=(T-*J[;qw()z96l s늪҈5M;R˜ c\Z( jݔrZjA Ts\׹FszgYNbΜ44u@N%D}IFUgVfQq%TXJsOWL"I.cGҒ!c_ҵˀhj2D>e-+I).*:>:KƬmG(R[ әnrcR3XRPj9+[:7ɯ$t%Tp@wtAhA5ߢLxtHAKޡN@)T?PA$7~;sU>(XӔ=I#ïIAˑ "1RnCuP$TMYN$&:D>Gg sZCVrV|ܔR奢P tj ykJݓDM2$q=B}L Ǩ+CssKj5%;':FjC+?c j}WRHƖZrRlY[0E̪r !iF{7!kGS;?V9XMj5ܥ-ӣyє}[g"<)Gv令dIZ)IZArroJjeeM%K҆F>9KkI`M%#߱8=eU;p ǩV{rZm2W7KѿRcL{cϫ,F }i%b!"S[Rn`ZM"Pk2u09|O1G'j-rauEc] +sLh̓LkRo*zLuCK rr;TCi*7Ri``B@LA{Q  ߸uGWjqƼk(zFm#//Um`Q_v")`Nޚ-Y,LKH7p̷R hE64Z!򿺷v=6%!T+z٘*b쫇:<=%*IA?ҁ i<`ljN ?ڕPty*ʯtgzTؚWS|qmF}sj.jZ!Jў0֧ܿċ-WSŲ8ŪtA2:X 0]-hJc# =9;SoRcȉaFEF+\s,hL4$Pg;8,CR{ڃ9E=C\x-PC_o#o_ Θ]fCQ t/)ƀ; ׎ߕG 0"Iӭ,ӲF[@YrZLeHOxtI_r:@I=ATA6T2{UsCΦPgKpy:kv텴"`ӽ?OYOA/ ?^A#lRek(TT4F2͸3^~vjr\mEݴ{J c/UBc\Z I_Q BĎ:%,wBF|Y.$ u鷹*F'GF =oW* aq1:耶Q6IQow. 7XE%K/iqK5(I**,I3*Hc ǎnph;_\"Ǟ]YdMB-#wQVyکt{,>RNR|^Uj &94򖋢{#LkZH ZiPR`1Eۘʘiob "lAq-DR{ؤXҦ bM_d>ݲLZco?9'0/e UvԯW}=sjd<[Z_Bm䁾`|l_N6Ɇd䁾l_Nzv<¼`t 9@] RcXD^bJqWygƺTǝRO9zݔzw| %T̘?705ǯ)$$P:Sy֞FH2PoyV?70.%e~qۯ͡bH[5ukR,}ּZWܜZo?=%m~?Por t M$Ͳ$͚tx9m ?_EhI1Mk8.2JRI^J <ߚ=K SF_?6-]nIRAJRDF5I9ʇtE꽲8΃%+Zѣe9{;GG%ܤ94nBkk*&c>1!W)FrO9[B{pT %#65A}Dwo!w Pdh'@Ҁ*_;xeo0aJ(iLdeJ*>MI:Gi}xIkWlߋ3c)oCoGťH9̮d̰d^-_/EP9uE!.|S gמ:g zO1gR;(T00fzZX**kZ5?MH{멳.BN-ԕJP`'q~Ol~~Bw]0;?/Bf:[d]]?sPF;f| z([G[kX-hT(V$[246)6LÒ2CR32Q>d*--kOK:ZKܗY:Q/:E}5>]_ZPnS-Mx;4Yׯxli\pL=~|'C)J딤:eu b-1J cuYGSCǙg?ؾu/oLf9AyJِǓQ%i3ɱ'M}VUV旿7pPPd(k{cØe#>ku|wuſt: p)15hJ׸ mR^>t3,(@ZkZޥIp2[d2$oOl`d4vydLA/&CrR,),d7$mD_gC>P*T(id85Kq=A ^<ߚDn'놜."c&w_Rh=٤B{U1,Th,SD^yiQjt eb9/8NHxJ5Q~,{wӝi{plH5&651G\˯I-Ƥv;g_T˞?p=l>`硐\=:(m@)(%T;_9 tL?'6v -E iJfqwpa /G!=y9nXL]@s<,ڑTYԾaɋv$L=Y]Ѽ}@jLh^he7]^|?CeM);t覼aJq;@WiIֆ$_>!u6ۆ@Ow jA&;gcPڱ@X?Q%iSښ_*YYd-1}v"pZ PqJ1Icjkdfp$i ghn c&mɩ-9C%!g ,).i^x?vF 5'P\(0 m2bA9:k(ʐmJ3R0#Ǭ6IR.Bҁ[pลy[*d%fH9]NT8*^ 2QC16ꛐ/N?*4Ňg::0uV P2YPj_B؁|T@И& fjΡ5 9>ez5nGq)mRRDfNԷkqs*JI!Yy*%zҍפԂ4Co=$mJQ\B$$.ǞZI{-!: K4VrQs+c{ e)s"4u4yV%c]ShL*ь -Z,KeI$?vԊW/رo+|OCW9iʛT߷:{G2WPT8Y ـYNچ? nLe%T3i 7= 4ؔ!^pvvnZ gLM󖥝㪤6o7'8!I҉u1x"A\j잲2&JbQ9f-;ߠ/NEԥ 3R@ESS9{eg9 >v9  LE>(긁`")oIﳢ3꒻uϿBOⁱcQ񾇷$ h#-fER^%AR }Ƽ'ܱ`(}/:7!G[=AIG$ P<H (q%I *O%s/cOk~At<$Ų?Gd+KV5Cr¦|PԺiBzBsBM뎕~M 2%dV"z{~ǹ!`s?k)ompz[!.3_Yp| /Ӥvds׳ >٤½Ȑq> ]םz+}ۻ;? r7R!j'Dۗ^+ "OeGB-(Je)% W=FZZԪרgZ֠iҺ{mל:&MJ+K GрrwJ*gk)ŵ%)&IKm:ܴ4i-^! S~=9탳 JQ^pt_LȳO2WbsMrm2Ԯ$O/8@StHUclDEB,=/B'Z^ocR}-]C(}P*c)-@˥ Ki^74=HZd@ΩPZ'g³G{gʭ+{i&ߔ,CRhX|vI \[f)5IcWN[C1iL%xv$ ́RjUB4&IX#+U x;P*cmJju>P"k hWE%ʧYS'zw '^~1-+M9vpmِ1jzRJ ́+eд)&A|IzOXZAoM͵Ψз=O16?us195$wSk8ǨJ2]2\<)iPsS^KR e*'8ЅNKʉvOۭ-떏j;fzo饗7vh|3.I 1 j:ۗ'6K7 s0)aj8<95\h _suS6:B5gXnbYR7LIƔ*5aO|I+v-g3,ZM]3FOI!:y  URޓ仃J9Y7!^)_4vZ0Zb.xƦI1V1$Mኴڇf>Xv^v+ǽxP%|Cs(Yh87rrRB5;ii`KPcq$w,CNs<c,e4z)Cy݁2 v ^ӻwkzJ ˻(G 1-K>8 >8:A5wY/]={kڇH DcKT('K T,p"‡O$Xuj1.f8Fhu)>x(XC~_kUZjC5 fqtءӸ֥4t`230!_r6IFDᜟ}S w֨Uȧ @ʾJv(W4^(jkၾ:1m\Hi̦Kab &5u5bܾv]+dF;nIzN^ `'S з R@J(]9Nq^kZƍ竦ho\WynBW.W`-]lrggtUx]uW}uRޅHujIY/W/MvR{U"7)ҴC_rt0B*9 N-0chP+J^fzsŖ Qo")eLC:oIJ,!_,m)c|[PZ%Tb sgC0/e>`MSBe!BsHۻf")OgiEZCk4Zratkg^G9dl=GrT &M["זHb1z(A8*xIÀL0"!ue-&W顗CZi5=ԴPC}-|%:$ /!P%iW+rRBmYB #2_+@iY% s):nkn]f=-;42xi2c\kJc~XG`3@v( oO6@)6fjOF늪.i@3]J8:¼[j,T!Yw ԱG5mgv&n`|IQ|_j$:s9ҮFr̡BIaiIҬJZ2I 3ߣ)=o:R,k)k7 WӝMwE}ux3?{ $JdIV(J6ېMA>JOɩ%x4h?JyXOPC+ᩪ쾡9>,(:2kwsʱ1;j(TzGM=/s"Vgyw1v}S'ZQ܍ endstream endobj 27 0 obj <> stream x}-m~qJ ݱ{I3x]\N-ߢDuiq|矎u_~oe|SmO_mej?֟֜_?~(_}.뷿-LYp9a㰯 kӿ_Zyrqky~/kk?xws:Əڟq8;f\2sĹ|I78gYnǚǏoq{_zo??j=iqk5~/_V/}35]oO~/Z[maY -BuSK;"EtHFyu⺿.e) P4jJUZdW[WԾ" q3$RK&QqtZW6[Aގ֮5K)]HEתQ M ڂ<&S={ 'ꀪX: UX+R.U)֊2֤OlM:f+D/Ѵ[1ܦ^l=þcE:p+Rj,Z;6ް+Ad`ZKhqmІt݃-ےztċ=RIy3\nr v- Z1N$͵YQ/gvk4y/RS{pQ]GADͥtJ^jØyr9ҎvfX3˙=\AF>0}Ŕ3 0TC}{LŭRbW2>DZ&(gJ4z%A"1¦*JSp$VK_<aVjveUm B`<9jHKǒG%-k<0(PQ䆣@m!XZ>ԤLm`tܺ&w-mҵ6$lIբF#Pqm<i?A$ 1?G̵:ޢ:S{42*h |.xh7Mա>Ry1:q3܉<V^@=bi?GjџcџS ?^J7---iX20u&̌V$q6y( ej05 ǎsG)RHtsœbMxPMtkŻ\JJת= :$j+H r=ttŵZZaJmMZNnSqFbPqU!'S)SF3 dߢnMO`M]^60#ʛo)68ZfdOkJ: G[S\-;ME t ̶u;wh|72J!\[mS2'Tfd.ls,Re-NNeS;%]i[`e"TJhAZS=)/Sw^"Kʴ԰UAOUѻP8!5v^"yu5J|Eys˵סTnGUlC:|݄҈JiTMo6ڂ)Mg|r`ypV$S2Eid KlNɖW9z^қܙ8cPM`Ӥw\5= wavΦypJdt|`,^]a~ޖ R㩥+sLQTe& 39j/rJSk_ru`A dsIEy< 4{)w=VLͤjn}q{q/zE+ތ(_j`G0)gF._ JD ]Kzf#PȽ.RoU՜f*RQE^W)^5=J.FH?ֳ|w=7*fo'GqrlGc5GAGs$&9>)-uo-5+ cP"q':435bdCE0Z8NI9^qz 㑾S͟z,mI+~XgIT´y^v#41]&&##- wpiOŠz|u,3@׵=7'xݣ>N`/+@,R_ *XZ>|0S Qz5Cp;>.S /zVܴ֡q?>zyy?H`D=D+kiZ~ 5K~hV_/O gEL5+ePS^#-SUEo$ w &Q'QC\ !)?)OS:-J]ksnqҥk>/A.><|[.U_! [{S=3u=xo~];=P8E>q/>EzUH:?5" = 䙄) GjQ!sDۛs ׋ߧԕ Rz3W*IY_zrCڧ:ȗ*QR8!`#Kqeog8ODuT2}Z$Jvd7#.o*6".Su0?89!NN`wɖwȖW?S(7tɷ_?Pq~*7|Cyp(( Y.(z£M?~ayܲϫCɯZ9;˷V-5ɟx`q_3o~A2;ߐ;`ۃoh ?w;? SOZi6/Eϫ*ЇdkidO?ޥ|lhsajz~6X?^t :`}SLM맃`Vq]}_}ihX;^jAkp|K:~?֥]rH!oթo~Mqqst2w߀)鳣I3Ov;}=~}>}?~:[r|_n}D}~6ug菨I}tGx:`J_xW޴vG : 0`!^cB]:x}GɯZ:a}|\nQ}3>j)r,[Ҵ!GM5Z *40,-ryHޥe|֊_klxrT@?mj@?* O.)k7M%oc*SMwתcs{%ۦԯ]˖?'~!Ke_wLu8'71}a^`TME#Nև%gR~VsS89Ӗ6j{\ds]6X,0ԖaE{m8/~vW~G^x֟Q_m\>y'ׁk>^(J#dF09`}emqrNE0irQ$j#NB.ɵ5%j+trD}Q(5933#VF*~|޾ӣ}A7z=s>6gz@f,Jcf,_R7~~i Z?[i0*Y,J%Я+"?5oο=xۃo6S]sfz:{~Ы E"*_גCՆ_>`̀z~ثƿRg!Yx7tgaVAOMi竌aUotߩك`~cTjӃ87i}ht091gKޥ0dv2t9\R$ ֦BBz#P :|Tr rޕ֤O-5}~hwZY@sp*GzfjWw4_PW5jÓKAD=ܸ+}jmjBUo%-ws6olM鷩sja|7M} K _S M}x/5(3{<˶òM_A)_>pl OoEcjh/]U_>ʶ4}oNﲬoz2A=Cd@~B~7j˾νchh1Ԙ|l2emj(Q3Mo"dKר ly_F/M*Q sWTJz|يMɽ|Ǜ^D-?@m06)?}0ajYN珢w_j%;I:_ֿ)L>X~yHiRAKN\O%MzK_`kֹtMo?;{?;G2ѹSrJo`߄+KN@c8Y z.Z$q}_g~tkNtCۏOZ>^ɝ>aB7?,r{[$USiZv-ɟsGA\O'{ߋ~೙~_}~T(u@:8"89u7`4HiI̷ 9$ipEnOe'p ?sO:n ^-"ҷzf|iE.tcZ ߾]Jsfw>{S?/7xCfR) >%v |){4~Q?4\GM@giz$0ɟV?OZ 6;~ݥw8ޙ^ezom~x\zW֧5/EOVqEߴ}=wm~{rzMFcӟlPpzG߿sD8};ASS~c-wX|? !}mTJa Am`TPo~O*tG4~T(MR]o#*$xkLL;NVU4GӕFFCq4Oc+nHv3݄xiYď2ْ)ޥv\/C\e͟@|=̿~jfwֿ[}~wh~s<ä Oh_y\˻cɢߧ\=kޥZO|zPOx:8Y{;z[6bkM* 7[zޤ|VjsGw!j(J!?O_@t) 6ͧ-x@iJ[}.SQsM'O!o(_%U~C*{~rڐ=_"qB\˿yRu7IV~P(>v?V4}̓ؖO]sQĂ?t:C@ ,zz~~#:?s:}&k7m`: u7 ?Au?) A.1zRYJ[J?_J}t@0?rG*ܟgҷ#%H[1/`*u-GGG#=1ۗL0~kH7>Ǘ|R\)Qx*]RYJKvr2?5JCťj0?_4#wײ$$omE"}ǃ|xc iѥV?ᷴɜ\ڃ~4 'zZuHoRx oT=}HѤC\B>ޭgmE}lGr#vi[&Ym[ n:ހ~?x!qOB o _oM뇻c3}cd#_L~CLc _.-!Oa ? ;e'~J+5vUi+iq4zka?`&Qۻ7O7[?%ђ6n#M*^|h 7.DҹXAΟy˕C?"~y^q:4}bLl:Z]vӕFغVG֢ 0~TӰ}ToEm?XUKܿ4o0$;_3m_A?5>~ڴ\>6M?Ѧ;'l~Kgߵkڵ> ʾP7/F_x e}|؀tOi?(ϜZ~K~h;R?_ վq_ǓC~_rvC|ȟ,>C|쿣Sp|:kܯ6i/?`z[Du|!~ `>K:p OOEN>\5$a;76D}wqdzEiR#kS#w Og3PYl3~Rg)ԊWּH r|=ԟZݥ!ڊ!-R?T>? >5e~:i?<ۥ?$x6u:-.rSS1e}zW_>we].3ӻ#A#| ~-AyPִwG9Qݑ,0?l qz.wozJ=rg CȐROS]ۯ?_?tq(ߎ}j*)qi*ݟw~:G^f+8fѷuTGSs;LN;{ LN[I?"y>H0|_L}18M.Xr L' U.kSbN FҡEɌc޺x~&RV݄:l;φr` wȟ .mS] @{[M!ݴ8^ھwӃ\|h1i>PCCG }K7R׶K*=TEj[v89є N;5 >4^Q?Ǿ|Y϶]ϡ(2"bZӃy÷L.`z4wg|1CW`-H ,M6rv9"ۨt85[hr.˱ߟB߯.قN)]Ԫi)O=h|z9,+5CgvgU]nvYX~^hl8%r'wWyczr[ 5N -NN:.al1yC>j5ߖƻo.pےU=4kџ=#>~VLx/3{9?i{ޟٻOrpܷlЇWڿ[:Gϯ7}5Ws#8-4":Jvc% &N>5a3F5]$g<5v:Gw-0_iz~>5HҲ,まyh'(>O (86[q o;8<eeQGӧ)= YfvS*ȗ[)2UzmF[Cprh@4Gk13_f]z1N Jz^a0ӧ"~Tr A1;.?dn_;eQ~fSV][>S;+{GNB1:gūԟ-'^ V} CߞL#jT'.H7^Ҽ93ҍ*2hPsJ}re7W/$d*9[fNi&f* Y狮7T_ ̢3AqCo?rCUZv; [\n8ГC?5oRU9|w/rcJZqI_s;pa.Y J;{D7\?$ ?ucO.ҽDԾJ{2w{3l??E^y"'_N2+=9[?OOGN?v)V߹jwH{Osv\_v˜?ꐿשǃ?P;[M+c0 K= }J7)Q3M/hR'$li9%a+l2=/{nD4y|5#~OeCnP~~t̷C?>.nu{̷~?KQ"D0= `~_GcaQ3/yorGS>").rcP!Jŋ:Q@)vHwuN[//(z]Cgtvf,;{+_QM+՟ >_lUewԟ?R?j~}ndz15PTK["NݮVMOS]Iwgs_oݞhht;23/mO۔;;ӿ\>Y55 K@5 ۖGGšsq~oAx;~ޮcvwQXBtG՜h WcGRvVw/qҷB~̾,b&w|1oƘ^%G$k#|jT,BarK]W@T.Sʻv_%[ G*?lR/wX| EC/Kт㨦-W]˧&dhLYM/#/9c8YxmW){nyz~?: ?žth9}ǃT?j])33Y/KxJךG̏LAX ,>88<*z|MҗuZpor`0Zߵ\tMeDq498/'ݻ,qɿ Ʃgs=Yiw9.c'oc9zZ/o_ӗY2?ȹYxO+?/_οLK7w0WD0]H{GI=1?DmFa$t/evUT=)YY,;@O/˃ J:}т"|Fӗ & 6`& 2`J/A zA`ƭ8F=%SC*ns+)us?E ~rg; ~Tk[[/`YmJ۰ nݭr} /SV|Sӿ\o'/p]o%>pw^1cGt׿()ZkFWL+^h fSe `<89'_09F(G^u#ɁihqlSv8G~7^l8S6-u7gT}!t^?tЭIV/tRW]oWe~]M?Ϳ *}mW::a/A ^Fjg"eߘ6j'f R;'|TKG}D?#,B]ϴlNS5|qǿ58>vUn.bҗEwrpM Gq+)`q4Gc~M_Ѡ[?+3Sr6G)/MM+9QCw%ĚT}xzyDYgmQ^k[wip<~?~g9쉺uH廻sneH5n >~7Ƿug___?^ endstream endobj 29 0 obj <> stream x]۪%}8?T34 ca &w*ϖ Ed]tN)W)<ϟwi-ď/ۿǗ0~WG~ďv_q t?.C_kv;8vgc<8ў~xg_Ǐ۫_t϶6Hq,w,{Z=.&'A?j{'/rK=jǿ?cjQOTS1//ۧ:?~w!g`@h?o-|ZIXy1-tK>D\y9~%3% n4]\ nnMN^>"@q$N:O?R&; Oѧ?3F~|«jo_oXAݝȯ_i>ys< ٕHtVwI?+!I vyW#}$}҇G7HAx@eҧ?F0ŽV6ٓ^e̫i=M >m"IjǧIL=ʋ#m1液O.Ҫ#+ELrI G0{+]sf3Ƒ.m.p id "3 1^w1e92Ip9#H B >#" 5z1ד?~oIH~*;Ꚃ_XH4>4>^A>m P+a^dU Y:'9$(e윸aWPYݶĞwB=WghY7j\}sA[7hٻkϰqGuޛ .?2au[6I>'!iY="\Ҫr8Cρ=~hR|<&^ u|G˜+-^s=sZ<+p6.#Llք9 +ւ/.0knR\ϏZSZJ5xDnoq.ָYЯkx\V0Kڤ.pݹ8+Wpy͊w {c8@κ#A[|f}וOz-Q[^ tcqV"0R_ +"0_܉uq~ߙXxeuoA9((/8sHpOu/)^wKZ[<}f3#r' aVR؀Osi-C~M pPYF# B-"JJ:+tVU jOp' /4 #Z+xmUs`qIkHzç9=oͧ5CxA+]_O[bx+kї >ioǏ0^W2w%eέxG~YR'2TSe*~jGvޑm z5<:)xՊI%XyDZ70GsI rK8'i{b^Llm(\`/8$)'0C8'Q?OH>tBd#UTǩ'!SSIZ]cFGB)do*{##C ؿ:br9| h"飵k%Bxkb!Z<͟fuzV[ӃJf|zf 9{WnbIbQb^mHN7C[,ǛNZ! }lw쳪[xw -VswV/0. W[؊Osvޝl rAv9R;v HN#:].>Q`ʠ}@_?;2TS+Ԧb+lP3 hjY`iVkPK?ٟؾ?~ZG69nBUkv?'HMTB{?}"/c;zW~=h ‹2<28YI0eyYBxSœdӗG6n֐4O}2:/󲀌eG}2le]ɌC_Be8:aE\-`G뾕//9P^]HrG<-MP"r遾@Sg'PxE] ']G 8G%']sb2 oxw޸[(V!i0_:W>CJo6cE76ķy =ot?o\yo|'$wBS݄>?$n;CK%y*]|~^DyÎ$뗎=F)=G~%1^#9R^L—i+4x4b+Flj/9cd-Z85N@SIo]tjPVO8z`¹h*u;-ѫ'n|Mk:\-eG)DYvrKKZ}`<Fk ]A l6j;Ju OH-8R8?}*Tpl#̯مxnTLhIϤf:#iX->M\Xv$Yk8)sFH(~$w#(ʳbNE+#&|bM?2)٫ |~:W k<8?՟߈ 3vQ~c@9jy(?c_..$[6ZL -pKmllzi6p f:}L}YۣhjYүVmq4qea~t,im'%q; LOT|\7-2\ze|6ӌO3 U]-nVu?J{s^n naJT|z[<]W[W 5W%һ2/nIaW-: ķj{_5񽯚~k2d[oM[eę_ >}WW/"ҧ||^Z@׊EX͟+G+)8_#s TxRuIՍ'U7Tݸx #KkC 0RL*jEP;-Iϵy5yNxF0yN|F~&9Hűu4?tme6}QK_N??t>8[y-|/7Mg~r6<ܴp uu~#b:[\WZ\:JaJ-CU&|t+o[jGHm_PW])|-80Dc7?&P\ _WI?ٍ=!򿪏bH.}nT8×Yx3|wQoQoQݨ]:H _6o7:xn.n,ʻݨF͢ۍg|pvr *pfF͢ۍE$ÍNݨݿF͢ۍ'ލ| 7jWH5Ro7j=qQSfCr$7W]#釢/??~>PGAGAq'w\"#xrRFoaЄ0h_?aо^}6\j2oͯey3|܅>uOw0q3rBWn䫓:p6%ӗql晏drV)ֿd܁aOxk&|XH^7x[#6ܫ]9jמ_E]jwծ)!b/>ծ͋{k~^'~kO5V|ovܫ]{~ɵ{k^ݫ]Hg7Zѫfny%э-=F'uf76|v߻7n7n޸ZzjiՒM_`JUA<ѯp|NW'yب]SG`>-MOwU.+7J0Q|G\6-?iG\}ݤ9W7Kxx/g:\Ue7/7WcyW?cM-gl(?zuKGvKg FɆﻓn|j_ Ӿ !x7=*Qc=]}*6Gn?ꎌf^T,~.ܫ_{ S%p7:.p)qp Sއ);LM|J2aaJ$޸JBaʦ/TPjwWˊ SqO{GsrWH~jwq~j~rp+9sS~w+4dKLFUI0{͝߸֏xtw~1Tw~׵34w~1cGwy?o77:$-sɺPv onv߻d n&\=q]k7gӗu8?>p׫߻ٿo(W|n-q$<૫_+ѧOE0}S7H׫^'?+Y +9Z7#Kw%Kw%yWT] .*7;X}nVKIa笁ϺL*>g;ƽlSۡC]mu;4m5;N|[8 v[˽{;ƍPkw8?U3Qu;ViU] -ϫkoh~|OUiRwp7#K=;g_H:kGWtktI~zWW}埝${8<gEg=kٝ礼uK[}U[U[:` ^]Ն/厪b+Vb+V >տbW>pj}b>\ZXWև+V2Oݙvw/leķrW7\}V*8UweE>H)GF6G7-߱ TB+dB+ds6 ݗo}G,>|[i;nsilad).q! jy']lΆ]֎_*ޞ?ڀ endstream endobj 31 0 obj <> stream x]۪%9r}ﯨpO.`̀?<yv*֒V]tu-)n~{?zH?~??_"mto__?=_8«_Ϙ^_ua*Ǒ_cO{={ ^kd_.<]mW_۳ ~\gq}WLb2ckͱR|Cl~/"?pZ=2?O?/ۗ9~xf?>݆S/j>IOjK=EX} ~[? w'*pd{F`S;➽RϞKH_Mx?oy+3^Ќ'h_T42Ҍe7f`f|k33333+}i75_AhA?{4!Ʒk BM;&qm/ /RCߪf;/ԏnɞm;z8[[&|&|n|n|n|n|&|}% ѵzpr;'Q}6Gr|sKS{r3Z1j/&G7%~$iM/#߆t4/O 3#&`1eO}%D_ iH1nzܓBOmBO֚ȧ_'gxaYX4͏NACsYwkpyÏ}@cxVrA<^,y )J\nOs9Oސ /=S&67OT"G 77l{_ |7|M7y 7_xc^PLoˏ̴=~)>[L1}PfqyAq7yGl/{;_YWQUf7UUz[ s:`q=Qn/ m_no(|E/4jg7Gc0in?7*z&74)>ta|rK'7lȪ$`換1r zR1]nXݤS.uKI=aè2iS Q¦ ÓI2,n%@Tx@UX<8 ̍c{ˤ[&uk\i0<)u+C$TH̓ ! {"x—ב _/n| @W<ͨfM_h^>c3-= /?a1ǺW +$';FŠ{""dgunϼ~A3a©5IϾ Āhag)ƹi'Gd=CͰ3soO>'M!GԪI_~Dؓ٭$''YॣKo|n{9eFi2ɑ{i| h|Xl?hU~@5Cw[?*a|Zr)JYӗoZ?㗥~ܪ yy#˃;a:W IbXO n cX;HUGq;WIaذ84v|C7i#TcWch">#Lc>£gCIP-|ފx_O{RI?;V+G< G,8:#?j(G1i|,Nv?Sz92g~E/?ƽ5}f4q8h^^r:ң"A|-¹?u_/^>tj:%p;H~(ur ;wz'sMNAi\~tV/OsT^ =yU蟦s]}1GsMӗ/kJ]nO&)X{4z 't`tHR H&Kv?)|MTr?q|ٍUbhlFݭB.$*ϫȠǯ@?[M?2I垶GSV?1TIߕmK^|.Hx{~z >iR>4Oz3VO ["x8kH)up8go8NrsV/~|v㴘))?-沦ϦۖhӭJ,1.英H8b'߲#ǣ{tEÕAFj}n0+ ׈crk0v㫭 #.%{*0>Q=NٓO?4il-oZ ~~Obo }^sD]=ڤ>̍2R({o $?Em)=ۢ)a>/Fܐҥ%D_kZ>w9_wGTJw{}osv5x?֪ܫkkE_TSm-h{-s u]wm#ԞjkYӗ/~bZC?}K~|K%6=?ηui|hӿ<*&K[ O]"?};YO*AW_GMwz')GW_(L /LLh"㐤r'sW];$'Q&D젿jvo|>ᰃ`د+]%#gA/wo|>_ AѴmQfT-]7|;7AA7;{~.O]mMWxwERu>ᰃN+t*NEpEѪ"ˍ,,2"2"ˍ,sb{o4}(`}+Ƿ4>M^W/)ko-}ڏ_6ӆ_Y\` r~J%za$>h#e=?R<|SIӶnJ:w>>>7X]^phP(P{>\G*\+*{u8~= }qj}k:{uxXў?+qjURV479($; ƯU߼~h_?(>h5}E*{QѨJ'T4*r~7N*;}_Ay)!?iO8ߏ|MIŏ'Wߞݥo>:? ͟tLR1KC7,KW7]k~#] zC۟'k' ~gTnJ;Ltyߗ8Lt=0ފ\]7˗'#nԞO=]J[zyCt۳ ͙>NbRo>+FLb;R1=𗃴ܒ?} ZmM=8/v&>p3+ I=#}9Iٖ^~ XV{uʽpBZh+=~J᫻ &onz84~eEE ú1wzy>v߿0V[%hCݓo&n1DŽ4xPB|{AR?8qe0{F,Us?P_G\b['󁖈#.8^!#.:ⲇ/5:_3z&|SU2؍nQU?U[|x]UӭZ?:s_^.%2[DfgNE_濂|ԯm2O['*{FJTi ω?OD/M!m嗪+!}-$>|a"Q>;G_rptM'sKoOzkڇADwA ^ ^]Z'[euuuue: 08_=',42 oapϟ~#: 9hs ee$e.gap@08ͯe/^0_>, : g|a/2 >y`z0F<NȖas$=vcjԺ.,L{!oO^~~&ws:`x;bog}yzUS7ZT\i~lTRcQ}ilG,)L+|rf*DŽSVٶ?8˓NT/}+G[w`Ra~DWEwxx7ꐷG>T\T^@?p~>~ګ=jo upEsO_}#KP|hH_ ~,p/'?!I,m-}Ih_cCL87ȵ2O#rlIu,E G#c} KAo )r@| !#t{0 䍸T G@%']=VEK+a{0(/7~)O/)߄GUG*ȗN UQ?tw(Z>cHr/  SV VE j~hUcs~,r=C !{jUMp0&{>0 1tzz;vqU):tX'zߧі޿XLq܏~ڏo?~ܶ~o0KxaarD1B.ӣg |f_ޙ3Z'{F,~^t>%i6hAAkʱzڱݹzJóaB~=kwm&鷷:z/_V!ݻ=vWD-V|کZg{'i#!*_ayf 3{yvwL:d·te 0/z7L `󴁞 Ӂ,3` ủbb._biXnĶ?,D/9iyǧ#` tzSR5@X\x{" uHۑd d&iWPӗ<;"nkp:R64OH>Ft}cOAώt PL`?X5h@0o~t!O]?PT/;1wlKL^ ]{bc =*9AUCxB綵)G͎W0Tī =bWI-H-XT,A2pcC5<4|et{kYJ+ M7Sbhr:vHð#Nt=~mC'zmJGiqB,t+ Ͼ);OT)^v`/Ԑ-~zHr^ſ?WVP );"fMϗR+zߔB/K!x] i[B>-ոu)ץu)ץu)%=~`}?CTXلWےWٷ{QW=|/~Hۗ+:#Q|\~|Lr/Lr/_{EOI *}Aqޖ&ױ`/}#><ZxBBwz{~p* =mjIoW9RFx ?:&M#?9r$gOr$ר髞WOoCU7t/%xd)/|J9I5^?-:m-:Q!,:[X+&* ʍ!KQ?5|_G-ڞkirVe..vws{xxYjR /Km{8-<4|zץ/mCgHRCC*w޳mS_ӪuMѤ~K{ -W)mKu*s,KNlabӪf6_;B>jtO(M8}'?׆V*/z!НN`/I%pjjSz|<͝^.~/T?*0_hգKxAנW/ߧp{^7?Jϗ+=zߔ/Kx]zi[?pK7qKw3Jz~LS㛓ޏoI/I_n_A~H{y(?+I$&D)?{%y$J{RJlN"$m?~}S$ SiJڶ&XJ7=EiTbT7SIC{Wu=OA [.+/0 CW0_qSZ$&?~aS㡽-xBAAmzL7颥 G]W~".o+_B@UXwPIg *Ti~nΠfߚ1Kjs43dl4Izj8[L//rU۟͡tte?=7WKK5`r'~0I5w9NAc)Q%m=q DžiR4\M8)p"? CW>ڽV5]nH_{z??Dmu 3gE_=Uǧ!cMGM_YO+ =?mfj QB?}A"%&ǽܽo5X(ZvӃHh7&#}].;Ψv{3r9.<З@~ CW}?2M_ΰMQ_貤d{;öΰ+`?͗`?t  \} R?ҭ>J߲7견AY`U0m5/ϊEudnn+z+?d( <_?7O O˄:ᴿLݐo87uP}hD oWAgvhjohjO6R޶6Ƀr?>]MAjDy/EM8յJ}Ɩ>AWO8.Ɛ_iv|{uP)nM߫|~ͮMP /eLePpC.O-eLPPIz=t9=?ySPUNկ_RKU_Ғ1ܶI2/^ء CUo_?ƦGx8t`~}.;v7%xhxQĂr^0q] `ڮ=$n˜9)oM_|{)k+G'lo'Mo/%O8]L+G?퓷O?eb䏙li5}8=A~#ǐ*nc A\Obcor]?ʠ.j:W\ $qm—>/߹{Hetdt>ץK{&?4}jy hDxO'@ >*~p\D'}%K,裼QOi^ur2O"~*w,8EG+};>yw<࿗И i;Khh]ٲ"82|nߜ4 b[eoOi~R8?'k#:<ˌ~Zf<̲0/c/LJ!I^~c +Tt`/t7P {yE_@˚.:r I- [my /kG7^֎nxY;UHÏa۵'|{ u/v:?}{rENP,oχ>:o1ݵ7kyǮ^gkW/L8Oi<>D7]qhFi=vXmhz/cPt+_~.zs 4O}쪅 GG|*Ϡ+?xp.&]H텯^8 Gu O o]>s t \< 놗dM,g?ꂬoܡU/ޠr$R%Ww5;%0ǕcR:vL}{ޠKa[ԽWHY?titGtYO>i.y]]éOtIכd_~?v0?h :Mݯ%I/8z~L@Akj_ ,goXK}(IEE/ ItQ'$x7貦-_߼A ߴA}|BkoH/j<ȮZqޠK<}S{*iOAW9 ޠSn` (ǯA ti _~^߱_ Ytt endstream endobj 33 0 obj <> stream x}-;~)!zz9a @ $9gWZ֪=ܾgmdIeY?Z\w9?f*O~~o9k~ǿk__WOʿ]۟_4ןGv]Wc__C~e[b_vW_~"iߨWoվSߧAky?*53~3i;#?hNm_|/FwͿY??/mh3<[~)Ư??z?ݦVKy݁WOռf;V~skx9N6tޫO[d9< /U%X<_4O)q% Kۥ]14 O5`.Rlx$(V$ٓo}ˈO=F&mdBY֥忆~@w~94A@swć>hMӿZR$|J /9J} }e?ڇ2}-+,e _R~5~La\P#*w\?*xh+vJSqkF'.h7kp~c@'߸~<$\;Gxq*uF?mxfGm(ot0?!>q}u\ߑ~\  ~r`qQhg,qEUiT q/?0\G7'7MOI5%O=oC|{rI3$_mC˘(ֱ$WͿONEb|sI'jir:dGeͯxжfJ}ėn? ?7N>Ǝ;}=1vؑ)vK8Slhzշ'4^A0h[K Zhv3Qhx)@|Iɗ?}rǽOIҗaqZMR?3LlF4ܗ|hLO?}n#ӷO m)bC~`\)yH-(oZJh?mwm gxpS&%a'yޜ&%f6n`,`^ƢƥG{US{pwoYg/˶@?0yYIee9 `t. Wm=yi_U7/=ki<:iK/YΟR_G'tھ95@>t@WAtS"`逮~]` m|@tU7/o>C\˯1Rr5^oԊ芦.}ӺЍ.OtZѿ,t@{~t#o<=MHG}jkwq|>o){|>|>o1_q| ? Ky~|9S:)~U9) q{Nq21qqO|ئ'=N~8g:mʥ$tt8_Π?tX34}piE%"%_֏iM`=(q}0EG-^?ժ%Y[0~韗ߊ~q"G6sjY{7ȻA|ϭ%)?)PX>gG/yý~f1뗶;fu%:JkkMmO1>g猲猲Qnj^nj^71e[}ML8mbn_}X]A*kW'齉yMΐ{obӆ,C{J}{DĄ8}]z%>̌s7af_af(03ޤ.8)L˯MD}ɒ?|q6&yp|gSFxϬtǏ 7AwF#ayIܛ>WXɉU/vvX?p2`~fb}Hz=KǺz.K-z`Oȩz>4'PMQ8'󭵡߫}Et# ùuh:3 <|(|cF2 \O14l鶏SJls]/k@^c͏ G//) %=8y4x.V\K q ړ E/3eg?V#oiO^vSU>y٣I3K繴|`}˷'/? je>ɟR@eke{˾^pO?{Z> f?{ٞ~a~ѽyUm-eP,Uۿ BX?.|GzdXbh{Tq)A]z5haB z@^>h}kh˞O{ȋ=h/=x&jo^'xS෫l_%=?*(lL bzgI`m%T%qtgUɛr&;gW7Q  @_~|r_v;p >9>{ːeE?-u?]o"Qݥy t/7t+l`H {-?V[;`׋az[)zCz>([MA">:_ƇE8f3S[{᭒AKU՗w=ZP-'eSڋ?_/hG;7^:jy/LaT`M-MjTE~xҐQ5jlI)7=!mV:!Rp$9+:}oeߞ,W?n 뒢 R<*{ u I?].V;=NG<}-`6qSxoPf7F=Οzsfm4ѣ~GxOeI F>>CfC !݈;>CgHϐG~x>W8EPZ>~1N/ے½}(T_?;\W].@dJ-Rn7/L|Znry}X+'7^>MCr! .<Z? ȟ$g 'l? ˪y1?@_(q:+7\~&Nn/Yc^; Q_~z@g+i?¾^lt'/|]+{P,Mѷ.x4~1侀 >}wpEoE|s;?18]J>陂N/ߞ*~^ k#}e7ߞߊ?}h O?OM".k.|)z ܦ!o?bC[AWF@y?VpB&˽{{Bȣ;Qcw%?82ܑ>|7o#n߀ٮR>O=u$HÓu~PdHVB*|r]!//1VH)~M4)~n)Rvn)R*k";~ĝ";~" #OR7M"}[RH鎟"?H{ ɨHЯOT"ORNR5V4}Oct))Y|& H.??wI)`0Wn_o`S6C zOKԫȧCZrVX4/J+r(|**}*;|(}(*O {ӣ;OjOj+`Nœn' G|ӣ{G%h@=F@>G( ԟIIT}h(Qt`(5-7)tW{PJ!S%{;)I5uJy\w_@ھ A'e]R~eU9?*r2? _&D}!'Dۧ gEB7A>^$oADOTI{MTXhʧ#t>\1q#:Z*4'WhKӇG}e~5|z:7A?0 kQ>;ls0aaɗ@shY/uKx???+hw \AM|49 5@s [&wFo܁rkJ Zg=_LZ4@e|=iKg91C{ 4ωCg}= ڳ>ޥ>f'}=/=&hopOrKʧ$?Y%{kfZJRhO =%#SfOm/lYSʿ^Y~г>[rA/~`$ɿ $ۆ0LyW|Mg/|ץ$]z=n/NC2!2+i^֧ 2a}ChP~KϯO0{ʿ_\O|9k?jQmty>巇os&zw^Q]sRÏ'sJ[OJK5٤О¼)+RMɾqgg<8f _ yO/^  O~_~^F2w}-ϗOc`d@Ͽ DȈ^+C10#!l>FFKw`dD/!$P 0vڔ{LIs-۔~|TO);gLϬ7z|]~1!@HH_E]/55^>ڿg5ւo!@H K[^rXDe D/xyW|{|*Cχ2__`鰽K3B{}TG' zBa*#*s?a=_eTNʔWIo0p7i_x8 *Џ[k0)0/C/+}xx{YS:|_.V`WT$ݢN1<|˼;|ˬ},{|ty)EsvٳSt rO_诞~SܻP8Cz#k+N]QxiO =|qoJMzQ?hG)Eϭ fTC-M?}":oL=uޝM_Tmо S?k`*n:Nk{~-| :ý ^/{~=W_Eϑ#m#1l1F11Gu,d55c7zE7}:=]Hgk|y]|i

{{ csrf.field() }} """ _actions = ( ForumAction("forum:global", "index", _l("Recent conversations")), ForumAction("forum:global", "index/", _l("Top"), url="#filter"), ForumAction("forum:global", "archives", _l("Archives")), ForumAction( "forum:global", "attachments", _l("Attachments"), condition=is_in_thread ), ForumAction("forum:global", "new_thread", _l("New conversation"), icon="plus"), ForumModalAction( "forum:thread", "delete", _l("Delete"), condition=lambda ctx: is_admin(ctx) and not_closed(ctx), url="#modal-delete", icon="trash", ), # ThreadAction( "forum:thread", "close", _l("Close thread"), url="close", template_string=_close_template_action, condition=lambda ctx: is_admin(ctx) and not_closed(ctx), icon=FAIcon("lock"), ), ThreadAction( "forum:thread", "reopen", _l("Re-open thread"), url="close", template_string=_close_template_action, condition=lambda ctx: is_admin(ctx) and is_closed(ctx), icon=FAIcon("unlock"), ), ThreadAction( "forum:thread", "attachments", _l("Attachments"), url="attachments", icon="file" ), ) def register_actions(state): if not actions.installed(state.app): return with state.app.app_context(): actions.register(*_actions) PK!q]VV"abilian/sbe/apps/forum/commands.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import fileinput import logging import sys from email.parser import FeedParser from flask_script import Manager from .tasks import check_maildir, process_email logger = logging.getLogger(__name__) manager = Manager(description="SBE forum commands", help="SBE forum commands") @manager.option( "-f", "--filename", help="email filename; defaults to standard input", default="-", required=False, ) def inject_email(filename="-"): """Read one email from stdin, parse it, forward it in a celery task to be persisted.""" parser = FeedParser() if logger.level is logging.NOTSET: logger.setLevel(logging.INFO) try: # iterate over stdin for line in fileinput.input(filename): parser.feed(line) except KeyboardInterrupt: logger.info("Aborted by user, exiting.") sys.exit(1) except BaseException: logger.error("Error during email parsing", exc_info=True) sys.exit(1) finally: # close the parser to generate a email.message message = parser.close() fileinput.close() if message: # make sure no email.errors are present if not message.defects: process_email.delay(message) else: logger.error( "email has defects, message content:\n" "------ START -------\n" "%s" "\n------ END -------\n", message, extra={"stack": True}, ) else: logger.error("no email was parsed from stdin", extra={"stack": True}) @manager.command def check_email(): """Read one email from current user Maildir, parse it, forward it in a celery task to be persisted.""" check_maildir() PK!abilian/sbe/apps/forum/forms.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import bleach from abilian.i18n import _l from abilian.web.forms import Form, RichTextWidget from abilian.web.forms.fields import FileField from abilian.web.forms.filters import strip from abilian.web.forms.validators import Length, optional, required from wtforms import BooleanField, StringField, TextAreaField ALLOWED_TAGS = [ "a", "abbr", "acronym", "b", "blockquote", "br", "code", "em", "i", "li", "ol", "strong", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "p", "u", "img", ] ALLOWED_ATTRIBUTES = { "*": ["title"], "p": ["style"], "a": ["href", "title"], "abbr": ["title"], "acronym": ["title"], "img": ["src", "alt", "title"], } ALLOWED_STYLES = ["text-align"] WIDGET_ALLOWED = {} for attr in ALLOWED_TAGS: allowed = ALLOWED_ATTRIBUTES.get(attr, True) if not isinstance(allowed, bool): allowed = {tag: True for tag in allowed} WIDGET_ALLOWED[attr] = allowed # instantiate this one before PostForm fields, so that it is listed first # when Threadform is displayed _TITLE_FIELD = StringField( label=_l("Title"), filters=(strip,), validators=[required(), Length(max=150)] ) class BasePostForm(Form): message = TextAreaField( label=_l("Message"), widget=RichTextWidget(allowed_tags=WIDGET_ALLOWED), filters=(strip,), validators=[required()], ) attachments = FileField( label=_l("Attachments"), multiple=True, validators=[optional()] ) def validate_message(self, field): field.data = bleach.clean( field.data, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, styles=ALLOWED_STYLES, strip=True, ) class PostForm(BasePostForm): send_by_email = BooleanField(label=_l("Send by email?"), default=True) class ThreadForm(PostForm): title = _TITLE_FIELD class PostEditForm(BasePostForm): reason = StringField( label=_l("Reason"), description=_l("Description of your edit"), filters=(strip,), validators=(optional(),), ) PK!C abilian/sbe/apps/forum/models.py# coding=utf-8 """Models for the Forum. Note: a few features are planned but not implemented yet, and are commented out. """ from __future__ import absolute_import, print_function, unicode_literals from collections import Counter from datetime import datetime from itertools import chain from abilian.core.entities import SEARCHABLE, Entity from abilian.services.indexing.adapter import SAAdapter from sqlalchemy import Column, ForeignKey, Integer, Unicode, UnicodeText from sqlalchemy.event import listens_for from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref, relationship from sqlalchemy.types import DateTime from abilian.sbe.apps.communities.models import Community, CommunityIdColumn, \ community_content from abilian.sbe.apps.documents.models import BaseContent class ThreadClosedError(RuntimeError): def __init__(self, thread): super(ThreadClosedError, self).__init__( "The thread {!r} is closed. No modification allowed on its posts: " "creation, edition, deletion".format(thread) ) self.thread = thread @community_content class Thread(Entity): """A thread contains conversations among forum participants. The discussions in a thread may be sorted in chronological order or threaded by reply. (= Thread in SIOC, Message in ICOM 1.0). """ __tablename__ = "forum_thread" community_id = CommunityIdColumn() #: The community this thread belongs to community = relationship( Community, primaryjoin=(community_id == Community.id), backref=backref("threads", cascade="all, delete-orphan"), ) #: The thread title (aka subject) _title = Column("title", Unicode(255), nullable=False, default="", info=SEARCHABLE) last_post_at = Column(DateTime, default=datetime.utcnow, nullable=True) # title is defined has an hybrid property to allow name <-> title sync (2 # way) @hybrid_property def title(self): return self._title def get_frequent_posters(self, limit): all_posts = self.posts[1:] posters_counter = Counter([e.creator for e in all_posts]) sorted_posters = posters_counter.most_common(limit) frequent_posters = [ user for (user, nb_posts) in sorted_posters if user != self.creator ] return frequent_posters @title.setter def title(self, title): # set title before setting name, so that we don't enter an infinite loop # with _thread_sync_name_title self._title = title if self.name != title: self.name = title posts = relationship( "Post", primaryjoin="Thread.id == Post.thread_id", order_by="Post.created_at", cascade="all, delete-orphan", back_populates="thread", ) @property def closed(self): """True if this thread doesn't accept more posts.""" return self.meta.get("abilian.sbe.forum", {}).get("closed", False) @closed.setter def closed(self, value): self.meta.setdefault("abilian.sbe.forum", {})["closed"] = bool(value) self.meta.changed() def create_post(self, **kw): if self.closed: raise ThreadClosedError(self) kw["name"] = self.name post = Post(**kw) post.thread = self return post @listens_for(Thread.name, "set", active_history=True) def _thread_sync_name_title(entity, new_value, old_value, initiator): """Synchronize thread name -> title. thread.title -> name is done via hybrid_property, avoiding infinite loop (since "set" is received before attribute has received value) """ if entity.title != new_value: entity.title = new_value return new_value class Post(Entity): """A post is a message in a forum discussion thread. (= Post in DiscussionMessage in ICOM 1.0). """ __tablename__ = "forum_post" __indexable__ = False # content is indexed at thread level #: The thread this post belongs to thread_id = Column(ForeignKey(Thread.id), nullable=False) thread = relationship(Thread, foreign_keys=thread_id, back_populates="posts") #: The post this post is a reply to, if any (currently not used) parent_post_id = Column(ForeignKey("forum_post.id"), nullable=True) parent_post = relationship("Post", foreign_keys=[parent_post_id]) #: Markup type (Markdown, Textile...) # TODO: markup langage selection + default # markup_type = Column(String, default="Markdown") #: Source (markup) for the post # body_src = Column(UnicodeText, default=u"", nullable=False) #: HTML rendering of the post body_html = Column(UnicodeText, default="", nullable=False) @hybrid_property def title(self): return self.name @property def history(self): return self.meta.get("abilian.sbe.forum", {}).get("history", []) class ThreadIndexAdapter(SAAdapter): """Index a thread and its posts.""" @staticmethod def can_adapt(obj_cls): return obj_cls is Thread def get_document(self, obj): kw = super(ThreadIndexAdapter, self).get_document(obj) kw["text"] = " ".join(chain((kw["text"],), [p.body_html for p in obj.posts])) return kw # event listener to sync name with thread's name @listens_for(Thread.name, "set", active_history=True) def _thread_sync_name(thread, new_value, old_value, initiator): """Synchronize name with thread's name.""" if new_value == old_value: return new_value for post in thread.posts: post.name = new_value return new_value @listens_for(Post.thread, "set", active_history=True) def _thread_change_sync_name(post, new_thread, old_thread, initiator): """Change name on thread change.""" if new_thread == old_thread or new_thread is None: return new_thread post.name = new_thread.name return new_thread @listens_for(Thread.posts, "append") @listens_for(Thread.posts, "remove") @listens_for(Thread.posts, "set") def _guard_closed_thread_collection(thread, value, *args): """Prevent add/remove/replace posts on a closed thread.""" if isinstance(thread, Post): thread = thread.thread if thread is None: return if thread.closed: raise ThreadClosedError(thread) return value class PostAttachment(BaseContent): __tablename__ = None # type: str __mapper_args__ = {"polymorphic_identity": "forum_post_attachment"} sbe_type = "forum_post:attachment" _post_id = Column(Integer, ForeignKey(Post.id), nullable=True) post = relationship( Post, primaryjoin=(_post_id == Post.id), backref=backref( "attachments", lazy="select", order_by="PostAttachment.name", cascade="all, delete-orphan", ), ) PK!!lD<<abilian/sbe/apps/forum/tasks.py# coding=utf-8 """Celery tasks related to document transformation and preview.""" from __future__ import absolute_import, division, print_function, \ unicode_literals import email import mailbox import re from os.path import expanduser from pathlib import Path from typing import List, Text, Tuple import bleach import chardet from abilian.core.celery import periodic_task from abilian.core.extensions import db, mail from abilian.core.models.subjects import User from abilian.core.signals import activity from abilian.core.util import md5, unwrap from abilian.i18n import _l, render_template_i18n from abilian.web import url_for from celery import shared_task from celery.schedules import crontab from celery.utils.log import get_task_logger from flask import current_app, g from flask_babel import get_locale from flask_mail import Message from itsdangerous import Serializer from six import text_type from .forms import ALLOWED_ATTRIBUTES, ALLOWED_STYLES, ALLOWED_TAGS from .models import Post, PostAttachment, Thread MAIL_REPLY_MARKER = _l("_____Write above this line to post_____") # logger = logging.getLogger(__package__) # Celery logger logger = get_task_logger(__name__) def init_app(app): global check_maildir if app.config["INCOMING_MAIL_USE_MAILDIR"]: make_task = periodic_task(run_every=crontab(minute="*")) check_maildir = make_task(check_maildir) @shared_task() def send_post_by_email(post_id): """Send a post to community members by email.""" with current_app.test_request_context("/send_post_by_email"): post = Post.query.get(post_id) if post is None: # deleted after task queued, but before task run return thread = post.thread community = thread.community logger.info( "Sending new post by email to members of community %r", community.name ) CHUNK_SIZE = 20 members_id = [member.id for member in community.members if member.can_login] chunk = [] for idx, member_id in enumerate(members_id): chunk.append(member_id) if idx % CHUNK_SIZE == 0: batch_send_post_to_users.apply_async((post.id, chunk)) chunk = [] if chunk: batch_send_post_to_users.apply_async((post.id, chunk)) @shared_task(max_retries=10, rate_limit="12/m") def batch_send_post_to_users(post_id, members_id, failed_ids=None): """Task run from send_post_by_email; auto-retry for mails that could not be successfully sent. Task default rate limit is 6/min.: there is at least 5 seconds between 2 batches. During retry, if all `members_id` fails again, the task is retried 5min. later, then 10, 20, 40... up to 10 times before giving up. This ensures retries up to approximatively 3 days and 13 hours after initial attempt (geometric series is: 5min * (1-2**10) / 1-2) = 5115 mins). """ if not members_id: return post = Post.query.get(post_id) if post is None: # deleted after task queued, but before task run return failed = set() successfully_sent = [] thread = post.thread community = thread.community user_filter = ( User.id.in_(members_id) if len(members_id) > 1 else User.id == members_id[0] ) users = User.query.filter(user_filter).all() for user in users: try: with current_app.test_request_context("/send_post_by_email"): send_post_to_user(community, post, user) except BaseException: failed.add(user.id) else: successfully_sent.append(user.id) if failed: if failed_ids is not None: failed_ids = set(failed_ids) if failed == failed_ids: # 5 minutes * (2** retry count) countdown = 300 * 2 ** batch_send_post_to_users.request.retries batch_send_post_to_users.retry([post_id, list(failed)], countdown=countdown) else: batch_send_post_to_users.apply_async([post_id, list(failed)]) return { "post_id": post_id, "successfully_sent": successfully_sent, "failed": list(failed), } def build_local_part(name, uid): """Build local part as 'name-uid-digest', ensuring length < 64.""" tag = current_app.config["MAIL_ADDRESS_TAG_CHAR"] key = current_app.config["SECRET_KEY"] serializer = Serializer(key) signature = serializer.dumps(uid) digest = md5(signature) local_part = name + tag + uid + "-" + digest if len(local_part) > 64: if (len(local_part) - len(digest) - 1) > 64: # even without digest, it's too long raise ValueError( "Cannot build reply address: local part exceeds 64 " "characters" ) local_part = local_part[:64] return local_part def build_reply_email_address(name, post, member, domain): # type: (Text, Post, User, Text) -> Text """Build a reply-to email address embedding the locale, thread_id and user.id. :param name: (str) first part of an email address :param post: Post() to get post.thread_id :param member: User() to get user.id :param domain: (str) the last domain name of the email address :return: (Unicode) reply address for forum in the form test+P-fr-3-4-SDB7T5DXNZPD5YAHHVIKVOE2PM@testcase.app.tld 'P' for 'post' - locale - thread id - user id - signature digest """ locale = get_locale() uid = "-".join(["P", str(locale), str(post.thread_id), str(member.id)]) local_part = build_local_part(name, uid) return local_part + "@" + domain def extract_email_destination(address): # type: (Text) -> Tuple[Text, ...] """Return the values encoded in the email address. :param address: similar to test+IjEvMy8yLzQi.xjE04-4S0IzsdicTHKTAqcqa1fE@testcase.app.tld :return: List() of splitted values """ local_part = address.rsplit("@", 1)[0] tag = current_app.config["MAIL_ADDRESS_TAG_CHAR"] name, ident = local_part.rsplit(tag, 1) uid, digest = ident.rsplit("-", 1) signed_local_part = build_local_part(name, uid) if local_part != signed_local_part: raise ValueError("Invalid signature in reply address") values = uid.split("-") header = values.pop(0) assert header == "P" return tuple(values) def has_subtag(address): # type: (Text) -> bool """Return True if a subtag (defined in the config.py as 'MAIL_ADDRESS_TAG_CHAR') was found in the name part of the address. :param address: email adress """ name = address.rsplit("@", 1)[0] tag = current_app.config["MAIL_ADDRESS_TAG_CHAR"] return tag in name def send_post_to_user(community, post, member): recipient = member.email subject = "[{}] {}".format(community.name, post.title) config = current_app.config sender = config.get("BULK_MAIL_SENDER", config["MAIL_SENDER"]) SBE_FORUM_REPLY_BY_MAIL = config.get("SBE_FORUM_REPLY_BY_MAIL", False) SERVER_NAME = config.get("SERVER_NAME", "example.com") list_id = '"{} forum" '.format( community.name, community.slug, SERVER_NAME ) forum_url = url_for("forum.index", community_id=community.slug, _external=True) forum_archive = url_for( "forum.archives", community_id=community.slug, _external=True ) extra_headers = { "List-Id": list_id, "List-Archive": "<{}>".format(forum_archive), "List-Post": "<{}>".format(forum_url), "X-Auto-Response-Suppress": "All", "Auto-Submitted": "auto-generated", } if SBE_FORUM_REPLY_BY_MAIL and config["MAIL_ADDRESS_TAG_CHAR"] is not None: name = sender.rsplit("@", 1)[0] domain = sender.rsplit("@", 1)[1] replyto = build_reply_email_address(name, post, member, domain) msg = Message( subject, sender=sender, recipients=[recipient], reply_to=replyto, extra_headers=extra_headers, ) else: msg = Message( subject, sender=sender, recipients=[recipient], extra_headers=extra_headers ) ctx = { "community": community, "post": post, "member": member, "MAIL_REPLY_MARKER": MAIL_REPLY_MARKER, "SBE_FORUM_REPLY_BY_MAIL": SBE_FORUM_REPLY_BY_MAIL, } msg.body = render_template_i18n("forum/mail/new_message.txt", **ctx) msg.html = render_template_i18n("forum/mail/new_message.html", **ctx) logger.debug("Sending new post by email to %r", member.email) try: mail.send(msg) except BaseException: logger.error( "Send mail to user failed", exc_info=True ) # log to sentry if enabled def extract_content(payload, marker): """Search the payload for marker, return content up to marker.""" index = payload.rfind(marker) content = payload[:index] return content def validate_html(payload): return bleach.clean( payload, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, styles=ALLOWED_STYLES, strip=True, ).strip() def add_paragraph(newpost): """Add surrounding

newpost

if necessary.""" newpost = newpost.strip() if not newpost.startswith("

"): newpost = "

" + newpost + "

" return newpost def clean_html(newpost): # type: (Text) -> Text """Clean leftover empty blockquotes.""" clean = re.sub( r"(.*?

.*?)", "", newpost, flags=re.MULTILINE | re.DOTALL, ) # this cleans auto generated reponse text (
timedate
) # we reverse the string because re.sub replaces # LEFTMOST NON-OVERLAPPING OCCURRENCES, and we only want the last match # in the string clean = re.sub( r"(>rb<.*?>a.*?=ferh\sa<.*?>rb<)", "", clean[::-1], flags=re.MULTILINE | re.DOTALL, ) clean = clean[::-1] return clean def decode_payload(part): # type: (email.message.Message) -> Text """Get the payload and decode (base64 & quoted printable).""" payload = part.get_payload(decode=True) if isinstance(payload, text_type): return payload # Please the typechecker (and make things a bit clearer) assert isinstance(payload, bytes) payload_bytes = payload # type: bytes charset = part.get_content_charset() if charset is not None: try: payload_str = payload_bytes.decode(charset) except UnicodeDecodeError: payload_str = payload_bytes.decode("raw-unicode-escape") else: # What about other encodings? -> using chardet found = chardet.detect(payload_bytes) payload_str = payload_bytes.decode(found["encoding"]) return payload_str def process(message, marker): # type: (email.message.Message, Text) -> Tuple[Text, List[dict]] """Check the message for marker presence and return the text up to it if present. :raises LookupError otherwise. :return: sanitized html upto marker from message and attachements """ assert isinstance(message, email.message.Message) content = {"plain": "", "html": ""} attachments = [] # Iterate all message's parts for text/* for part in message.walk(): content_type = part.get_content_type() content_disposition = part.get("Content-Disposition") if content_disposition is not None: attachments.append( { "filename": part.get_filename(), "content_type": part.get_content_type(), "data": part.get_payload(decode=True), } ) elif content_type in ["text/plain", "text/html"]: subtype = part.get_content_subtype() payload = decode_payload(part) content[subtype] += payload if marker in content["html"]: newpost = extract_content(content["html"], marker[:9]) newpost = add_paragraph(validate_html(newpost)) newpost = clean_html(newpost) elif marker in content["plain"]: newpost = extract_content(content["plain"], marker[:9]) newpost = add_paragraph(newpost) else: raise LookupError("No marker:{} in email".format(marker)) return newpost, attachments @shared_task() def process_email(message): # type: (email.message.Message) -> bool """Email.Message object from command line script Run message (parsed email). Processing chain extract community thread post member from reply_to persist post in db. """ app = unwrap(current_app) # Extract post destination from To: field, (community/forum/thread/member) to_address = message["To"] assert isinstance(to_address, text_type) if not (has_subtag(to_address)): logger.info("Email %r has no subtag, skipping...", to_address) return False try: infos = extract_email_destination(to_address) locale = infos[0] thread_id = infos[1] user_id = infos[2] except BaseException: logger.error( "Recipient %r cannot be converted to locale/thread_id/user.id", to_address, exc_info=True, ) return False # Translate marker with locale from email address rq_headers = [("Accept-Language", locale)] with app.test_request_context("/process_email", headers=rq_headers): marker = text_type(MAIL_REPLY_MARKER) # Extract text and attachments from message try: newpost, attachments = process(message, marker) except BaseException: logger.error("Could not Process message", exc_info=True) return False # Persist post with current_app.test_request_context("/process_email", headers=rq_headers): g.user = User.query.get(user_id) thread = Thread.query.get(thread_id) community = thread.community # FIXME: check membership, send back an informative email in case of an # error post = thread.create_post(body_html=newpost) obj_meta = post.meta.setdefault("abilian.sbe.forum", {}) obj_meta["origin"] = "email" obj_meta["send_by_email"] = True activity.send(app, actor=g.user, verb="post", object=post, target=community) for desc in attachments: attachment = PostAttachment(name=desc["filename"]) attachment.post = post attachment.set_content(desc["data"], desc["content_type"]) db.session.add(attachment) db.session.commit() # Notify all parties involved send_post_by_email.delay(post.id) return True def check_maildir(): """Check the MailDir for emails to be injected in Threads. This task is registered only if `INCOMING_MAIL_USE_MAILDIR` is True. By default it is run every minute. """ home = expanduser("~") maildirpath = str(Path(home) / "Maildir") src_mdir = mailbox.Maildir(maildirpath, factory=mailbox.MaildirMessage) src_mdir.lock() # Useless but recommended if old mbox is used by error try: for key, message in src_mdir.items(): processed = process_email(message) # delete the message if all went fine if processed: del src_mdir[key] finally: src_mdir.close() # Flushes all changes to disk then unlocks PK!%%1abilian/sbe/apps/forum/templates/forum/_base.html{% extends "community/_base.html" %} PK!gʬ 3abilian/sbe/apps/forum/templates/forum/_macros.html{% from "macros/user.html" import m_user_link, m_user_photo %} {%- macro m_postinfos(post) %} {%- set author_href = url_for("social.user", user_id=post.creator.id) %} {%- set thread_href = url_for(".thread", thread_id=post.thread_id, community_id=g.community.slug) %}
{{ post.thread.title }} {% call m_user_link(post.creator) %} {{ m_user_photo(post.creator, size=35) }} {% endcall %} {%- endmacro %} {%- macro m_postattachments(post) %} {%- if post.attachments %} {{ m_postinfos(post) }} {%- for attachment in post.attachments %} {%- endfor %}
{{ attachment.name |truncate(70, False, '...', 0) }} {{ attachment.content_length|filesize }}
{%- endif %} {%- endmacro %} {% macro forum_menu(global_actions) %} {%- for action in global_actions %} {% if loop.index == 2 %} {% else %}
{{ action.render() }}
{% endif %} {%- endfor %} {% endmacro %} PK!74abilian/sbe/apps/forum/templates/forum/archives.html{% extends "forum/_base.html" %} {% from "macros/box.html" import m_box_menu, m_box_content %} {% from "forum/_macros.html" import forum_menu %} {% block forumcontent %} {%- block forumsidebar %}
{{ _("Showing :") }} {%- set global_actions = actions.for_category('forum:global') %} {{ forum_menu(global_actions) }}
{%- endblock %} {% for month, threads in grouped_threads %}

{{ month }}

{% for thread in threads %} {{ m_thread(thread) }} {% endfor %}
{{ _("Topic") }} {{ _("Replies") }}
{% else %}

{{ _("No message has been posted to this community yet") }}

{% endfor %} {% endblock %} {% macro m_thread(thread) %} {%- set thread_href = url_for(".thread", thread_id=thread.id, community_id=g.community.slug) %} {%- set thread_length = thread.posts|length %} {{ thread.title }}

{{ thread.posts[0].body_html|safe|striptags|truncate(155, False, '...', 0) }}

{{ thread_length-1 }} {% endmacro %} PK! {%- block forumsidebar %}
{{ _("Showing :") }} {%- set global_actions = actions.for_category('forum:global') %} {{ forum_menu(global_actions) }}
{%- endblock %} {% for month, posts in grouped_posts %}

{{ month }}

{% for post in posts %} {{ m_postattachments(post) }} {% endfor %}
{{ _("Topic") }} {{ _("Owner") }} {{ _("Document") }} {{ _("Size") }}
{% else %}

{{ _("No attachment has been posted to this community yet") }}

{% endfor %}

<8nv7̶Fo84u~1jL9mE?U ʮmOMG}p7i,Z:7/WN3o6G3çH#|D o"14|zfE"cZ$rr(T<r|wIvS>C1/6=U$؀oS Jw1!-!?D}rBʽj@?84>,'~P l]HR  ~e)_{BXŢ Heu~p%_FSʿ[MZߖHb/Ho0/7s#?wޟz"Ϗaxw0=G}u # ?~ǁOo2+L+reqG2tCC|iW_>._gN呚j$P◦qt5O7==,S|-s=QetwMS.^L㺏*:;MR?n[e0~zCyhO?/g =~q).[5ЏqM/_UOqlcw\6>ʟ^D_'z}]Ͽ 3=iPU/4*۪^ZQCiz~6,qGAoa;ak>i wc >0S S/S ^Ǐ5LWvr/T=: u K*~'Og A: QXK 츖>p-z Ry̿ CNf. w?ײ.cע_EΏ VMLZSϺ C'k`rFv7\?&(ot2| 悽0} cyxxxKw0i^ۧv:LoV`k^e0}I߇q-Uq{^Φ?a|zךevkO^?{Zys#5I)ffM_R?sf. ycJP3{Sڐ?ϡayb( Zaao_Z_V-}`E~ O^kw{!*yiJ+Z>kMR>VPi^m8?*]jcN=n6/u5\_(X;%8*F}7S==jM ͏|mOiC49; Uhz8Ln}0ƫٷAΗ6 e4\~|[Og,>Y*_8N{8j%8m??8=W9NϿq {?8m??8=pF0Z;'>5-/\i>55وn8em{c㕘uO%]~zK 6ti ̲Ŕsr<9Atˋgo{RN?|?8g^p/t|hy6;ͅk֐qM<&v\ n;?m\/=QŭeZ -ʨ*vIl6O}MmmzXT/V.֗:|l/\<_l#+񉀫$m}x|}VlSf G渞oxO(+\,s_gICbGSђ㷻1v1֟tO{ehz@7RSF}-+1~Muz|8_}*g O ?rxIr>Zme A'?4~\*^Vҏ%T:??ƕjP4+R{Wz~~+*im*i|ßXmJFܱJچmmb>@q1@O_͒ 4~†)m=i%8ŸyG_p|e/אuh$BϽ\~k}]R!Xq|JI4?'G'Ly{pZ=}\JxOx W*kIy ̤"g G?^/1BY &_Q:$kn-?(,K?@C}^Ī\^m'pt蝵5Kk3/)h]GCoף&~~%ϟKhhȯ~䗔aGuMzTVN9K,cD3=Qmfi[1s:e9tKx m >Vt`>4zM/O { NO_2g>LŰ nSα}|f >8nk=Ei@?KZ(,S{=pp?h5xE>6}Llk50}cƗ@;2S~>с- %oڱ=)0i2>IYK~ߞeV}`|5+{:{AϦЯej>~gۖ~/~|h묧۳&ͨͮnp>>77J@^ԌJ$2e=ojٳW#/ $k_>7M^1˝C> >JR4?g/zeswӁ,'[{{]q}hf7֟<꒿w06?w064w064%kpؐ??=y~Xi5V>p%Oi4gJ#\R*.Qa]5=8z%MoWW^BS=8F)`lOUԿ;i^@S0Kj81 CM/R} bYa~%W^Gmf]&5sØtQ}Ŕ5f:6VTc}Ω' jLWͭut Ӑ';!{N]R.] ~NiӢK yrj~tozHn߶o~{^g?&y߾>OYZU󯲴YZOYZ|h;ei=1K+?zYZ~ڈ;fim1KkE66U27b37k7/:iC'- >:33a)^)Kms!*g-1 jϠ?eV_.SY׎cb˨_TUXx~ձ}ɠ?ث2qvq6k`(WkcyEx0g%\g4ޗMS5#Ssx6#};tX32c[ ?ָ @1gÏ9 ȷ]5Y vEE?'O=(jd bշLn=wEYm<yW]%Ie^?8/o1s:Ҵڅ؅̍+ǞW߽k;_ϻ$_WSA?AY2jd`/jdwMoϻ"o+ ͢LJC"=]SK_tIܻ ԟ ?]OQo/@xW] ??++SwE?+G0Ք#/S#>Hm)Gj#{r#}TԣQPHHmc?Hr!A+9R[c$Α~ ?Hm1GڿHm*Gj|ے`MA{xJ 9SdBR_p'To/xYFXPρ"_q+]>q_^%B{Tcw?KSxIO[+Im8fy݁P> 8?@i _ﱒo~,+KO^Z%X?B|T|{R>=/85:׫zR@ƊOZFJy.?p,FV Om[' -CvPUdzO{<>M0t Nj]Ps;9gOg|o= Rsxp:TogʔP]IT|$pe唤/0I8c/.VMې]W ||qW2`gF/? ^']`M,k%Wp!K^t}K ~xmOXm/7&=$p~rk[<}+EgqjB|yגi/p[O:.mR> A"K;w׻}\gf A'vڕCI!:hoy*߫ӻߡ`:v4d6'|k!ZcVh Ӷs@hl쁿: {~Yl쁱bP0ɴyA&ðOs:+84NP".(M̡b: rSY?>zyΏ+>oޥK,iaV,++Lp&zwJie464}h`ᴳY?~[ ޑ.λ6Sn- ]`(Qu/D]ЏEw;Mډ?)J>`/h'q' ^^p>J"^N =RV,?K݊nc}oגkh^=ȗK{?=_nߪ> E'J< >$}I0>ܥ$/^>PA?ƒ=:pW}}/+/t]/K}t:^փJӮhopoN{OS>pPlE / ݌,!E.ri`gc݋>Z,ЇCox96=5} ^"/Qe\Io)[VSPL,_/++C=$#GzAEOE"ǻ%}Lܿ^Navo8 |U} >bcYG~[֑7y(п2^uI|~Yx?yy0>ڊyADvm_*.jC2|o0TA ?e%-ݾjp~cץ|Z _ ?K?xmiQHƂѿ#m:ȟ#Ugb3zNbN3#)ɷFKf`(͏xӛy&Ӫkl*׾w?8_RLV"^$7kAQhWBYUx n%UM߷v^dv"Jm_:gЇ~_c ><_>OIxUIxϱoTZ|[kX |A瘣7N|,)HiZLխ>Oz9N|D'kLkbXͭzgG=YKo'fߓhr"ٻ<޼ٓ1?,.K9}Oz7'i@ηb}ͣK}7 -ZZl9}>XB^s%Q~PNۋhnubšOT3h~|HP`}j^ 'GrV?.g{KC5/^ouS1ҩ~ג7&Ɛms۔d/j1s]OsLʾ5AQ'v#g'y$ɷ[[{ʪ*G?]Ն]MSVG;Kg}<}`w=6OpUtP_GȹG>֥ta":_:{Kiv/"?o{[ǭ,YE󅎇>I] #'یƃS0; Џtk m8}?>Mx"<^\~<;wCQss;&5o6Ǩ#)~#w=_ėm6po_?po*g؃c4ɜTZ]JJ:ĢZӃI;xZ%ηbkJ_F}\+q>. lp t?> CatKOiSϗ{t˷@+'$2W<K =2 x(_G{Z`8W+ GXBʯ^z)Z*'y0уy<bGɼrS/dӽ o.!=izh9^G}! 9/TN|Iyg'r%瓝C EאsxK[Bs(j?*Y3q* |.5?.Br(O-*g^aP ] ›@QyLmzXXVg5ߔ.=z0ݵKoK[^(uݒv,jgQX,9gZųK+$iN=_7ԏ^ƿ4;7\wW龿so?,S-.^n1З=|J'HٍQۍQ-_T!腨qJ7Z@; K S }ghǦ6Iqƥ+~a%oqm<'?r{S{ԟ qKNM/ܵ*=I}kqxa|<maͨx~mOrBA_Rݾk8`,%˿} qҵd{Sa 5—-zt ۪Kr~W|iƒUm/XB8^I/ &[\5 h 'vomFy|6#e=h67ZR߬*h|Y/:ՔӇzk8 ^@:mM S[ӞxóNZX1y??8ߝ87Wݞ ToRy` N?{S1O9a|&lq<Jo\7|7S鷽?WbXśX7,Eз _òNa{69Rba7g9kX-%% ܵZ"xV 1ߌAhGN߂^7kŢR.9>{bkOW`ý!+q:R^~c3~|t`?(zaN(/&llcvwϿ(6Tbx)[6)H*:is0/Ĕn쥊9 }t9G:tŻwK?Ȟ$Ac(bT;n)s[h_@pTi|Ir+,CO;sU[^7KMU^K=i_@_6XfwdLj=ˆ{ͯuAMCoYVX/rlџ?0%9H]pߦw'QSl"?0>,4I / tj=%VӉ;E%D)d]ME?)P,;giaL,{|kLRt?s:"Z~'Pt"~X>"X5|rvJG-S/ҡ})`p|xE&9 Z8ʏEx|/o;oTը`̣Kϰa"+_YTA:+DP~Al?jrY] GL~ `~si~S"(xU$O'Q2@^~X>N$ jR)C'|Ǩ}~_>R&w'Ǝ3ȚIa\-^<|zo5zWk14lBn?F35?vxϾ+Pw~+hכb` M|zmg5(֞ލRV*+NMڤY}k哇/ vMW<}+(+ۗv J]A8w/+?ㅿ8iWw^ȫN^yj}w0iW]A㯸Ӯ K kp#k߼+~7/MoE-ڊD Gd)]wz=lO ^S'Ǻ:4{R_;V{iz\/rҖ< endstream endobj 35 0 obj <> stream x}ۮmݻ~ݺ_F4  gizOq qh).i&(2q0 _SXo#?[?~E_caO_8c| !~׿߹w//3S1ث0_׿>T/3i}o_s~}pwc}ןk|MODEd9[I̸;-9q_wwY_r?/R~[wu7,{XK__ׯY>~|+ - .i6ϣy|-z4ϣ-'xf˅2j n?^;-ʰOW AN>E <_Olz"X'"p;@~?G hUƪů ~-ͪi ӎGi4/O ;‡T|[$h-- /z|+7hR'|XHo%wOӨraEk&`p|?{zE?ö˵ҏ.7X/ pjnA+`8 $m ʧdmK]_-+O:;_m9E`\p߮E75G N'O?ϤScpbv<9MCr&& 9E~?7h9- ̍Ceîp>xrZP贍7"&4S6Q I'P?T~ŀӴ~4=i߼7\_9v OCs'ggw\_S.߇7 k7?_d70_[Otbb+;)Iz4~OrK=p|~_ QpCg>Tܿ2?5kA @ؗZOs|oƃ}_߂ wwɟ`CeOEĬՀIZтQN9kԺE 7Ae0-a+Jrq^~J51$9̋DQB|'>f磣nGCq\=.IjUz%?SK)%(I]/ee`GhbW/[ԤT.sS:?|9jj?9'jzt4l2r:-v0Ow/Gwt~{~-u wfj>GEȟ|Ѵ;=N#Mj nqD[(km}іԳ:hgEN9~4p_ &=뵵 ŵ qKAҿ ^ _[6o:6mL{A0~+O ZsX[o oq}˰0lr Y8튰R2n_Xtp9f+qH{P` 8Cɚ˨Zǃϩ >}olz|uS_K)ȟoIʯw_:8 rThZVjh`M[-eo5  iٳu֛>(_F}, >nSFچȹaQCvrOdI)s>E?aK&-5R[l%Gn~Ǔ'Lz8V oA/L#R43KS'}7:t)I|%/?AnuB0 ur(ScXS9 J:ˉnuԆ {\=ɠo?I(ߡh5Bn-/G{)~C<{6[ Aa  >PBz|+W[uHV| ȟUʯZWOǃѭ?}? KcjiR>6!~y}PmMA3~Y:E[뱥*?О75\ఞ~?1rud nӃ Aմ`EQ]tg^pѳ;o;;ov+|7 V=zѠnkEN\!ᾆ dY2Mó>]QflMc\⠷=R[ASV OY-Ejc};r٣1p<(" cC0C=?]UXxEW/[QWBI?xz`;5K)vnR+w<~ 䇱[8~[܊ǹM(Mv 鈚~|JG+}0j*~ /='ľ)?I)WF)ſ|)/^ewW:~kNvmG/{[}+%Ů^ν@E o-DWTfHt++7ߗOV=֗v֗Kۦ\V 5Z0 0Ks_0>\)5 ,[I/Nx >x|/(=؆ق/U)џ q?lROdptdC'j=w=ӆ<ˤG׀?aN=!}ki#'=4II~? I2ʟ}C֯ 'Za wߑ5}щ͸PKUƗIyA ȗ/U2/;#~;i(߿^~[|<>~&ޔyq`? "9W /t8>U'(H/ n({j6B>A?(Y'Hro _|1_|Zks_ð??^k}v]ky;Z?Y_c`Tb ('yhЩiBE hx RT{=%ֱﱮƭc+٫e ?Lj=/^yF>c[0" A"j~ t핧yyG5k#w>F/^Egrkǯ>}OJOD'ϵ7w6J\+]65}`]h)erAS$ hJr:}+ _Ѿc>uЏu0_ >~X`nSL۠ӢF5a+frM6x:M$lps-~:xwɾu,+W^lVd)%A7xz2Awpʍoop rzA~&r))Us>!;6<?>^U} B AN "+9ȹrOϟ?A˟'rSpAqSq}#]wAo *[}S/( r~A xbUӊBYۆmCVpW)7hɿ@^ 嶀07%i ?ʏ?!\M?G G538Xl??ּּ?Qn;9f nf [0[0VfzaEC7IҞE>&M\L@5B-o9,'+ͯ anO~PfI^qˡmroӵ.?f/1S5<5 ˝U/ϵ56?ܵ{Z5A~е~{ ~SA?8wG[S*AZ_mEuVUj>z`0ȇʵ QkT>QĪ$5J.r_Tɿiz(\zhm˸׆z}Ԡ*&m?j:ڏ G-Yt^+o Fm]  nÈ~Ho-(#J5r1?]"˥Z Ϗ7gz;1Y0JeFcr Y\ȗP g>w<'|,ېzܖqo9/:_iRj1`kv~SJqS, -CRՇ}bZ5} k4?@x]\9b8O Ѷ>0`1RW1__r.R275>QK}z@ߋPfS=!~Hp9('iA~N>mvM?+KWy6I_ AҷmըW*s)o-ھz|0ϱYG\Ԧ?'ҟ0`O5`#c 8r m,6BG}i.Ota06hIocar`O}mSܣazj&u|x˭/?\j2wUG b[7`X5cp!$~ kS-wmrmɤ_6Aδ^\ ǣ~a^Q4/TBnY^8K%2-OaKLڥW!RrAFGn9'N;?uO{2-_8U[^q+?U ]A -O燶MJ򃚔moTM?emO rs܏uZ5CK\ҭQ˰`PuN>srV 5ۗ0pV,-x\|a~}?}~7CNx`~.EL>Pp#zEj0 !}<ơɎ?^pA-8bOX1m-w>q.+BQmܫ+*ƍah8ŕ_;^v QӗRMETb\^2*j|s'kz~zMSO+'˰Qܙxn#~uO܀~OjWߚg)O{\a261>}`pg-K}Ԇ!'NV 'NVm8 UQ򷠽(kץk_W/|*Ղe 6 >P" sj t.v0`><D\GOw0@ZO|Tɧ6 uEt>͎jD OHM>BOaJSl'q`RN)L.&_F|:/5՘ΐU~y]E^o 7,~|WhGXx|AJz~Q%7=*}T%{ 9֩ t^*kwNU2t |g*Ωs&׆ [|ga]Dݑ:<-6l9mv8XF)F>&ZZAe;?`=z5h׎`g6R(?I 2ӧG✒F􁾠>$|r(R_3ȟ+W VU|isvB`VX0qlE) Q6lӂNN3n-5mώGY Ÿ'7ܱ .C;Zǯ5s4bP/m= >}~bt/I|\L#Qb󽂫ώS[}H* L8 LA01}8 ƈ9_ѹyND|rQ?Nπ3[ԃ }/W4|R>\EL(ac+;u|9_鞣F/s4X_xρ*fM_ _oF~A/i`(_E W /iw?B'g:3.o_p݅pw 3#*?h0jTk?Yܰ{E 5{%t2cvc14cbcPh/p+a/8 ^ -K{z{A㷌?|R.IsGg_-L9dA??ȿo ӫ_tkh? ?ыAU*?sg?m??`z-nx  al` 19KymV$64ptW Ԡ_-}.v_iV{ }Gfj'= V4E:3"O`F?#&wxpcS:jZQ8٥~~aKwsr-=;Oi>ק|ѷ Fͳg9[y[otY/pnC. ~nlӝ) i$~Z. rÏjd4Kdm> x.Ki+?7T#I[o/ם pOY68J/  >Qx?hfl+TOCb뻡}bo Ř]twz/xOwz/x}trl~t}tr|s;=\7;;=_-םNO2jFT㝞uᇏxӁ y.S)wT%ԟSC/׿u!WxbC?~OP>+R̟?T\AD?ȟ;x.h>!;OK?UZ L>A=A ڇ[AԇS,ޝ7| W+tIWق0;٤Ԑ%֝sE?tw~yJ;=1?WR g- ;>43{F =k =TwnA#\O _'V(1;f{lc_ȏսwFw?!9opsNUIEse j{| 81'g̚_TާX}3S#ޗXys0aTSnh8X%>RO꒫/wo{2x 㩪{π|Cgs ҟa3Z?2GR{P@?Tނƿ@:{/_8wZcP4=Kf{,WITcn izޓ}@>NS'5ϓڷ A5_cf܏kάiiZ*7Ԁ_yx'77x?/3RP.-}9F >^jK/{iz/ p ҆>^'c%1>c ~콴{i콴콴|=1OϽt "Ճw _|W] N;s#?SN%۬z- 8+"p~X`$) r}Ɖ@hE=oXoh}ˠÌTa}Tn}ɠ/F bnhǘKuɿD5@D+S|^^Q?4C?^c4@~Tω|l??ʿʿ?ѩ;T~T~4\ݢ^'0`'?:h"d`gRN{Nry vE$d 01\DxQjP:ȇ:d=^g6)9P]N=Cl }EUTX_J6oIlpp؞kg4=棼 3i~_-ѩx*M㩾zA6]/Nf{v,B,=jG\%a[N*2mXۻr_AQ(`/SqoC;[<@~BH(_NJw%eU?=0Y[U|ES7BĪo/iS >np*TxqGo>H^#$Zm-i !cX,v9Ԙ?aKVh#YSJoDײOРf 3i nٕE0/N<'mpߏ?B&G \ "}d+ǧoYm~:(Oڤ~m<_Ӌo) Eu. E!7 Mã?ܶ2#NǮz AmW:$f7|NxѳUV Q0lMWC <]9Hy/%}(`_h_(lDgxAa!+R(Ǯo +ԫOo>s _n9$*ԋdJXcQ߮',q K\%',yxOY&Y0=`z?j Y{/c mv9, 5\9b|m-ǯӛNn;nëۀצG bK|sZ>iiR1 t+ŗKX|!syP$ GnSyQx]}Q3_AF{@d@-RWP1~+ }_ϗ,|v{;/٩l`bYf'_p wfkr=99zrr\> \<+Mrn֟ԀC>*x9 wVR?:eJUۯZ~][Uې7`?]:?WO z5?l kހ?AݽR]|}h`^vsi76:`*viGgs,5gﷱ`cM;:p,߸^PYͶtaOgL >,[ Nw A_=]F,꬟7\OJ^]]ЃuP>P |W}y=/#iô ~QJPƬ^}S&>2ߧ@sT~f_ ??~@{"W ?JYoi}(1ixR?J$W{$~ V)E-Z ?]GV} Oʐ+'yjh_x*Y/RַB][QJA{ˇEI`o@ۛ _>XW1lO`Oh !v5ЇTztS/bn^nޟʝ|StS1f`\P?x1xmNZEP,kaYz\4~~磌O+_wق?66z _5_x{OܸH$jNӟJp?Cenz$zݳ4z]f H<>R9T5_ /xAhS* of+Ms#!9~PcLGf{*%X,H(`sR>%__7?=ЛyA_#2p7G%=;KjXśΦ60:UӀŜνiȢykBn"WʍJ]=cs}pгӹ~v1iznr C -ޖV%tΜ*z)xA>ly)`K_Fp/2(/!}@/%E/B`K.p+?RzA7A^@(R?J!/%v)u.-'/ֹ/;;z)KV楔G/|Rg/x5ɗₗ%*^Eeg/xZ㥼vR\>^ ?{)ūx).o/ŕ|/兟~R|Rg㧥R^Kyg/R7o^G+H_N[V8Nhov44y?&6beFiXШ ;FNRA t7Z8$뭳7bS:쀚-76~xߧWo? tXOh ? c_R[mt_Ao%a\Y^iDsG|XG ;͸gF>R~9T9~xX0}W89 2p2`ߡ(_įY(Y9D-Q/׽;#;z.hk^eߔ~S`A{X@_8 +UFȊ_V5=J(j|J|n'wN\췱 txTɖi109͒m1h?>;?EIad^Sr/A_A3lAECTRWtK As^>HB/]^$0$4 Կ~2Pw:$)J.2L:e?;efwNޟO _lu۩C ED .i GKgxSJݐ=ӫ3A׆_,t j62|R֣ Vs)A8S*6\wI[}A lP><KEl6/cw:d(Më/a RFfJIp?5IOU͗sHҟA|\~ڥ|r?Mۃ Rўe A?4DG 5ުP!wa?a?CUyӡ/AھP5 _&? 3ZS=ͷW#=KK?tv*[I-)Yh?]%wjsntϿD}1/W_[԰??##?1i+سv ȣ 8ag+Z1X֢'7hM[E=|7 矿PY fOwKv>$x'*1A\JϾ#󏆖4uO/@#n'7-)?H{J?:z[Nߧ*eJw#|,v~ k^7>:=k #k OOO/GHguIsEIϏ:)W) ޱ*G?uM^cUu~K_F>K_"#Q"ğlL ɀ)n)s1<1Hcčx-鴉';"$W |>E>}G6+߀h;S+_DMq2\[78'~y[3ݠȷN_ *??_@y[S.؏sT1OM?}$ᒤ~b?}@'izl Zʺp=NYSi>t*Sw~j#A_>ԗBg ͭY ||_~x? /[˱.x+ WB~pp7˩Wm-{Sw4ߩ/%WQ '/'Ic N^{9S4~?yjxߛ?_@XXxF^NRWo_r>ʂܵh.Og2̴R |4%=BO^S,;jZǗ*euo7j Bpp1|f~PiWA0K JE=E 򿵵]i=^;,/}p&}v~~6l/W[V-#;fG ONN^'腓%Ԁ*_K$O.tmRAjG5=Ij -L-)ڹ*]}C cOK_??ynC]%z4~zAO^_ 73sHj Y6SPɨ?2I<.??uAr"SrћnwQm7|7rK]p>}Ϸy+׫Om M4INjly3v\,ICm?*='Y8X>54dкY%|RM0VK3v]s};?5FA)?IA_BAX :J A-ɣ}G)CCsj\\b2j)ֆԏ_Oʏk 7%OJ;sW>]-PKGt{x|MW/"@I7`F~~T|=9-ϥ^?d)z[u]kr}y~jqvhz~ 2rދooZW-u=~[jFm$҉^T#~P-Z0*״`lCV ]θ>EM%m`,W,_ȫv9{`OGE?^^H(_rp.cE] ¥+@FC=\>5<]#~P"{&ҐK3>arq)jqJ{sE]QOs~f/T$TI:h:r,ܮo]Am˚_FA =@>Ǎ`~~s[9?_GeSucEU.P>pT~*^5T|j؏cv9|**okv"TE_b?* ~RT4{wJ_jhEퟵX[aGg~VgE]COQgr}veet`:1`v1r _yտ^Z=^_CGGuvN zPu;(uy- Tu=[ypF?/O\g+%[OEsb{-Z R6S QȹSޟS`K9mc+s5A܀+*-g".Y|DPsr +Yw֣͢&ڤxV򀘿(îG:?Mu|lpCӔMŵ2|c*1kV?ۡ4x=ΗX#ӱq%Mo=W[>| >?5V͚9vytmOeB~ה%;]F7R?o2(ӂv%n^ŭo0 m4"$x}S.K~{wr mCßOl?Uj?7'p6\_ѣC٦A3q1U'V~qr?/BY8^~Qk᜕iÍ&W~Tݴjŏ޴|PM}3iL0>t?X$~Oj鯯 qKI[}=`o7 p7X68' ):p>>˟Ra`Gc(GhkQ@% ĪQsHMZk.o,QtrGz?(JKR+7ř*][+f?^ cV Ff% F^Mfe Q巧E ڵt?.Ǩ&YreHK;I )+Qr9vyD->DK5ƯQE?@>x\|~ Oo˒P?0QT̎Oz a &e0`6FEYȨa1?FE>-1_o!#˪$ކmpjcyKQMsL͟5?xcj:,`|WԳ?6z%gXeSRV 1r?j^EUhF9!~>+i 󿿒t3\%2 *? Ma+9W?$=YN]9Y]>滁|YMGi4J/'pZV#z]FO$-XS;='cq=>T=˖T=ZU-h7>\>~Q5~I깳G.-tI^Rȟ~x2ƿd zqT_> ßd/z|׶g >9hw}?>q/ۏuRJ?}D_O.^pJGN67O2O'%_}MVMUJDpXT%)ҚL&}%* =Q+ȗ=.Qt2O=^=||_R?+trܴ]U/?:BrYU6Q>'. z5}/O> gL3тOy/ȪaQd) ƓOl ee^0x}P*Q_-68:͟wKJ'{W0+ ~Ï"ko)h뛈IB_:4UmR/(!߿.Nmp+rcz~;hxvrKN/|Pê['QR8'Xk ZWzt8zYWFבU CA(lFx4qIh_?#>pГ)YtrYڞpTh|]A_AʷSgAwjjA/R?W-'招uT?JA?Ğ5}o iw؇ koV7w%5gi094UM0춼}S'| Q9ޝܛݹ-g'g|oTN;r3+ t nӱSy6y V΁T*j|.:v\x:kQj*r*r}яz;7|M/$ .~6?;n=^U㟣/Xk$"[M?ޭZUV^A6ͤ>2rR3ˀ=j [SҗA?h6xkSe''#^ f߭:Nm0_ >~`nSCۧ6>xjz}sc+mv<5i-į>jy>I>-OV%c̤ .Ow cKR~oICwQ cA%1 T{@ Z**_< vYHK/oQRK:ݵe=~~?c_D endstream endobj 37 0 obj <> stream x}e9z|Uտ=4`>γk)lq$]PH /aw?F?_~_ka׿%}c:x?_[^?5]1Uvk׷@ o Z6k׿վS'"[}z1gόCS_oȾ~_?wO l,~n/F~珋xӗcS_?~˨嫗xwk)sn~m!>3R\^?z_Z/<_e[-xךCo/4? 8d[J0?/Q^~^-25X\[cڴwh]l[?,x@͂g&e/;xCxŘ$m16Ix-Oz%1w=~*_µG+_V^7OA MqNoAŠ6Ԥ<_S4Ԕ%2I'pMoj];COVcBx x)RrR_2GxArRsr+a?e7' _B0%}U@g+-4_/.'ȿ!IaHTxVp|qA~2?|_ =~оi_vWz~K?߆+#O oQa Z)W=8_NV0j[ߐZSLs)z)Z@ 3HԦmMAmaGK:X- RZp/\flAW, ."KE=<7Ң  NP$~ '-8i&S'YF_.j_ ɛ[πQS`ei7`~0&^ F1,]<L :P$+_"꾾\ɿKܿVkqg"|l8=A% 0S|1 O +7Q0/wL/CѪ|x 7pZ0$~DPX3nGb)ԪZ!D3'=W"h6^>W(0?a&3 k?E _J⩟wdun[\A&tՀqvl V%^ 9 Ŏ-zv|Znp0x-a-?,D/Ni݇\$}+ձA)~`dI}jaxq4n&1'gZ~&/q?wNVq?(gk/8MfHS#׮{S78y4M F1HK "!t}1YB/#ytmMo9J>ZJo9`~+c%+=y9?y%{|Ý҃9_mN^遟J.2_J>fJO?y%;җ@N^ wJJO缒Y=O [w^՟;g:y~++J.ßs^9+Q~缒+;sJ~(s^鱿s^ )Wry\yv$l䕞缒+U';Wz缒o#s^ɧB+2WyW,ҠLӗۼEMAMo9AWnt3m.yx=qFr:I$Z܉&w":r x٫rFa#g8sLl ><)tO\tX ^-47([SyA3Grԇם8z>4}5K}z)ԚpQ7OZW: 쿠b"%AA{œp[>ȷ|qAVwwҊƯk{'\)8bk(umYD?Fw"ȇ刿()+3FY|iC{w nQ܉Ǖ^#{ݒwO~% L% LocT-}ḀT\ /whbE((jtV/0m7 ;@~\]F׺|lvxNY"")x3:nb߲d{M s?hO~chȓ@c.dS NRR%RgZ_2%e?3?ȟgM٬d/?{KU{`|J% Z R~kG Qw]zo1+`]z{`//%cSw/.~Ҏo 5hV/kui/7ߵÇϑ(O\j`d7OORC߭bCQךdܓӕL,DE7 5]UW-tU=)[avi%.2?⟇*۞&)ħO&7v=__i{Oaap.Տ-hOFT5M_{+qa{~_V.,i86ikؗONS_YIle`53o/kFnp*+`G-OR0UY)3e6Q|}3"˧x1570,^ڦzݶƥZ)Z g ivODe >`É&ɏ%)u8aßn{ =@sh9KxZ G9p?7QB{ރR)=8%E|yA|53bpޣa"m9}Ї{Fw1"e/=o50C _>P-j˷uXwnoS/B+AWB=i{~)~) C!~(_P~tx oL}DҐW*OZ'%~OMk;]n'b.&4u`~Z:ݽ_xm}}7SlyU3DT_^ %7ڽ6K֧1XEZ1[) ](XIR^-s7gXOy*;EQt*CR>5I Kd8dJ LWI KumAt9 s @lk44;u?ݾO4p>v$OH6z%Zs3֬=Uͯti~Ӂ,vLw`&ַkdӎ7 ZƘڔM(?DԎW|2ȇR_.@? @ߟ5~ ?,̽hno>sHJ|WGu/5E/IW)MʯT=]/] "knJ͠ǟC_qŝ-o^Mھ*ʟQ?ȿه+yn.:jtW_u)>1H_wU û0~NhoE{F)VIeS}[?5>X9d ;6A%9vGW6lk*{*vl׸ȝ9QO~uQ_@4ToS &PT+RZTρQRYݱ SڱIUN1HR3=Qju)y"?A\5T>PO~m>U+8mJӁrS()7]?҉?TI(QU'/*-um?a8U }lǴ9l,yӢFˡhq4ʚ|xd9˗~{#o6k~?mAOo(|N*iNfM+%VIkyc=!r;>.ﴓԭ==hBxOX>RڪKf)-b>~yH`|ϑm']*>=?!HC|ٔ+(?:^feyjJ_P純+tX~}J(-s5}SNKU?_:qo|i'jzEb0RN+#VK-i{j_=hx3+ iy~7k0ե\+lsRYy7zϰ&19EcXxwkvN<(#w=MgqnGH~S)1,]v;Ut@Zmb I$y5Cg5Sã'v/u|TO|^,w_NnW&Qs^P×gsJg;W۞΃% >$?ӛGzMm4{lQ"*坦~f03 ?/}tе#k'*)C:Cu%OO-?0_%ȟ޼ Vp NU?k~;}C翂_ǝ}q*?QWL+ᨏ 1b+ r nv|S((M<`t~===%fvzOGpV.}] ñܥf?gcz3n4V?32~zl1>b}O6Lkx7!n;1݀+.!D t-q0Y(Z3*e[;vdML:9Y'~OsG A_Y? Lu>>FqJۗ* Ʒ)#ao?XPI>~-> M7|hO v|JEK%R ʏqxΦ_>)oʋ谮K?1{kx%kːp'|xW {'`/&M4(4|Q-=^)R7tot┭N;Jvl>mte ]DSKv|qq]\Gj/& OCY$?׍<JTt_o8]ɜn'-2ǫCUwx׊?Ƒ'??O/sJy(d74976?,ՠ=dowN~Fط g7n4FklAK%[F{ۗ1f ThX]v.s5غm th;?Ul`ܬt > q_Dli |_a٠s}C̯n"0Ydܠ5x'$~c~ 딆~Xiw̌NM?QWӾέ=Y9/58W3mp|-ڜE!|[j/ AY"V^>.+" ƧU΁i ç{{:785s<owF|}: 1hMW:$~޶ 2Hsl k{ gg 02Y0lncg&[_5vl:g^mkIwq;lXڐ*Q@CFd#E?@D@w-8a'w j@/>n%MJ ߨKW-o]q:IY/W'}|.֏TߺDoC'_G -?pC?>ס_V2ȗڃO4]i~)t cE7}'*#|6& >璴\93BKrYӡW)Ln_t/pgJw8.Eϱ7 jc?:f>uO_1}5`>biMՂ9*nS$ftG^bmo֯ 7*9LDJ  X'nlgjPNzAĿfA7M{m &%b{6@"Tm9I5?S~&4kk/K_~oL 0_?_9[exDaeI\EJ1&peAK[XyIL&YҷΒ xw*5)SIxK[M%h7uۑ ȏ֬l݆kͧ *g;,u??*]NZxoï@р0vf~v~zRvk}zRSHԚ%jv頙$j^ϖi{MMAl[s(uG[aU՝-+LmZ(eԢR_ Ҽj-K@)֓Ԃ;%Z* jc)WE7Kh[5j+-K&K~є'}/Q(;^b=Gy *~VZ`X\*EkIA_W=ͮ*}-+M{;sC}:B)#K-[V@/\ `I_-]ʧ\ճHmIzڵ^(_z#J/Q?F_ o.0hŻELZ0)nX(ft w}\[ssm\]VFE]B.so?p~&);;WhOwa>bߵjő5^Ywi 8)W1RRԂԲ{SՒu>M>_KMyW&N_ 2Z9՘ʨ$d]ӗiS>JA++\xFIo4=̧߶Z //)p HV`|fJ)߉Yo}O l9a(R34>}GujtFZR˔Rѽ4ζ8kV]BRq>g?cOq>g?cclkz=]qƫ|Vz}oiʶozCDžqبC*ފqVv{g}\LJ}\^+Eg_/[o{iACy*?:jEؾ񮗯~5J#HhF/+KV.{KfV,k:4s9ВZBR,VbT頭,Wked2uM:J_ KȒa7q&+.h_koYʖoRlxT2Z&"`1ڊ\K&hsQ]ߢuŜuTi$GBiE)˴ X!KVڪbc֯,~߿% CBb.%IsD u%XR[Uky/K4$BBD x9 k+$^RI+uP^dDӗ٘/"/tXFoUjxͶF!vhjxM`Ѐ5 LzmlaunugͺW㗦ǯ| 3j5+unCkzcɭe;ǝu.ֳ>ُ Nȧ6#/`nzkWy'`5% _>/j KZH+?`o^S+> bYK n&?NpW_77Vwz+اڶtLp=ӳSI6_]Nkb25'/Wh72$OWT;5~+O:akNFkgxy?һ,)e)Rҵ{kZ^&8C3ȇA>T]mxӣ?LSgv)uF;0M҂^ߛ?_s>k?ΟΟJK U Z1*\YVQ+;≝<$VOΟ_+nG{J)~*otbFu "ۂ-&O AFde=_V| 0rp3>|:0o|[(K szk0-4ʆN ^Y2ܿ3Z6b-KacV78ełdfZ ؉.5i05'pge|~FJ񩪰I}Ym| Uj-{-!cXxPvON ){zfxvK c rc_ L Kd%  :h{maJC!ޒO[W˚ P?My 3 endstream endobj 4 0 obj <> endobj 15 0 obj <> stream xՕ7^ӹ+wi{rAьfQe ,@ @d&`0`f׻~k{quM?өS]]߻wûOV\&j{dOU5MNE=-Krp..\uv&n,n ~ x7<{:v-9~on'!4>P>G[>o0ڬ {'<#WH߭jNI.~nKtևʊ-sᄯĸhװ5xq fs8w^p(n o ?B8턷ςOܢ  ׄ ==M[V1?q+?v6?oy`5|7Re4! Ёϡ[%8p:Pk-ĕf] W4x7u.n+8[('34rX X x?/;`O}N l=@kx0)a ngacLQtY_qSazLyQ" 8Ш mC zE HM]22PBq+)+մK:W+'Ӏ艂P$7Pf~j6Y#w7zL\!.lMbMҊzPqzTq~,+Ψzۈso~F+S_QFI@N͌0JرPFeWN; ^,7 ߱Dآl$ ~o |OׁBoAeD8OEtۈr!   "oɰ"7LNE0 =C"ÊBEքJ$;1 GOGxF>8C " f8t1y~J("m.WI6:&g:X$ZGjJ. NHiFCB _!J>DH &QrD9g>HoФ&|Dlh}W9"q$p6Т By' 箙q|7ק,9 D"0@߂q /bpBwѧaAH"BaFf]tn)gHm@QĩOᤠ7D >\PM2մCɑ ;;v$ۄ+@V*8ɯ4SymkpuA; np $LѢII*m(q»C,lA  璃&<8;v+P 8:TT% 4QX%j@AS+P9\fQ@v6)$ Ͽ'HsIk$v .!Z[xa),(f&{T+h8+G(C Bg&iJ߄IDK޷MBaK`0s0^W<)J`G#ƌtBKWO9skL+t!?7 rV#0:W˅ 5e)pA^"$t7L3a(ђ'i'W sFHp!#ꆸcw3 Ih 4%& Nt ' P O4`I8D P #bT>A`q:p j37qQү+? D < 3<*:@6>obƜo8fRĒNmy6!:iTȍqfa2LL3:\b |0*2T3A6#͓ S(I£~N]+UQUh,q~b@#QVSIFQjJN,.2O^كҌx.JBPZPΐםATZ]k4i~i%l9j@tW"$%O!jfxzG?3Р`k+)C9O: =?`3v, .CHLvR+ $O kSR$p@ O]i罠HPydzt.#w::( 53K$!>5L"up|v1mL~3{\x%.(R'mΙ=R,ϟIZ}@-M|0p"I>%e)RW۠pU G/u,; Hd)Vk ~#UNidB|RͰ x ~ŞBǽxL*~1e`Nμ'nI MۍjƵi I Tj<BaBbd`ڏ,tAM=/:ADd8ScbA1]ԱCz@EFrisX+h`+OS yH ꂙ--'4/%(чOSIU}^%N4 hKsIʔ( o*yz$dqp֠j#'4 dίߟטL(/` ǑQIɜDY+O(s.hdm L6%ݙ,D֐…5w#g(Z:16UР#"MT[Hu'j( KVJCY3xC~H2g)Qs ^xe-"~%3^ pxAʂyY/~|A%(˥҃pUb0؛<%Q,sm.+VQr>S K|TtL&pE"/b x8_qrӒ-wA_w&Z>1B]r( Gp/:mxp` 299=mn 3W# Q|){` s>X:iUk^ L#M8ʈWš7#SYԭӓ$yJ#Mp#?JF0#fr%!/SW5LwTjfH2wlbd&* N =OVKTSL+Ϧ aN^/"1B eKbq9AH<.6h)hʧh8A<9 @).#s>~w0 ť@),)b䔐`:|y| {ۑ!wOl@1d]P}m&ܢE"Ɋ9RbΛ(#~NlMRWreLk|I 'db37s(s4b&-(͒i^bb&eOΉ0qs#_E4h,Pe\ P5B-!F؅l1S=C%I‡$9slj>I Ձu6,g}V$@C:䊡&y N$@\t"bv"jj* ? -+HiP2E&*f#]C)(Я/8S.{(dJr1T q 5TxEeQbPUQ3;ϱ><%`_T^L*`#bH5#tRDKZB8J̗+<{ev>HA1cRqx.3tVH$ (!o_ɅH]ZS%E#IP#M,qd0=]c*Rs2i`Udy*|:k8d}8=@(/KSJ2CTӿJx.a 4+I.WH$~QF,@{5߃2S@j/R  ~9?x :')‰9 Ä4{:ӊÌd|˧ 皆^MX1$hڌCC C S{ A;D1ݹdƪGI͔ƃ,Q2FZߧhj8 <`eYEH8g :N< &Ɠ+5`arwO]ǯh^ɬa:L%-ng~(/\5#VѦ$.NXy}!O3Pn)ㆊ]G S:jW"P1O%^f<>I;3G_aF&-Y  CYj\:bYpA< ˙X{oc&TiZPU +_{qEOez&ByԆ桠Q |M4.Ar u4xb.SwvoʂO*Rp+1Id됌2Y!D`k\6k8{*(Pc)e=Po r":Sn\y6sr|ɦӀW!ܳeW+SuX$;Y'%̫_w=ѭA/F#~5 Rp̏KɪG=fM oJTȹ@k[EwG%ycmo 2>=NCE'+EL /+ZoB8wޡ'؄H:"؅%+:*wos|뽏rKZ~HUSh,GݒXP 7#|̘G+&)AZ2H>vqR#A>sb )`̝I ]BŖ'5.*Owgm2t{/[Y9cjh~‰Y=]MEm}igV]@% X E}*Ќ0P)A"R , S2dZ͹\b2! ICa d뚞xk?7v\7*c#NzGg6]]4842PVVxO#W4 \"$A HA@(O!JZ8MC"ΚD+\qƲ#xbUWnٻblUŁ+gϟѽ 1)V[ҚSyI[PJ웗SB(V@T}ċN@-2P؋zhMA$ؑkTxVDž/}DaMkvnشsM{>;*aP+[W=.<]@\14bT+ gxO}zzE1\~&D|i. 5]jjϙ ,UT +qO\iouͪ'4Fz5'|ceʌ]tdrD:J\cxNT@3]Pߚ$H";f?"9S8U%w,@wjwUyEj357_>=s&@Tp8-s3[tI^t(\ʱ%VdA2\ߓ̒*!.UEI<ҹ<uz E(X=S|쁖JO#i@s;{\so$VcޮޖvNE 2-nT{ܪS䕲)3 }2H*瑨p6;cved $ͼ"%@*ь<1SL' tRPU +v)+5ҳ+nl.ڠf՞y'2ɸ)~Q-ty+MwСeEtPt2站D=EW(B$^d|-O@b]P5;ХnQl3q. y!<ق`Xɂ*aXЃ)zd΋KG<=>=RS{%3e.>Ձչ$\G٢v Uد=kv `R͍Um-T|vKt˞@PR.1Xo'rn4ֲĂ)OA 1FL S8 hr|iʕB1 Nm,|쥻~` L&6S4{T.漣:\ ;'H-hC +\jS!ӆHH;A!e5 +"qPsJAvDQo(*h.Ǯl0T,!Mrxd;yծWO=O>|H(Ux( 5Q@b‡Gx\| e@! > a3}̭Thetx_qm\ZB!dKې\U]< @܌j'˲Lw_y_gRWʊxY-5"- [)" G5)MIqg\n0i8щl%o)X4O%6ܘ*X aYX DGHwb4靯nݟ _w?.jA'ި$d-dÉǧ磵d*bٜ.y\y*!6OmTwoQ(K]*qTn)qucVRbC>MOgN-x1_ͨ"j6+`AVu--kÚ^_Ih9$5M]e- vP"!PMю J PʑPHRlz@䖸Wƒd.ISoLUa22GaT 6UnhM o <]V&44V64e-VAemZePAȲ {5n_`\Dp( >.ZܛWV!uOT`VtAoY|/pZ)W`S{G vt>KʌYXCywǻhvWaȼJFbsjܲ=򕹊* u-9˗;hB+Cpт Ut g#Q j9* ~tsM'aqMh)Tԭ5ZZF7;Lkqտ `t"-Z`1ϩA];Jm:~j#kx"H$ qmez2ag0ܲ$n+'#j$P;#̫[Z:2Ѱ`]{X&b 7׭]ݺkV|>_gU\KGU"p*eV+,>ɗDleJ´<>ZUQK&e"ٙcuV!$C_ B_?mwv_1{cc 7YڴbKҢł[p(L!Q0 B E "<co,; [^8GKhh䮠wz%N>M˳ոc3"Je*P$ǚ:fJT:2qVRzXr0is_~⥧vrޑC w%7v__eݝX{!U.rUT*AYɸnOQ2 TuELz#23V;[2&JKbh-Z&#hqZSË.>0tnݰP}vjZtepZ?@HSeP`/+}ǭ7 k|dfϊn-̻exBś"/\և-Ygs0 d\s2S _zD"a>}zz-+v:15S{N{tXY$hyl lg7 :d)VsT'fhB5 cX>Y?k M u8Ӡ;m[5eI3=*2U,뱁*}%^=w-+;(fh]RKl8ͫ q_uف#ld8Jހ2岢ъG/Zڲ|sȚڋvĺ_>~# C76|ckV_P?T_^ST!U43 @;2@aU!%u 5t/@k%V %Zu$BAā.{uLĹLL9%aoͣ*<7zW=曞Xtp 8<~ @b`enR.vQxT8"+ 55WkX 3\ēW߄+3B<\r#"5E8 uĊecqn9әy. {q+`X( Dc)0O4梱m殝W޸O\|g͛6u!6XX?8m Ríݵ|T՜S򢷁p]us?-Ml\Sy*fHސ' A{@444:07K7pR3Ì9b㚻sC k_tg|w}_mms t aZXb/2LI_01w^70]\l">axrD=˖u TWDAQo3Lhg"-:%K=gM;] S4W&+*#TuEjngn3WWIP g}"JtxӃee̷ z/0r@-`#X=ϭu;- %S`Dmij5I]h6⡰seu (FY6+} nUu. L Φicj)+bV(Ah6q8TxI‰t$RT=DьP VJ⊦${{;ݶ2DЇ zOX21][E}=ƒ+l.ўjRN4t<:KNKl(у4)0," 7.q5`cQ)aRcU".)@\˧ 5 /5aiul2=Y:xu}cG7^x;hCUU(zT; B =`1Yac{-oݲpǖWc#_|SynັdCK&_RiQV!]'_B=|uHJ/?#_cO~7)`Xuꔲuՠc=>f. *]F?cIHbVXv/}`xcp$W`(*˼oCbD bQb#Hx8UU&2iY.t6RF2\΁9DA`VVXL@f܌HaHf٤fq¡5+z71op%{lH$T[W fYSqj/ ĢΒݖ279D!}痎mgOo\Ξ[{vݷs֎y:q8p-y7Lep%jJÇɼ{OO}MRz}C2Gl>xpeQf 3DeM*[{PYN}g+*cPq }㷒[ xgOs2F<(FN/u"OOIm(3X`SE}d 1:p_%Ok#q X684Y4$( ž[v,M%vڳ}s붍+'F/tc$ ŃIEj߮[ҙ {[.>qE]Ml?U:m䴐i*fCTL;7Wԋ><790ЫT bF pςZli8bf|C3A7l@"(PUu,G\D\qtL.۔jVZAT/i^%̾m`!O9QbꁸɅ9V]NFb7bfL ޱg6k'f{LTZԐf@]uo@fgy OO& Xezev8lV[84ohmܱ9;EUڵ[Lv )Vw WgR@B~UCiqZC+g0jq?,|wj-+#D+_b=IЂ9U"(Ipw [y"^YUY1M0*޳!o,RMx#qDܷ3 W2JI^cFJ05&B~* ؎r4#44Vf\ -髨.yC*%a.vӽtv%;Oݶnt^mueP+*3t&Psq;k‡:͚mCc;uYr4HWr+_~~u;VoܲoǦ=;f%IKGob⢞ި 1څ3- g(J3/Iٙz O'W9= MUݵ `@y*PF5ڪ+kpq `OɱTF: G|-@,t΂>b٢@nIh "@v$n++0V6&U e6f;< mFP_PUEk3V d\JN {޲fޢCS߻9u3{u:?e*R UT:sGq{~@'8P,aCe0X;}L{|_|Ɂ 6/jN^?7YK3‚+f , N Yx5Io>ŝe# (gTa?zAk  uU{6Xlj^mnS)&pg@v`F h ȫp-53*sX,,:iz߾~veʽ'g/hlm.R$`5Sg3ѨLk@<ŀf*b? e @ 5ۖw]%yUKlE YjQv@m Nص{˲U[rÍ~d*)OA Z,h:_Wh2zKs?}{6m[T "UFAW׼΍[Wm4!JL] 8X~h-0 ,-Ui6 I`=U& jr?ȫ| v)e9*fo,;\\4 +X0lPTA/oa*|ں%ګHL0*NKUd.ăiUU5MMt.x_ٷul㲊Kmд`qCCs2 < B(+ ~/^ R?Kpj|rxU% [r:|V ݞt#ZJY6 ʓ?}dw"dxlEm͎>Շ}xى{ol}B& tR~& VPڌĢUxjs[EVn'v]x7{^. J3K$J_*%(t!{292VHE¬D/(R4 Ao>4CCb Fd:Pbeu2Vq MG; !]DW51ËlDۣ݃h{~0`";Odqξ `4mЦыW]1m|l%-u-`n˺es}Gz/kzYR#{k=4[7(uɓ˻9/C*U&bYbK2`0\򢪸lVG]x&g_nti[EKq4Ӷ=_/H­-Px ?Zv!^^;1V) Yh&4,2b.pn`/ ƖZc;*|.:}_3[;v0,[f]fajg4x0=f߫-ψk{ƫ:MOm-9Fyzȷ;Za_N̽ʱ7o31:6W0 w`Ftȯ20m:Ǐ\Cn\w⽷m3с /;+m]+^-r~ã_ W<ݽOoÓCh{Gn-7|s|JsvnXOts#3=snS[-|{w߹^3wqc]WyxyP]_ YsԠ6hA? Is1C a+)A(2-gZ̛@_DK.je8c?r7W<`HXLVawMnho7U+]W[A}瓅§ᄆg?td,|#{ol'*n=K瞺d|릆<#/W63ň+rs(:-N)-vO\Λpk' n92{άް:Tn<9k;0|~Cg/y{Vt1hoUSM}/߷cuDm -lѤHլň (ym/V;~kowv9<8֖xD O ƒ»oBY`ڟdsI&W`x6$G( I{őH(όBmFt(R/`jÁN j( 2cfU(6~`Tte*ˮ] g_gl,{(ememoYWz5'mXrk˫Lj3 ' Q/y_nxx={[u{n2V_ vicnl}'Gaܾn<ã+~xnۺ-\7?s/xǾϿ~ЭٹM  66kސ{.={Nz;{hmzy:2Ï\%+U]ܼlmv_s(F2`,<@kZ*s s,Y\H&EOl^:',묨u^=v{/?t  5kd]C:aYf@&7ULlu]1K5sD~}+cx\)\$ezb|I`97޽?V^ S9J$u%= BYJq湡!JG/Ӥsf/Ǡˀ6td>[dzJoM穗Zr{z?h讕}77^_<}ΚnYykvξ|-X6oUn.VU]Le5PDyoܳdVFpYA'!@n㉌hQC]MMe\I/ܸ?{xKjU.hnQ'n4J4i:K6FGe/぀ vB᥷ m{5̩qBwDW+~݊0{^}@seukqz2 hud.U~Y4-wY&aVZT'D6׻̭G TF_T4/,Ѹv׽<2+\Yk.~[xܧ>.|/Z>{ꢇ̻hh@1ǿ/ޱ'|ٙ-=WsxSɎfe=ari+8I$Y|z"VID\aVrB=Ai e@QTgeek7q AܺplsW]^{xݩM=v-3X[iH' C~hb0y Z+‹.<߶fwsNjm;5y ~W Wxw~[(:2?qg۾CBے M JJTIappKrKh΋O㗸 \`4*gO[zc,&{{ @Yόaj>yfy<.뗵*Oֵۚ$s˞xsC7_=Ky[nܳkhP}7F-[.+T6LXh5n!W`/)r$9}ſz7^>-_r׾t7޲__'WY>9?z`txH蜚`_]ow]*E2բV3KeluW s* ;nٟµujd^RQ(|qᇿ~oS|8Vҹ H͌o ;3qDpce.NMh7FwS~9iC\љdEsG_EKs6uv͵GU25ss6wjv%p~ o]bowlZj"5U̓=glݿtlUGP&:e ^T0@buzc TƿteWk\kNn~e,S5'6l_մkHnܪ#WVBFcH,hiq4JȺrCV ??}6t@hm3T\j׀_(?_7j nPPf["\Ccޢ,)"~ ԃSh1DZ:T։-we&m$r۠AK cn,$,RI~KeU}eM[Gǜu֬Ձޜ~?HYSۙjm %@Gg-^/L>cGNx϶t]X`֧! L0#5 "N$M]'bU#{sx Hh!Hi/Cő^1 û!KUmKgow.޺Cg/eƏNDsXսk @6=?-|V{?s 99{<}G/ĢI|Jn_ BlU+Y\4-YԈέss7#_6 OBA"T4S-{wNt5yfx͛n˛G7Z;xX9= LE]y,aJssKjR8T&B&d  _o[gpucCA6>~~ݑʏ ~s.9q7+B{O._g.GEuLZX^mVr"P +`i6Le;,vVmXu܃.9:5sѕh 6*v @\֊ZU u͵Uׯ[mPj힛v..L${żs2'X<)KTk۝^oY'zwyč/\8앗 oZ]:+/I>] *.MV@2V +]3ޑh -.]+x_Y"T0_' Iu:o8Yx/k }/]ٽCvdnڱ䁅C:movhix#>WP6kD3Qd2Md|cXպ ?~×í_ά5V6h$ͨ.؉I4;Nsz!iB(B/aCY:,e²eU aa gWW6jтX[Op%`n #* Kx@b(W͊JÀ2H4F>הIW)2@pD2 ]8USlgNr ɗ-C-?t' ̌hiavp)f#UB(31x"@Sx]+>sv[]^77ral |nʺ&*/nltQ+LrTl7Rk0#Hy"%?5kIGYfꋹ}B_* xJ%" +@v-HxwݼgK l_uk{<4&!$֤H75G>N H/a^&N;u|;wӛy݉.m3wiRA=9جP$nڒMsSUnOњ5j[.ij(Ez\:|D\•\Duu k'^bBI ?!A'pY@k,,`17_J)燈󣦲.R@1Y^bznPC;4% +B^o N2)ک~wYTl9:O_yk mh;UhU s Vx"I@hv. J-  -':B Bd#*J5fb*AxqY4 2^Fٍ?X`d6w{g_/vy׍szSӣG6?o֕#Lht6t (\KԒ"dq贘րJR"(Z\gL8&N?|e6Ƈ_o}tS dE\camUZR%\(PY:JzHTހI"Cp%mfQp(TRDY0 [TZ=z@ r|i';1?BVYz&X\5le[LF6沼!˫bū> ZhoH8&G:VW3vkO{tӄ's: \"%[ܺ['Woj=SU|[Ce.50PXH> ͨ5%{Z ]~$ts1YzВ%rX&)eǍ :@)O+T5k`mGU~slq݀%7v ϖU3at7|fI*Aaң)p"`lnuW#VM |R#j{\/8{rmڠ&#p9&#!n鋷Zfڼx+*[l*A*I;3˙ bȕ馝;po=DZtJ`y6rm+T@b./MzN1_%BpPf,wI?r}~&a\۳ z:z4X3y@}ћ[Q 1%& /-?$C4X~vëwxu5.]:қ\8tdhkR^Vv(Cf5$9wUZ[gw-*[vҳr /<98Y<\7۳]p/M4P i_Ym4S6TXoW6F+"W$zg6Viunj`;*V U0 EPI$:f @3+ X.Œ!5+,6LQTT`wm͚|Fn8x@Y29U0${@.ڠ%l|S}s#R[4׷ipZNڭjː?n:Tim 6:jڙtWC~o,vtU5GoWڕ<ќ3cɀOrqRpJgVL.jobx*,Z6eWZk;4ppiUC'4l^\ikeJc㵑b5% F*c{YAz0}#Kp|'&~ƕO>?ÿW1{ @yCAi0V”K+#>CXY409̸{m\^T2'Jkce9pvx 6 nDpHf@> )#^8X(3D6ˋ Ê\e {ʦGq\$!Ii~=+%T ᢅ7ݶKmxJ%N&:غnx*,Nx TU6?l4_yGֿ;=e_';22|tgm(\o_v{m7|/X~ƪ/_Y3>9]3cûۿ||nWJ0LfP{&r+ xzD䞭U7i_0t%2p}px^~|i#ޣc뛧$BMf=y+j߼o]~Კ!{˞ _hVHIe{!BR AYu!\7a `b^cWy8NT;cYiw(`L:6 Nfe=$㵸 \^J1;b^$XFV$[(; Z29]FS)m5sl:"{YOa } hMʓgv׷ܖZխa Y׎*;)51nc&ri&Y} _eUW_wkx0ϋ Kиs Z9(F~-kysnl}5?r˧|vm9o޽LmlQ*SK!H &O7ӆ٣]n'?{4"A{ b'#W} @e[>mM[*P^OKE+f"@Mh8B\!ezh : (\ES8kP7hrRQPUׇ^QGt͉0K֭bB(\6l$YvQ3SW;{^F|voWd)o~3Kk{eϏ^E_E__=g~{nffא6;/ DLbT]']ڊ˂+jݱ-g/j'w_̑eAYM7,}lͣmx`'~|z7ύ n_d_yW~Ʌ;2@eѤ4e f2`4wUuzy,D%TWՔv7aoF Z8\UBR6QOxҠ^`9\bA%3 .]˜a+GVq,6%5J܏h,R#֑]Xżj̧?wWV 4ӣ7fFxϬy¦wnC;=iW̾7&zq⋗}o[~xy˷/%WY}uy%_?7Ǘ'?j+t훳#_ܵ7%WXb8lC=q=R-sRhT޼EK'XU{բ٭M7.~r_ӳ{_;6vFh1Da4GW2N`f,QJWMd߽;g1T}xK.U?8̂c /Ⱦox_=a^dts4T pہ"2J?FvVҢ`irI lѱ}p8| $mA=aj_\NW"4@.ص,WBGR"/D]sWi\&1ͅ~+Etw.g6ru:rO"E}jr6(SͿʗϞ/Ua BHURe@uuGo88W,n쾥__KoϾ7}_M|̪^˿0}i*O}Fw7t?X?\E8 d"16mN[5'ٕ(>ޟ:}/yӮw<ܱ;7 ~p,߿aVm~|㿾>wĞѕ-Dm :=E/`Q6ZʂaG88!]#e`N= MγSlI&dZ^adqeW@VFB_PWQm~$uc ,85>#*%pS Oa 0 /Bv,9w2^=zYƀND*hąK7u{-މ}pyo޷\}tW/߼>˳>3.lN%?>5zh-y|]M-. QV!c"lv\?]?o*&kJZb/t;vZzgu oО{޸qݗ ]ZF5)j!N`!AFMU{[~g7Gg|ֹo\c ˳,o^tM8#e9h;6XЂTQHB!+ ~xg:P;pJ0ZL,Pg;bu)ohsp$m6 YQ>ZK6 2_' \˱WMN:=>`^xhhBRV.jul{[}oۣ)B mF J!xm5ǩ__3=5=(O޲;Fa O_<2亯G?t?0'ܚM(Rc0ETo}̝Z/]5瑝5,pՓW~|v_ny_=n$JT x:F()B=9FrlxʳgKz9Ω/Y/~vWw •-)9ڪ#c%3edvP!k&l05&2% i; d≙lS֑P'[a" (OP [3.itY\jpjU`A W]fqkWgxrHb E"0[7\Sf~\GS\M r){ Dq8z&o]]e]ΊI(>&)p\h6ZJTAtӓ]umyݩޯo;/{a{ƾ=ǧ}ϭu?}0c@n1sprV=ZY *H e𒥱W p—Tڱp`^'_zGz^;-q{W{ﳻ难8s|J7V3 (r*hQz{57.]U򐲡vWtߗf/L~vߝ|DˋG^u]ck-QNMu߶yqWݯ6޵s߿s+uߒnVyF 8P@TV*SDh[̹oȮɦŵ0]֋<}jsK@1ȟOo孏lcSUǖ׮j ԅ)Nn5 fjM}4*rYVٞ0KI1tie\V|Ӓ`,$PɔQ'COaMxq0 BHƟLDC m0KQP<-#A7rDhnO<==:]̕9+>~ TOΦu@}bAJ<rWUo2hFSIYĊH =>?ECEllzڅZrjV7VTĦ-0L--UR4ytQœ b357K*"֠MDk uO oMcu_o]] o'Fvw~o8KOoja][:ߴqau0䒒B{\Tz5}]e}3C_޴=uǹ'l~֑w驣o9/:6!)PqW/0VRvNh4ˆ'fͩ0^?03"4H*s͋gN['~srWW?;ֵJ.hW:^f4pQ(\*Db/PukwmsG6q? ֮!q,.5Kr""L 2ܠ3ǫ-cKG;ϏTKiVuǖ/\۳p τ1-U.ВY-cܵg~Uן}^+7I&7mM /&%mKV6x!jU˜O oskuG>s|xᑽm=9Q72++qZF`6B^'R*E~M DH…:IR0nΩuigMYTH{QL%Ojx?>%iF'HWkʚXVMFpDJ&5 Wb;LL?9Y_1/iww5#5M)5Fa7V]C s (U^9}a/h]$q ,㲘 P)h`c s-E?l_=uV&HGH8#)H2PFxe$iNG!R>_bl"},,ebI'Nse}\41|%IhZ(p8TW=3m a6BSt*hK '3T)`YUc[V89kν;o~7]Eۦ&l6+*NZ\Xl7־wϲ?_;[[ui詋_j}oCDhDjTXz!/9xIkCeԘ@Zf7Y5=CO3-S .V͊Si!7B1=Fqi\?gv+rd d"ZZ1-F S:1ii]80cixzhwSVLW)9V\/PJ$Ni`tx"^߾k^Y:vЖs޻e v\1窉W_w̋vU0 &Tf 2,~ᷮ}xM'gvTmn/ooonr]yKFGKM8N͔M) /W @ JbkUƭV¨1ؐ8k: WnٗHL6 7<]m[YU6bWI)p`"Qca$V 7WN&s\VQV>®Lf0A&t0jK{ ^W,Z®\(+δݝF*ܡcט-xWRj5R ,R'ȏ U`гc vz٤,֞:šN6O+WX߱{eQ'J? ͔PTbzEUgye_NJ ҩ`QZ,&SKv \y1!58JB%)зK=<ڥ۾UkN~|ٗ]j TQ7qpjT%{cVV頵3ێ|Gjc#m^ܗ.SV3ٵUQA;LchmwG+%\YKAmqidְS =3*әbo2)/Œ`&a3Uy(OkwLh jqʹ ʜ PGe3gB Qi"S6PY`*fҍcv8\PM #صb_ I3w=V "ŞP[lv(a*S"X*k(0Ijb+(:~K rr{$ݒH7 dqd?g|l}β*T!Y°AӈhР')fzx=&`uQRDeBL`#\%V:}Ź.yjњkGݲh׹U NThx@103S/ rd7λP+Jlj Sb4Je״Ċ+|+cziy4APZǶs'!Å26>Gb%SҲ@0dqz yBA_;ҨAy`q@|$EK\fed#*AzI*_v)C8 Ɲi I4,n|I` u =qp\PP`)'z稝qp¢Vz;3&2qo̞c4[6ȯKx?5e pN~^ʟ</ ʯTesxˍr leƨ"b\UJK.3Pz ލ'l]~p嶾 &e3 ߻|˹%l> \iq)iE hF+tfى-;l^?;&Tү2k)?NؿFk(4< |EYM|nHfb -[q, #i`IѨ[c͏0GV9([vZN!U W?ke:BUR]&QGV6ʖ*V_6B)̹bN,]4ղf{s6rݜgN\=h}gl9p2 Gv+%QF*3n}+&Zϫiu-*[s${Inj&W7j^ (|Hbs%r^05ʵsJ+ ghvni. &;)+)eN+P%ȁI56.̖L{ tl R YXNil*^{`"T f%Niq *-@Yzx)-KZO:`рWbqWHM:JIJۋ09@e/Pe `qs6 D NArE,X;v ۡ 5(ZPbRdЁ!f{F*1ʊv(D]5bdEC^2g=Yu#b $7 E}Z/#`bDmb./* GjH|{Z`e?K9pXx`n >D:7I@90@ "Ux 0ֵrxhdXJs=T`lc23a$ n A& @ʓd-0^[ .|N#sQ58+@?5'.<^dUX+q"J9ŤPʦ$¥` \Tf2\^E jv)j= O96ql,ae?jAGÁZ|ftRA!ŔLy @fm9Rhцjd P` z@@=wekv_6w=} տӳ?f?G9'CZqX2D-po46՗t[`,TfMK$XK{ISG V_Y='25r'Uj(!jiv)0=*LŪڌlϢ\Tam8иd' "ᨋV2+mݎ0`Q+1*p=aIa;H0&Uo+WPE/hx8[*WC**uG<T`1Xn#`]QV;1²%1KD\{n,Ɨp)v==t[WmQvprFUrJ D|ZKtt!1@ ;n  A :DkRݾ;/x`PV7:A("pc50:" |"hf Uni5Pj05ۺ;rf3bhb%Adsh<Lf@m]NYh(+Or=! K y+v<_X(X >[{Ìoav:bwFA pJ "\FUAG) P-al6ԥnh*0mpQ\80R&PU@8#i'͉s3m\ %\h6Ea`Hh8BC'!'6ݝa&:'r=je|8`A6"Qc1|I#W˕z-A4rԅ%@_YW9_.J"U,LX٢/\qrg^ŀRXPpB^7QXLP75I>a$x8mu푫v\9ԗ궊Lm2U}6p*5FI˫c;J Ѥ*UVe1+µrB k!ck nc9ɢiw<{n ٙ0㎺@ z9p \_E.|xCFQa"0H)0g@S' gGWטDʽJ 2 o -n8* Q, \[Vn/W9úrc c,ZXs&.\Cd{"@k(!;w3bYSP٩@x;v6+ʋc"] 7w Wm0dY'Zg^7MipK5fKBP؈f$h$mnĹ`qqxᒾ5L\:T5Vo\4gq_ dhnx{."]B&\]W̄>Qlt"J% ([lRo`zW<} RaF d/nIM:BQ[$d,0aA1%I;n/eʚb2 $1&Kzux#LYk SI_Ipp;^D>Jp5_Bn{gm%CYO̱#N)͒/Pt4"7ݠEUl$ 3@3grRXҝ=dGhip&[1VV:Fy`N`D恚zot˦q]ȯ~{'UXK8 RTA(~~܋R!PqX dŖ<(**`qxc6<@VhCBI Er"`sAJ_=2w%XIi4T\⫫3Ց(9G ")R][4yMkIEC=41H"f<%52Y`A3#?&-HS1=?s+>&,fNdCQwܔ(vjBF+x*AhC4T\ם0 Vmwջ~8\ ([iq52w3 E:g5d4vj5~-)FM=w*}G|zh^mYU4Y1z?m.l_УJv;;XBS0 v-תE-63KN A7kA(q*7Fn.ި44 (y'A9̠;|6kiDFJC:Ou1>|+`|1&Y1I;).k *kQ^1\T3|\X(*\r5>Y cj F-7,8PhL+4zW,tEH a=kaւV7Yv`V-e-4rn\ ,du3ԦPm7H~7s&Ύ*2J(.\z. 7 F2\E`-trhL\w8B4‏R фQR_ E< QdU,,, цOkU-%M }-Ҡ/R*1:NZdsfKWY3iv%s]&;;e=/}bz(4ܰWv4;*Q QѡыzZz3;\mU`SƪXYunɭюJ+#ҢAr@p؝8+,CWO|G֗6E*ޠK&"fdá*i_5֎ھ&#l})+I"*4pŶ==/~N|VRfpSNңens]*sVް'`q##o(%\b2Bb_< 4 s)w-Үp g8A8J)(%(o 'k}XMg3[}&ʍN]_\n.`=Kx*kt,nsyŜ'.JvmGhsZd~r%I9=uQ1=্!ӌWL)ղFfi1^Q[4z#bHܺ،UOފDg(); 3J ml1XVU ȔRH1bר̈́@ȁ m\E!V[=u8uJ \Xpu:)ar ov8ZLvt^!Lf?Vߋ5\{sx\ʵbjlt@~ P7V&B=F!%jP `y]>/jӧ+Bހ0^&#([\뉖-gԄ 1-YIX;\TFjZ#9B]հuZ%*ԭAµ5 kX..Jeym\Q9b/Bj?K즨qFjKf8 K$X߹qhT0p4L(R)us \r<%6IQj6+2Kj>zɾ L ݳ3K4mZ Lkn],QWW24kD&eBeƣ!xJ)/m1 8(mnۦ-+&o-MR.nU Ɲ 2T%]¬2Tz|/JSbpYb^^YG8aIW*HJ%ud%fո$h-q4J%M@)T\kH4&ƉB<7-e '| d e?!=\.L#n/gYz*YA#ԒOxw>9'+E5&8uxY[xU5Ln2:^ %4Cx@9]*J#HI ?ު,tjXmVm?c$`?=qȟɥ*1P4CHR6PX .fWu W3;h' D|X^vKR mk 'ݰ^@P|  a!Z`Wg+#i3`RW,1o~ d% D7^N?3Ʋ4Mc{o7ȌYUe|wuw3=ڞ hw@BEh5  BB@BZq2Qƍk|s:Δ~ݛakL֍~k[].nn 1]ѼoݿA4Rυ+qeYB!~~LSA~}rCj&DZQ_Nff0^;%YB`%qo>xt-J4 |'o>Y'l6GU鍦QSVqWL(]?5hR惵AO.~lGo'EiJcrk{0P]yҕʰqPlaPp;]e4ۘiRC * rX}ROzpTz(Q\O6v ZrݜnڽyixPa@whX GnYmb TkvW9>Ln'7zpN#zNuTŊո`\@ahR|% "2N> %PxJ, F:`JY_YaPZLFqV ;iy<*+Ѹ =X,'pZ{z|ox[ßJ3p3`Hwy\|lqoޏd;PiD_LI  .!S)]8Xj ǧ2B9y$<4 LʰY;,,Vz=0@:( oZ88_ ڣ63>iwʝxׇ.Wrf3|9V?oϻӳx09ډ`ſy[ oT =cn BoM|#bY(U/*V[|? Zw~ +rK͠BF>uugAvOv3j*ƚjK-e{dޥ[}zU &{ .D҈5(>OgwaM: @|ѧO}e襰WHlF7x0I9r%3E=\=InR-ݯew?[|W~$yvSFHP-<Í՛j[ь2f3cpPzQc^UXl"qF.a? SiZ{Vjo՘jV=$MzrZM9-{ۧ9j\YɅf߯ϻ&jb(Վ {ӽ[^}}6<\ߙ_wFUgՇ`U¶Phm1 d)54R& Ii_rUC*C HGҿa^?w=討1l&>:>?4π8|6w S(&}gfwjZhw;qrT<,j\@vC9"d;ٿyaǟ>+sצmPx{oG)%iEP^ b@`*$lԬKTUi`?j)suovW{%^־6n2]'z=kJgjN˽.gNew_=ݚm8p9>3Nr$N/wB49%%!+%`.{#ʨ=56=n|_{p^u.i*Zwp4eOĢnEϧoM?z?[, >)՟O޺W"3*,v v&GbV7?x a˿/;Z=;T[Sćo.;l,S*&t.@jrZqp/{~'!{.71Bl)f4#>MpHx) f}/eJ@w\jVIa: dBasī3V9 c'qaPB.O[CFδ$cWrOTOhd p/jWսxTlЈ2Wo񍓋7׋۽K%g֍tPrk.gǯwdŭd{Ƿ3RvgMgMZ s #IMkRT3%EP$M&" PE :Nl>}객+ʣߛgb/Sj.\\c`Hbwo ~_ מ퟽߽p;7Joϟ<>jc;-Lo|;bo7޿F@ tOB7SvG9ɾf u+;E˱3",pK=vUBͼ8~_Zc;=j3zHmukLLQ(OH XfN1SmBF3 ?DyJH܏(S)PTL*^LELG}*MÆdV"1w#!o~QVsrlɆ`_QW˯C *Ԍ7qyփL&b9lIyq8KlKAq#wy _ǓMkUW.Sϟvx(WQܧ"(,t2 f':L?@3@lVZ@Lj)j|ZulC:p nɣ;pvFZ@;Lb-L15 ix\LÝ`]ɬ^qK\$7mvV+[WOq-I|v>XKL϶{(P?ɌJ);D: Gr?7g˄, CU?{9OZ-N֟H9ӏEA&rⱈcOۏ_\>hR~n4zМUpzwHGb)zt_$dʻ_$JO*ΝGv_z5ex 4.\В`Fi3;l|I8c;=y竏u+dJ|5)Z$0\^Eq*>rocyqʭ<[{Fxc8Bɸvի{VWH GRZkT^M0=.zE{yҋBZPOgSFfo+8ь [4&2J#˭Doi1 Kb>~wzXg @J0*p*,xfuf`ef%z 'Dm!eX +O0.B1rXRsB~cs9!ٓ͛]|'N׍T';gu;]jDF@Fȗx#U%AXRsn:R7)y^^5wZ+x[i}`_wv|M SM79qxa @Fպ3*g*Q=gKޤViTڨp[:h5d[7Q{Fou_ݳIV Th€Yw/&^kn /gO˽\ NI]r%ɼi/E|N's)zB)b l4 r),Y'|_ywón+ MJEM1o4֑1`a:%xcɗOڝ\n*-Zn^lFg[)Jne|ë1ZfI> Aۈz\YuT28DVƂTkxIT: [7/܂l>I~zRUz>Vnuޏaptu/نP/nRtPvb@HPO0)_2ᔲo懭i{r؜-jRG5Xq_hv>LvNoIbI,Uȩh5 d= dz_V˃L эh8Q2#261l؍S2iE6,po)*((Ӑ+]>%V q/&ѲYjݢ?=OF9]..H\9^J .T?@%r15U(^C#לoZv1}^Wy?fʲ4$R&m9:jʐ섟0 +ā 6*LQ(~C.q@YWjlXlUr3/^3A- .T“Eƺӯ@9,5*rpܠuUW5GW @#S,nr"2@V{n{~Bm"p 'ie8i ͝[IZju1Z|vs2xd8eѲ;̖r2μfQ6٨?¯T+kzf\/闺^>"Xe~^z0$KpEv}t}4.{E c" ;: j=U5\Gsh "x+54a*f%%(Ɋ%`T!hҼyq`0j.wD؁+~{ʕd }W(gzr&2|&MӫB`=[]47MJ6]cWB^κ#|96ouN 964`blN3 O^Ŧ jcwʹ(rOs f% kJ6ҼB"^೒|\'i}ɝ7֗Ve)%4iEܹ|,Scj**15JI|Ԧg /@:ln6BfY77 [u d}Aݠ3f 8dݘ5~;Rfg|\ >'%d >ɫ*t9 :ײ٠XEz)b`ցhWCОn dݜc`[+)8K1jz\>&A P9ēoVJ2RK`Sl|4X4mnXKN*Ь'h8i#)/W0y^?PJUi>3 Tx푔aJyu3U;sUz4a ~3 a_ \ DZR@2BXAnyn ד\wmg " \Η̲ @"vXWh(&L&q۰հ*x76BF9ê?"L;گc p(˭Row.)LBW';ӭWStAĂ8ͤEJM~4&z%jeGGEE.K*Vgh ٿ'XX3(sqf8T{r-L#N5Mf I_0`'T+ ((aBx3G"@~*0PAj1XC@_:ɖ2̴=?,8W1!Ҭ JJj ki4Jub骒-RNɤNnMj άq9P=UIà ~9,f~I\M:~Hޚvo/n-[[;>pJJaos!º 7 od BW6+N:Ȼ@ DHkء+aFz"D2`PSyJ vVD\,S`"&.>Q- D.4R6ᣎ|t: )va18h%m(Aq H13GRFWcZ2TX5.ի|r/ /Eh;t; 鵷[V2] a}\>Z?]/pt>Z\jdW2l)|; N[ĦTT[ɍpgAe٢]UMX,7 ZHJv_9H9,aľXcRd9Av/ӸX*tREnԌ3[8 l\H ieo?6)b&C^sd.% he&݂y^(ә8;Cr""r2CvH!HQWtcIq`XR2d*l7СJ\x}2NoOw.R?[d;'gx7ٿU%㝯O>_x뻯=7|oJo|ч?x˿yo7<û 痝}Ww^>|؊TTw&4.5cl\/\cؿ"#v-oZN ypҩ̟j=U'i =7.@=aЂ E1 hyVa+ӜW 3mX=Ot$͸DtI\H䭋l.iO@Xs>#jD*RջR7ߜԺDhѪNy>|{sl~篯_[wV,Ͼt˿G?zG׾/>ww';[qo{gyHFq0/WzQ&2n)LUbx0odr>7K:O rd$ x: 4Q" A#zGλl)prOن+i5%x`xV e06a4}W9x4_CN$cE/Ay6ceB+&0.' ̾@hZڝl[\&-{Y^B=Zlš)oKLbf0 7ǩ\;iq4m۪Vvv;FgM#ݤcWǷgӭ۝Yj*;I5TQ,7_2[Ȧ>WtӇ0/W=,]lIw,/g;Dj;`(سBu=ٯ&l9n\HLf 3TةNVyzf<k?,-AUs?7>̂TTJe‚0S¦ݟqUbJ'Hӯ '~Dh2ʥRZ<4KH( ;4ͮ4< `;I/MԪJ9WojB+Uz5a;ƨ_.3;-N{um;M&露a{bypg틻sB":ՒBY\@6y䦬g ٔށiNIsaeJ'rВ$|IGʵ<$9oWx%*W"E^"1LPeQ(BTj4"VðKŖԬ4$af!aw(5Ŷ 5ͮ|lS,)6T>SsFSL&cO:Hj5e*T{B sS4sJ 6ݣ5|^4l.AyQN6a#[?ӸUE145UKc"R OX2u>,DnoF֧fιJ `0R)ZVƽ~l@`2' Q!rEٰ"U{$Yl*j(5]"%xAױjtx0{}jOS˝KE!MuS(C#@9\68KGXP (eUj[Q>]I%\d6$4Cn@R}+pa8AQxր^Qp~%I頒PΆ p[q= D=T-ܘljf5?C3G9K}jtǏ^$v"KXlcRoA+) Esq3jyi{CYv|妬r ]#o׭ofdq Uf}c1~p8l\wQ b_DI䒥J1ғxOדfZ*/eA٢9C7bf&+Aũ{h]9\84E9Sg #շJ t׵ ]wy=v _V$n?L6pگzȐ=ETN)qWtqܯ+e&6 f3JjHԤ|' isz< O=fg'C T8J bY=v(p*+h@~@oX7J Mj ^p2˗ СZ6IfrH,l&x  +w!i<Ƨ4TKpm>Fv~~J%xr-G OUiT2ܴL9،va}vlO^ӥڒY?gb)1 V]K *qKZ*KsXKO@B ?bsx1nǷ&֝yf)pI{o){)| ؛"E4I+ L˃K\{$[+}xBD42>HoPz'zmGuJײT)D`VD1ABXHX[ Bo36,YN">x})" ơ/łLܧR sƙ$R@5+KpVT [OD%/y,Uijl0ɏvQ5{<Ng/Y ՠV(fJ"uO]&Rzrb kgAW`Q2g&f,0xQ0hŇw<:4b%R9<IB*WTZ/SyWљ0 %[^,~S!gN0h-P+3Fhf; mA1i:rcp"l^AI/Љx\=AŗPCTqeެ9?B79Zw{p|0/C^u&UZgszT2Z- : c@0KQ.Ii~q;eᤑ*n//=<|:z{;ώ_{[[7T#%>O?8X=\7DʈG h\5\uMb31ۼTAU)qiXCZ}-Dr{`8 \ܝo{.b4DfyB^h~.ҙSK=(46Wu)փܧZC $c/Njԣy%8 )H8ba!ȲKir9F~$-0JO,a>59ټ6-no7:ݞovQ w7v m663`GsA$b^t)j: ܘ~{x~x}փOv77=jrK GOnNb6%11 XIH.@5Ԭ(E*d[RX‘%^),)]BpSrQ8-g\-Gj l6g娌+KW3XO<7J2Ҧh6̊B1D2 ;x Dta @mLթl9(`)~s 3)܌S"g/}|wQ`ݩNanۯO.u'^L v!pDARA ›hOd#ßC 輺;]Ap5YuoFmHZ:{zgrt~ɓn&~;Qҩ `:(fS|[E~_+A{>F^2J<)^0Wܽy{fl(f mE]Uʤ}NX >RIELExNM'`Cύ4(*ф jXFr{ȋTR79 ewhrLqH.a)& ^ 6eJvdډN5kWݭVh4Ji5r><ڽ*.pHex&˒ϰA.wWIb.yuG-6Ofw??}l@h44BdfN*J\xA+PgENa* NQX-Z:H5nLxMgi mhQS:X]RX Aפ4:"CBؼ^M撱xNЀ+|8LrHϞU@XIn 6 ؜6̯13}(=q:ݸ \l<gq;) toxםv;-ǧw73X7x6S3RuO!CD>+T$Q򊚽oWp{jup ~ps0F"?LwV'aA;T# vBF4 y2Lee'] W׹|qeu#V(M{vŬ#q?~T;K&04hBU qzNf~֛ q6v Ժlx#^Bg֛ /fgۍv P%nbð?jBb3pFS'O:UoR뎫|27'Nj7.;9zmy睓5Z/٪}/̧Ja\_uG#!isI]e֟b%V l \x-J>DyX4ٱCpRK`0qZLJZb I r{ZqgaUZTo6s&ϒl.aPuHVkM:Y^F1KlRNty8Cwr2P:t1hVxq8\ EB r#wn _kVu3^}m-;7aS6f˥"X,#1H#u ݜ]Q1ċ/l٢N!wȹwwgzlzhLl=rVfڏ{սd>§5QB*NNc/^!kp瓺ъ׈fU %G(s NqN p}'k#nEu|(a/0@q:RvIy xWyPPRr/PK28Miθlt֊|f&,4X S%UG/F)hÜ9+-YT;a/OB3l*d9>Db`R&Ajo(dMI`IOUOώ=[o=yZ^tחQrڏwFNAfWDӫe[+ ߦa@ XSzEt}J3T+-P!ʠbƂ]H#/GǛxZhNǑM8+)Lxp5uۇP5xҜegS!> X#W`fBHo&H'O# g&CRŸȱa˧xz=6wΗ4g[j/;/=uP 5\Ԣ j!7( MVMt~9N>۞Vօ y<{ލ4VpJ1{4<)n!=RbUШR2gjR{"6]L%%э)l>mlCX]ЀmZ8fZ0Mixb!1"`0~|$1̤*Ob)7RS/tJ'Z VvSg.up[slw3?۟A;2LImR9d?3ÕRbzr~yn2Ҝ/FM |%*> o<U;2D V"/f~GLr̾^hFЌrԲC)[.+tyNySgR)*.5ҳ9/*uCX[,pٝxwn*.KIY vnn7y 'm}c20鳻dfEQ6t`'0+A9EvV%DfgrUE0EX'ťR{ 9:vb/hZy 1BɝpqV7 SG^J k2]Sn6`ߚ&} !T6]tr_ *qD-G<"9>33(A b.W<$BzW\>Q{2л/X8ihjPQѵ9vr]ɘۥF ވ AV|A`2峋9kQQMJӴ|fa(v)ADdY1>"36A5I4YsLL\3qxv/bD-̊\\'M:bvʉN5d_ @(2y{=8cxmx{|B{jT=t&{UlsjP>ZȉG?w)ײ|4rb3f(,;6(9Qt|y7$x-sj>i˷7vkzY%rѽmH--z4K A=bf =Vc+:?o* y~ U/%kJ(yPU;ɧ$!se W g1&P, P5*|"s[yY;7> [p-P(bJa6[tQ+DR)DVsh*\B9DyQw+6 eD єL>OeP'Œ8ERT>4Hsg| ؘᄒ^♄Ik"u|dԱPX1w@`qF#M!y ,>·d1LiPqM M|V_a>>[i{)VFl e ixp~N;O5Sl+͙'a8-ɇ2517qjnIKpzʲtKIkQKB'X⒋ٙc ҒpRPN:^Ŋ [Ccjfu,b" ռig@÷]‘ CFMw\?`_HM p\4L ,A`IjzPbp5V{uIK߻{"/ .A X7Ȧ1UJb/ PvX`ǸbQ+DDݫaكԮ$̸h n0(d#<,E^f |DMD1kWj[uFC3Ƽiε%ᷪL$"v .x0x`.9LG#M\+9=uWo+ [32qHFrBw&fPkS$ISdB+䭯j7np>jԴg=|3b3A5 q#Nbǚ#ąY P_&[R"t(p%+va -82y$TO'A+ѭŦbRGٰ&+S9@u#%cIyy: L$Gqֹ́ ')jl'f:T$qKqHY= C3ILB]ɡ_g#̂R%A``霚2ST6Z `& `J٭x=\do3!8BpƅE"Sg,@A%R/՚wy寰t֍LW}9j(uܒ p*q%xM"?f%+Iǚm"wE ^]">b;6rMFbaW'Nt'uJO=^JU_J5) #+Rww]2阏(a R^ d&}T4EM=Ȟga,G[{r,5o'|OF2F8r,S' aJ]ժ{\.!|Μ%IE]<aWR:C d 6eBRnI9U ?t ye{A}H)ЧXd?ݜm_5f\E1xA#Q}Dzj\,p&,ODL\$$AaJr<W(Sv9n3~E*ؽ綾-z% ^S_b/D*W eNDUCL%Us-DC?zo> "hzIG*+V'vbʉע ya5E7H ?S^n6b>&r* a2?燣/Fgjt)k:9UFw3 C&TBCq ܝ<|<[&~fo5=NM@QZ?{IfV*TY,EBx4IQ\!̇A[Ba@(}$^UNw[j o| R.2!NF6QۅX!9 S}MVdt u[ ݡH)2q*,J0~XW$n_ yȸl}~^j"!@<)J?[.P+VFf' U$ojZa=اbhTαF|Yq5r ZZd pTyUnf'D%SV2MMc m\lzˏb<ܳmB_zI$ `CT|D4%TQI>i^ppHYc䈤IJzO@[X۸] x#29ꛥ J#eHڊ,E^+U2Yo52M%hJ5k&j=; ^xH<7wcKHZ|Rt)SLl< ivq@JD0 a@x:dz ~ÙLe# @jW@&}5RUxTBBlEDJ: G7$sjq@^9L$ހ4If3wT)!ƹ)e _@Q,7X$ j͗px?grt:ivtr@fLżL 5 hJ!izgRn5y2h9N<. }k6hM-0حzx+~[|'_ޜ`&h;`" pcWl (p VFf)bjۭP2d~_̯xqT$ZgŠb N!D}Ȥ ']Vq|>• 8vS3vфd&&rP9ߌBP̔+P/EѬWmꎛ~9{˛o7ƴ\dZm^ʵi2;t-(7j;wg['dMw}}j F]+t1)~W5\VE񥾜~/V"<$36 %e1,mY a[yP94sJԶ{_.U~-Yz*ϋbLyܻ'ɸ@AQi]r|a' Ѡt7;Fީo~o]/ۿ\g7n΃w/}Ǎn󵽛~voDu/%R#hiY /B#A~nTD{EܲRRsTM$@Jw$.N^ a~WJ}9.dX<'2o9qofސeq֦˫sTw3Huъݪ#UTǕ;&tאW^Z #;d.*D? 2챪 eI|[i< Kت56K0kJW,8Ğ('| p'Wb!ce43a@x@~.[ۜFܤ cL?㊮ "T0p2 {6 syrz>G+x69p=vguxlvlo$F z$bT*U3s0ER"Q) %kfL$7[JkY՜DU2iF_2SU̓ l:Bt*''\M8_vXF*9#WRZ+R^$ S¹!zlӃu2T \ P.429#VBŕPOŏRHxV ,RRqDfbYL p z!c;TIsa\pUꓲEHP8l&hV"摝 4|jN<\xA2v3bP<ơxk#P1bbETLB+xc)BﶚT*6wgg4>J ʞ&]|b<o4B)8b9/EFhpTݾǩR4Mouvl\ 2[PpKqMJXkzB8P $fY.a+ (1k5V$'W^w%ʇ>8("KxTOr |Q&|朾-w` ,e\]sy_VsE{Ԣj V^4zGê| 5:{-\?|J*Lf\8ъg qQmN %Kxy~ 5~ן!۟G׌w&UJ/y_VF𮿪Ro5 cA"XyE8@gy_\& joٜn ) QD&,-sM %Vifħ!5[q_ƓƦݽ[FJQ~˾iG2d+4ftպT8ybq%nc4k&QGV%^)dy9\<'p%_Yik&̅U+.ơw"~6Yg$X^β1Zh| @4e=xMM~%<*Hy(1D|Q*b31\Mc?7ъ~8vm0+_7x*"CۀLnW8y5KP̯V&ӉW5Oʕƚ*de\JclUPtg ׊,6dps>#)n3|^ ?OBXgz9lЧ>]q{T谿BA3v?#bj,$Tw(>`4~t\dR`<{{pt8HQU״@ݓ¸bARP2" )R#ex+G0iaױWsU ,D= [!d`<#'n)Ы K.PM!Nlq1-#{"8;n݇>iB6 eYxe\u)>P\5GA,#Vc \tC+<"x{HZO eB4> %zl4`v367^bp/U>ZdjPKj\7> 9Z W*.^f*(j5,BͰ%vc~?Y+5 ^erӍ'Fr,do}~s!d휍ihIh8SvdЂ) śOeS{ܔ(Xhll ] 45 @GfKI!{B5;`ÛttEb>4~"]WűƥBR EBK3 <{N-f/zSm(@ovCR!OK4[2b5G[gͽ^gVGtV+ݝˆ_dR1jbB$%Jk%㭣A=jQ,| x+&vD4񹸐ϩklƌj8A#_XlcފwhK,&鷲ኙ,ZTA&g'V$nf'&bbJe4޽x󳗇Ƿ{'ɋW^ѳ`uS7{5\uB`-&bq6.& &`a 8C;y^<,@a)`CCPK/22`4NkCA3yΠ̖F|g{~ w}$yӢJu z2. ?q 4-/ F}=3 z/SHh Hf)n:PئPIN&n-x^-z a=l' -@P.dF}Ň٧gi.ڑvukE.(xD9hfړZ֙57H-Ӑuh; }6%CBɾiRWۇ/SM G dJ!kdwCGUYt\bBC2g0sqۑG X0nK5ofqՙ̆O^cd~:vGQ,Bc͸ەŅn5 7u-ٮir%o FOzy@.Cj/_+:GCm/Z6 !FAxaoH+G0(мmXҤ-Mɮ|\ #ɶBRT+K.H3ô9 WÀA?nie;O.Fͫtb:_.Wb4,ɌZ̽{V+Zy6'۽hjsv><7;AXʮ~|;StY /%R,Q;HqF|]cˆcYm1*kx+LĤ#q%188MNiExE˃#-I?p" CVuWǧku|ع0\m=G6W.Wهy VK 'ݰlX+N׷n>^}w{[Y4nhإM7)1 endstream endobj 18 0 obj <> stream xwt׹.ǽ$Va':SP;@`iEuRXY$KV\NM;űOM=)䬻ֳ?g3Z92$f Y3 E+xJKM\qPZEE̬YٳEBqZZ"8+3<:53+֊hW6Ӧ*z^<*WyZ]XxW* ƨΐ4pJL F$QgjIj41UER2sF]`PUFMcQ]Qb4-:UAnv]ۤa&]ItX Vo#]` Cj.[æ!S\^=WDs۟yKI?FEcGQр?]wNҤa5JcZyS`ja*UPWkt9˳IJtlJ RřP|Fǥȑ z]Tq30!=#Tkʴѩ\AŽk{ڧVjUYQWsTn^^i).3ZJmֲ B-+ɣH(RͼL5cz&Pƣjʙ*f\V6e3RزR\Qx|I  2ˤ20c`~ήߜ uVu WrdOɏTT"^z:CpW12H$XeeFs,ct5&C#:]%UPPSrN!GC%1T `Ts8 l u8aGh{-Ժ`Y)ԡF i:5:,rU`#I7g  F;l\3ix%xK[҈w.J;Vza40/sƝ6m:#sĈZbu4 vkf14x !P5i#RtŴN룪 C -P7>bƣꦓ󨴟D#]RuUU4]m kl Àu1dB0A,s-TUaueDB 2V!q:բCof#S˘LlkF8 [#kRűn,aDק,°Ug[y'2+,&0[agldDPِN1E=i tb]na2.fO6{MEU\f7lW& om[,u]*~EDwpN˘ۺ% cbm2O,s!e1Ot ZVWv%7E͢Qu "LB=#8\8hxRu{Ցӡ{\lu!ښoաL%|"; C9ND%E\IVKYU*CZsqtx#Re> iUJ:Ur-oǛSF `=ˡ5-:lf-5˒9=VgՆn@ !g1cL bbc6Ӹ.ěY)c[8V2gۂ`am^6q#[:r$,}Z79$\׵>"݌҅﨨n;˸(8f' wݺPΗl#<!a{ƿ\PU{y`8uPruS h˙A1U\d'u.ٛ=z LsUH»D.+:2͞ u/V7Yu._5ijY멕l(0<UT9O)?,oh^CtN06xT]==KV4å!e"~Pui1kL6a U1XX~uƲ?!96Gʱ?, {#%1sm91ODG#Q w0b9vBXf#-5+!ێ#EgB/^ PszWz}+ڒ=Xۓ;vF;"mXwCmiKPp"\~_7.uy8T-U53=yP}qi6wB>Gְ9 #txĞ.e2ԵEӭdӛ! 5h0u:S/Gd:[EєԕJfO?cᯭ2][ei߱oGv]aaް}~0f=3'ɔMukkkSYu9ӵƓ9D?^- d9P9T%\F/lo=G{gȄw>sIx]7U5 x!P`}0.@[{>&|~plU6:iv;WrVQ9CnZ+fc ]뺹Vtk:;rGxo@l9C-{;\:rk8q&** xW|ҁq'xG-tPr* 3q_ܾ@u0r;t\_qJnHԺlZﺭq{:[/"֋sOnR?RSPHo[ǟپUg#3}If*xe¿'"/Q_M.l~4Y[OuyNJ+r |RʲM(?.ϐm5;.os4źX7XemR3uzܖE,_ A雼G}hx&b(i,wW6_G fe2B;,Gչ+k[6=2>w!su.~uuP6|uU9]K&H`K8)MAƀ^ ݸ; LW˓hd1*GD=C"i(wp[ugz= }·Oi_hjF\v^G;O5>ak]M`뵭'kGÙࡌD϶5 ž/l 7k) V+Xh[ukmhНQh=<][Ȗ~dj7}n]/ ԡ1չ/JUGu@xR 03Iue3ͦYQ,uWݛꊠ2$C3e.˳dr"7}5\Tmu(tc9q}б)`u;+ UThU൹ ּZwḴ=2<;}u.Fo0/[\x׏˟@/_Xc[Ғ'FZ쭾5~[S䖆ua;Q9{ѝ=mz6^/}ŗ'"niӺn͓ϊ&k쳜{F/#7(ujn.nv)xTlk@&Օ|Vv2a5u VkY!ǒ==G,%:\뛼7읓Ebsyȿ"\A?&: ҆SNaC{ݣCm9 ȅˇȯN÷ xxW۳z`}ykw|ow5_ƨZ/g&OZ5ʢnycEO =ؕ%vGcЭ!^|x.v:~u6q ؛]JoG̏rGΥ|de׆b :txdiȷX겡ܡ=l2P,"rcd/-wuXJ@.%~2tuUWN]ti+ їL͋ͤ\]XD]4m)s&84l!mn군nJn. o#fbrJ VB+#AfG$:,]wr}UV?MƮǮX.zk]쎆}͑5?\Ƚo񖎟|':g[On?zUٙ.?{}uq͑KZ2_H9z{cֆMjb'kɫ2++Qґ=JUZm8,lCBuxg+aL>טǎraQsDҐ>Gt"Z>]]g4u9uxkDV IDMW7gKʒ)u3P\}Z72%_<) >K'hh+AhfkEv,^۠M96 RV5QQr(th!疠sW|X>\ &Ed.~Smحa)Pk3ᗆy1}}/npHϽOoO}Z?C_g|;[7 ?3TD{Zw6ݩ|X.u*q}*?{\/O6ұUXJ(w)*D̺s6r"] ݀N>BbMʙqɉ;vR]"uYYlPuRks止~+"gY*OSG}Vne4ce4l d_<+d&KeExNO`MYbU5hQuiӧ!Iz12o7wV}N1$vCI$eNrsu><_rb.C!i[XqH.2) ̈́oOkíڽOйߜ}e +nĭiܣ䉗o?'~)9ʿO3kVpi||ޚ{3w4'0Am a.rMMX5)PwUЕIr.ew"7 ff#QƈIɖvE1Ǖ1ǁH{,=Y&6绱:zwZ#wwm<{3Lw.Ğ߽i[e-Y~]>sOم o"?7}ky[wy؝3rwGK-M5NէOg碇U}i?)cnBnJΪthc·>^t܍c.lzwcu,2sB\s,nm6O35OG;gY5 DhjlARGwяRhnk OHΪ$f ʲ^(KQr֘UvV (܏T]mE}7s1,Pnu+}u&mnK\[cmQ$ۊ\W&]W\:*ωnlnIܚ-sO[^<ݞ}f}Λ޸A7'8!7/K{??8'/wyieo[~ͫ{n d׺uTݖF1~C]TmD>z,9R:X:&&Ew'eyGk¿eo?|_}G{o·궖ukj'\p\,۟Kz({Ank u[>- 9WxE@s&@&Y!] ItgR#gQ8Su Iuy"z,rHϫNc5S^:|2+b+/(/ P@.9BG{/J:T %7]ݐhazKs;K]v\Bݦ4QgAݎ'F$0׹e'rnwM]]v<ԗ|7Bg+M[>ZYEKe+/ޗjޑ?< ug{_͏,~g?Z7^];rwudok)uCc a< ˇgw0+ Mֿ-Px(^QZu˔NkϱH,ver1lTF;#QvߑM)S갬Y,P8c6xҢWB0P.RF:t/갘[udSV( 6CCS'L..hL:<ު1{#G/?_9+?'_@ L\8'%n̏[#P[r_כCLg<;{w!/MY 9 PL"|J{O~$5^Pp̋'{>SWg-ԝn<]ゥs*9^û?0늘3b"dxk2ŽZ.A-^RX(INCUٔ˲]UR^z:㉣ȧE#P0FQu_Ψ&]ѿ?_޻~kBWYP4Q,%^2 :L7Lfآe'm ۆ-`63:,UGti%MUGװb-P&pd2]pc*P#kxomྷq_k!Z_W+޻}P5g },O<-O<%,c7><~~^|sͺlC[soMpY#}"wo=6ov>z3QuW[m [79q9g~(-?^MVEng:&K]!T=NC8$)uoEv &dyS gRgjXMc$1ƧR~xI+qUK]anj7 rLYK2sO#O % D-V=kdݦhۑJSn,zU&Ǫ7yN׻onp聺'kc4=6$>3{qNy5ߜ{Ckw6ϮϽ,,{܅`G=N\xhw/\?̋:{7xWĽm[w7Kw6(wXV\ѬHw88X>PݗK&v[1h:s [E틛,ShRux#]Ҥk+C B!. :qOS]zu5uSJ,)k+V 5FPڧ UG]n!ɂܘMv|o9Lu^x?O.xuA[#F 7LG=ڼ8mtlc#CWW${2 ÿzs4)Bmz:6YrAV_']p)@ Ɋ{LHzI%̊&ldmBC$eeu~ ]GUDZOeMޤ5y>m6Y'[!kܨ_0/vY1}GZ#^D-dtw4ā|#оUD{ƍj!=yj b˃WG-4oP+_)롦<'ܙ?/|(W~x ?<|g/s?'ߕ/1Vo^ ~5|K]mvLJ1~'k|5C5jj2{sU$hd]G+YGٽOS,uMw$B喊6' "npPm[#iI|LOՙl3!U!Ӥ :Xp~s|g>ڼ;;%APޛ1 ƽwǽĽ$.؎߽zA&d&;'sps[ ɏ#gamV]?LO>xN sSog'8yo#7{/gs= =E Pr`PfL@z =O@ufVlp}ku j~@] ^x.*׵dQ@pq=D tj^yIײo)U|lCDwr ;̀r1`bQܽSwf͍NLOϾD*2{tjr̷+QJ@5qIu E{z':PdP8 {i!4QuuP:0{9q:[a a`:S^] |TWâ7Hc|!_x.T7|X:rC&d G5 ?ncښVu`]Wb}/pޝD$OPG lJ8< 98+ ,{PM}%_GN##3wgNOХطܻӗ&=C^μ gwC&EU^ץ.煮ngc|xoyl8n'S]EP{n=$'@ #WB*u:^h6cdK$SG5C Duաe?N@աk?W?J2ё3׫ۙ T >bP@|Z>xno{UG?s>gSPH.PN 8 $ "'ޒRǎDM4F퉚)Gn #S'M{3=1y>8>{fz7*ryw {47wptݝ@;=BnEf#sϑ5n r# yV|W(d!d]1.)"]@t_鱂In'#Qu ܽWQ ui:uJ'O+uh֫KXPeaJ&,еO$-uH:ա5oU.|- |AoWɲ:)c"78XtѨm@fc=r~p!,Ы"د*4.k&:@][G6%mgu'9n_®)H|tVg24$r{2| (n!==\|v/V0D$aX]Q.)@ u=]dȀ` ,.@w A)!fjvȝb{s|˴6I2F%Y227ZQL"7:e_3ouџT{c0:uN~NP]OmX@=>:|u$@]`?%`1&ps\愐m 25EM-M']ğ'" Q$"Sػgcsn"fO!YP"g;mb=h |7LGf>)'=:8^Ka uǮC~O",$kw?62|aџl [$[J , AW0,@πdX>]Plr?gn۲ `ߚPPu-@ȁ@]/5h *̉ %1lFfDlK%J = "$Ұ2' ])sz+]:N'3^W?;7}9F !w;&u=# $\NEcc L)vƻ؀1}1с+( uAZB +O܋YbzId)q0ۛbLA7r6WӆOArdK`鼅OZ\ \˿;/RSu::}C-ͫ._БY-VǶ1:K:t7uXxs. r 4Ak!ϫ Dͫ &؄Skioo}rdv[ ;܂9d= SكwWPJyה"n6zWsU{Fy\H: ? :ߑ Ƕ~bzWDZ#C{>Nl ~t)}5W(,'ԁ ϫC&U94ÂbͰSdA8Kuɖ@)o;!IRfae7߾$Q.:9ེؐuCGu'07$?pYqEp-y=7EEț.98{_M3o N\@Ʒ!O:&.f<,x3fX@> #%4$+8pznyoIݜ> ^M i@ '©Ȫ0`7 y^\OlO['g4.NǤfшkkSg($Y1uI( @Yq..Vy, աo @͖P]a(`Mx6FRgSgή3.ع*c^޷B u0 ~׺))t$Iʡ&ǟE_T\Юd0s+L"ȕS/PM 0Bu ٧PًO3F"[SerRfsR/Iω\ >/7x/'xgjʹ aqqa}1+c»azu0j>@Ċ>.]N^k*3Ꮅ6PUme 4!Yyu;(f_R͗Z- . v .&^4<ȩ&ģ.̫1ܧ ݌i!\]`\ʹ- Vq7r/~<#J _Jg|mnE3٩FΥ]xuS71LL#0ȡQof Oo=駍NO6氿_UKh@i!+(woI!I58nETx;5tA]PWs/r.w(/yۂ=bv1W9ALD"uF)ֆVaaC 7,onQK`:̾6[s+,'Z- Quh:s:+:_;.SVՄkamNO7gu@?PPrH ~s>+99o %ʄrdW:rLrdf?<Ƨ1`o.X7?yyΠ h=w^Lc>Av}QF K8+:%&H0ĠnfDnN&K$ $Vk Cux/"إ4c4zuGSTH@)T+zu1Aup N_$-  %&>RM|!(9:'hӫKSWcWo;Ԅօ7+ژ2 9ĝˆ¸iL~1Mz9WxPx%Ql,@ %/A& gg A-jnwޜ\rtPlEZ2Fk%o$er./S.).JΈ#}\NvVffPJ>6{qQah5|p`yu^yUD5:Pɢth@] ( u?7Z u/yuf@ uh)WǃCW:u:u0q]LnY^>օ6zuUau x&G3Sk azTvN.a~QLsN:]y-W~Ssd:r'yTA\źȨb)H1 ={.RVV>ڬ~Uz' /+dqg$QDŽaVTMt2T@\@ºBZAMF?SW N?T,Pdz3Mg؟6s:rdsC@",bu,\TSTwCw{m u'7/ ϩ߻k9up ȰKA㺈U:So $d C:K:rbH`z2<,"2+- TW;VMDG; z(~`P ha~B~ {B|P-J#M_wVe޳|p)r 5v>y 9}zBz2%j׾֌T],MZpIƾ(g3}CNĈZD'uޘNJ`k_x;`E~v؊0Ҁwm»=@[ȭ500\fDlM`Ւ}2r K)^Q]wĮ|jrRڅ̢`[ǐ5ld%kM%eCʌ{;;[Z5BtA.>-{ܝBn P,u1!=N;?M@]N]/tBe'*a5N1ѫKWȁ@wYͫ(Fuafzu_̟GՁX"G[L9]3su U@k$5]I]pAsMthkAu[]adFq(Sy.Kv+Gc~5kjk~ޯBe 2ߝAm,z7E&"wGf#,dUdchxMS2B<<35TԪ3* Tz@"#೷@#U]|(THV76=UA.hCxt̄ruV.o:t\XՉ)r`e2aZȰakpuw^[I]d(X?Xu#);9{x̣BQpz񬌯sg?+*{]qճϺS$I~Q8 Pus„\y<ёI3xcMՊi-Ⱦ}936lz uQ\_"-p4:Pu1h]>F{}{M0PXw=men{oSY1ujb:0zPP:d cՁq݂:{ %zFwDuVR<b}<1~35%q'7uD ;*M*+Jv *^6y3ݪuw叇ƾތ̼C7ϯMLq[7;w]13͑sM1MWUg)ʻ1lV)mqMe!E~jPNN@#K$2d OWݭfkck]Se=P2l7&?a@)nXwt9ѫCk?XW4) h[ֺ a-dXleCukm-x6sɩ*eM wqHq[S$:xFڜo2TXSEhkߊ!kUOGʐG% .77y^<;DkL&XDXb7E r/.9Yv\[zXSxPOW͗S07(PڽW #Ws5;5:W`|EWew[|gݯա:,:td~\թ צ0fs+ޱ"ҹ? dص Iam'-Pisז\]C~퓊WȊde!қr+P7; !<;S=ӑDGLTG?+~^|RH|HyH{ bArjfNܯ*> 'wq8af䅂B F DVSSC}&Ю" @?řjgKRh_`kކ_Yf-uBcȁqt&Rt3'Vΰ4LNIvuOL8Zp kG)H';.:Yy"RV鍬{y=h}R0Rt!3>|̾[P7;@ -rk˝NIȊn.zS Q PSyRqSMsfL<,/'(sì(kHk𽠚";#@n94u0b <;Okqu*uD;k:x!R3un_Brh:?#xp_.VҹUµ k[T[ckg::r˰pz ^p@cP7d":#\#{^cop0Nf "%)ح,ܣ);Ys"RNܲ;U-oxR_&&P݁'^g&nNw|%ȏnvGW h!5wIU'Ee4 .:]X{hnÑꃊҢ}=\0'u#y=~-z :6M$Oɹ5bkl+mX7=sXlX3Ё  *EN/% ք:ֆ9\nVw9:җ.E"*-&/ٙ^K[?ZA鵒eŷVd5hvxdy=37;;$^v;]5Ozo:>\R%ʫEsNTWvՇ`'-#) qe &)7"E v^T}cM:ԥ K^Lw Wkt;R`1cowϜv&@t@nԩUE\UVUY3bOV솓9 ^/VRp"vMD|Y86 >D&uÄ^ݤ'c[oq'8oW5'ee⇥iW Ngf+9Sq,Hzᴪ}=¬!fK: ${cWđ;)푁-$Fnι6ı:XI0f2{ Ё:Х@^@WaKG DrH%;:/O0dCe6u5 tGNg_*8zۺ;%Zlu:̳muj8tųȻ >XW>-|}(z]/yR!BLz'Or#Ov-W}6]y.+Tfi4U{UUÂͺT`hMJI&Njyօք:WnhDߡNhԩ ] \Otx}) XajP]ɳIzb4|AT4QlKٓYuH[u"\FY׋7Է'*Hu] d$rB7,{d-2-rgUisY寛TO$+?J^ˑOМL8΁Sv}ݪaQv W'LH芥G=&ܽ2ĵ2 +Ęu:݉oWCԑ:rp;5':&YOO[^+ )ݬڢݞ^O[rIMyM5uMm|Rуw1H]j z}>FՍՍnlMWͪ7?(%or8x7gUJQeAe>U^UNUvyVi尸d/w7cH9+h1T\cDPQV4>Q/bߡnSگ-`7ꬌ3E$XSTZ? 뉁&mC@;ZKpLwD'd}Ռ~VѠ|z~XӴ=aF PwZSyQSzM}K']$S&|Ty]&=#:7ggoM!O7a0,: ;nbrcWYos~UX)dnf|1snFrGsl1ݬ \CoMJ >6p 751݀m%z7,PbSg= :5`Jhp*]Un O ]8X\^DķDƴG3+fajPܸFԴNܲY9ޢhޭê-Hƹr3~3?viERR:V|yA!W[0\&`WMz9\'{m{ŋ5U?Ko簮eЮf$UPO+NHc {=b-a*MAYIHw2;-&.<*ؿ,ȳ((>g}x` 7=# &2$k3X+kyu&QAl3tKg܄z ;=|guG,nՁ:?KkNw}BƉ_]WgH9+ ZdpvR:5P\?Lt~i[EW]_cXp3JeIh)ݩ56֊7+U+]ꢓʬJY bVzm%!RL_Uы'ómd5e]upmbbFTKdۉk'{F^?KZ} M?$QRDP 8{}읒!Y:Yۀq@Xϯ^)]"9;I/lJmLl"Rpꐠ B!OTPVb-q\Twq]f[@auiPu}ZO  %gԡ/Q` 0g.>.L!Ȱ."pB3)h%wuVuz-:yۈenx"LyR;dW'Έ]rM>}AT fS7)ϬNr%d-nrfwxS:u&tNvi-yR:rխ\ε+sHR~Q^}'l4 Nz~'=#Io7Dա>N~~NގZO47 )\12g[5isuXrӫ 11 N]_KgG,|l!قZ;  H:ա5xJc$%VԒJ/jeִ:x®^qq:IfY툼zt$cD~T$8.J9.?bPw9Z+Aijkn{7~cSӺ |3p2vw3+f޶?JL}3-yEK;< PH"#vqwp؊A&AFA#7 nY-hԵV0K9 XN5Q\SZR{/I:G ڑ8b k>ƚ:L"ܾn5#|PϫCO3[:k 7:z*XKP1&k+uW:ګX oP8{VGThQzf]#&u׉k6G$;eۄB!> zJyUI{#vS1VL׋ur_mAfFG825;;=3; {#+f6v =-c o[+?Ds#Gt5wYJqĽ<.N.v6wbmV4;xm :v]mji5=:^ZͬB}}<@+vtrsֺ8j='Q0u)Pyuf%|aGWGuC m߽ NPGYxc8&***GLM5PWmq*u+- +'$Eb5ښN_oEt K6 2GD2V.}7 7rTqRF\WG_NOT)7W]@AoAq݂Ĭxӡgc5ڧƱkK}[u-[q5[u&2tD?( p( mIok70 r@ENYi`'elj˩)pbI@`_OWks9ItX֞q`L }(t#rQu /~&߮auVf A`@ja1 ֩NYnX0)(+ +,cUeIyԮ Κ*ަPxE0X-doJGM>č8 '䤯WQ@ݍ<ƽ DyͮZd733o&t=&oJ=롵㝍ʶ'U-Ϳq-W}%[zZ8.>"#rbh~f*~]IV_.-(a70˒eTn%V/./ssqqqu2Pq:Ɓu`bR쬓moL4]t4 CՅ\|^p.PuaQAu+stP;9;k2] < p%!Q8Fq0QUl+e6TFE# f~_*i8m<^qV&a7 C8I"+7Y EE =3Sn#nnF9d$c+[4N>mnYɍܫY4ҸUɇQĄ=]@9OGLhVrK;DMٚ 2њbԖzUiB~q"*?{oH_g[ h:up@P̵~.iA>n^?X_ߚ?Wg_?X > uGebDaļ:h x`?f 8^6Ch^j3@5budyfeeDF)s iUy̖BbRp9ocmhU7;%FoS)i#НlA>逄|LuQEt%]NI|"~̎T:6NLfn2jrcS[T-+1+\Dt􈚾_G@ ϋ).)iU2nd5[}2R"NoQJgAbC~Le!5.}B<}3=t!vS8ڈl֎eob ̼DkK-t69_D,u&Sbkb0<{JM@0Ϲ[j:0lN۰(u XlnH:;c GN td0s؂LZ6#S(n, 6Wm)~Fj_B&:"aKrж,~^^iaEY킊vQKYȻMHB*(|LO 뮣0=G^Opll|nS_X_{j~٥ %'3k=\GnNސ6DYEKjiZAKhuhmp}1o]>?2#-ZKCcL O WtWǟBuzu r6 :>N3럌6n~5\v_ׁ̫.bzuq-zCB^(4..jwOg: cQYu錮4L@`}hSh}p,hTԝ񩫓$CMIA[aXҤ IE9eGej]:8D&'/˯SN?(rizĪ@ku(>yP>앥nS)y I뒂=n-MS(4WVVK Aejet6:'+RAHM ONsIsum耺+hKu$ s^]1hL5WmԡPu3Ϡ}Bx >;ov\p'ԡPu 鞮Wu.*W=@ꅗ'+*5XߘܣJTrg67 K}܎v} i]G]ߘˀVnaNk{\=^_^=xVyD1qg{@̴._/kB^|LA'u5Ż7+J4P[ 5],h=rD[s l&o*}\MJIbĎbW'w;b118UMrQ[.X$(C*ML,WsUA *g |*'a1); Zͫ`[8ݍ:zr@1u a:'h:V>~ZIwl=pcƆ#)G1.ӽGlPaAUpTx=C1pZ(k̍Jv[p+s+X5)y~KT\;G^.>dFJΡq+ŻTңJPcO .dr/gU=*MR62+qyRą? u?>ԅ:L?@ffAu;+>qyGjʿnVzlѪ#Y3[wk*wwʲvU#\fFFzb_iu"ae'Ekb*+MM­JgH%R 4'ye~ g3<1/ekl7. 4^oa buRob:،\nXMoSl>O:@@Ck D@\}AB/0.%gI+Ō!1{\tP%ޓ.ّ-T_Y̪-gU'))1 mrgdX_~Mlؚؐ 4F:e(5q;7uH8"WlW(U'UKYBn\鵚Lea'A1;=l׷6>B׿\F\D޽&'aȾk{ܢxPyNeJYKӼM^]^K]6Ĕg{i1=q$'JnH#TEK M>hX!9 I6#$*i K-s ;*=Aw =ݴkb:@.UHˏPuzr`81uzr:K[h:dyc꼗˝W7h9P31ěIU:ѩKXx~akKuX7 | Љ"XNvǤc2~h[m.5^T'/Jj "FEDŽn7oJMmHEAy Vu $; 9Mn2^=|sEJdcx520'nf{HwXq(zIՅEϔ?@Κ]+v(jwF9ì􍩲Ɍ.Z\w'K%"VqxR#>Z@Uj㋔V)P,%UIG]/!ICd2_ܝ"u ;I߲XeF`Cp#sp&@|˥EZ~I61- 4 kd疊u;yȜ%<';"d 9I,֦TΡT !T:T6c_f`ftC69#/]B$I-6JH;5Ne0W%iCDU"&엪OfdOS˔,R+T_lB޼EƦ`w=~rNkꛦt>m wד+7j<_s3?ZyV!?Qr&@ʑk=%myMu̶8N+5 |#d.WkWchhuxT1EP9 EJH0$?'==BÎ vJ |+ES3͝::::6ֱVpaB7enH1:*484y.\`2/]w쒿aM7aꈿX_V?_M iuw=kuϼ_Bul0pCKق%$Slm@bkhgMrtO`;=NBG!/%5ۑ2;@Od OJG3srȹy$~!"6HAM25:3EHzYk89Ead@G: ͕-88\}zyP@ c(ȰFm~[O!M*PɾYx.2swtäiї5K i2z knڴ:m]XiN-oOV1ZH-$o.UM ])HeHv%1dSt~VF>Y)HylG+ōqJbAalclcllm`37b f ̀:@3k3ߣn Tg俫ͳzu(,H)ggh++V 8n @f|;&Ieid~X65 <ط;k_姞DUE]$S;h7d^)[)൯vh҆hZmDD!qF.UER; DŅDCȵ||}J"ŏp3|yXw>օmbIww`l; beinA57aMMM-,@| -V SvI"4kbh!Ɔ\0[̍Hf&TKX+{@Kq9 "w]\J.>De䟍_C<ְ֮(Lbl.UquzSdv3sVr5+~Z0g(X\S=.ݗ9!-P|fkQsɟ1XMLߛn]wJ"^Ľ&=b\B@ǰypx.yVxF}\qG)ܢޤ^6c*Hv[cږ&qoW,WRR+8JExDeXDE(<<KL&3ۓ'+ɜă 18|Z=Ϸ`\XPqsla&3'wՙA]9:h˖-_XvYPP~/vᝥG<UB4<ԒjfmngZ3h4kW Αuc؀HFK@a'/GHnʮ1'9~BRd"9%8*|heBe'H;:])]n ¢Aq&q@JUלR~y͗W"c)dfyrkE{G ej琹>++9*%v[S[ik=+65gmZzZ2!XU^Ix|yhdeHxUHXupHmPxm:X+s 6.g{b F>~Bi мYNA|d"5usdDv Xb +9zFP:://?~:m_)J;mO`._tM9OD`jp}3PMO{zT.ڣ k]-Z[QmjPT:@M1&&WEeʈTN]K`2ue@a> ԺO[]|>gԕV*S$S:4BqB T)B Q.*SRע6u-ZG!dJ-EUt7ٙ)NE{կ G|3’?,Bˁr(n=RܱڴkiֽǺVN[>EǮږk6xBGׅ'\~:s?}|f {ҭ?ߞO]X@{}i']s_niގ9zg0LW mZ.n.u|پ;w3ɮHj{0/͇͗39{/0ra;;~_z,\KEFK{1۪kl3#n9^.EʈL)qTLΊԕV;u3M*Sp R>˥N3r2Wc߉&0߶sжOx]\#_w}pz.5ҷxP]@8}=u'Q'w?٩+mɋkw3.|_?x)s-rˏv:t毷}8.ٿ{Oε~gk쳑 H=Cn5o%(_NOL^7~}+rh]=Om83S\J LGsVz)/x'>ˎ{ ow:_;ߎggbG➩&תum }T.%=a96$$\kp?*(M5Z}Sgy四4Qԕ˼}s?ҭ`Lu I/SM-QWV,: QQWֺ2u>B*DaB TH ʅ90Nd.SoJ$Nz&=-o恏luuL2 u_j1v-yK`l>YtmYi=t}mw7?zq±sG{n{)={#5hzcJ[˥K.>~wݓ+\^k~սMv/wd7~:kb.r_?/w0żAHw'{nw<nn~\̶ سg,|oK_o`ߛsӯ](,&-r~g]twE'"ؘ+9`kv75]'~RW|+y^Y̙nGR:61V,nFQȃI 9'E9'!1v]ו]:x%:ש)b}: u%إT[n}Sr,PQ. PXz,6 C,&TQI6 J,FZpirkM!c$xėх m]ro3?h_lr4͘C[:b]h勭{#`ak}97ҶK/i;ܑǺj۾=輖n]h(\6{OON<|+ɛrW>{O/g~0nǣtٱ{_?utJa ׊],W/5F7 GHDk_zϫcO3?g#/tΏG%ty9gqNr/~]\MnHR.>n ̛l;l%c۽mGv8;}0ٓӶom]Ė>/2vYkrGFݑ 1 JJ_xZ Oxa}[B)=&!X~P*кvnK˛+ `^ ʷ/t :u+VgQ֒pwTZ_Xlx` sRʊwd ZPc%B,"iRB$X\&5ޘTT5o˿AnuGNFM~{|jlJڝmNv[|Y4̌nY׺c=+]u?غuowO}~%XMNM#{VξptGV~{d]N:?PKw_-F}ô2հWH>xRբ_ͽ5f՞{{rnwFbϵ'}uvϝ٣G_:qah =S3dX0?ݕߒ[8mνs=SO<=ջv[YWݴ=n=xx p?-p٠aç}oy>pvOE{y@_oO<ߡxFF3MwƟs7.O]?=tpj}T}TlhƑxbo?ܺ^-[ t]_>+{{~u]1|z1@Mտ؂!T}wAͷ{=_$@}]I?/aS'WzCl[6i>xĭϝxC{4 LFÇcÉ؞۟LoZ[=a]fX盰&AKk0ϤںlvK Urm,dJwL B",%+DJQLȫCxNX`B PeW*#a0 f`quu'.EA(1P#jM@ek^C0洭2Nuf䗎1YqE4qqԤ4̆quje)])0k3M9lCP]d:Q4uv2ж5ܹ/{(}(r$t2R'9S?4rЮݮ}ρ],:/^- b~.~SFwG?Y&tD~зzϷ ,o5_w>YPi^_nػz~['o9vc`qo$ [GOO7p~jȂ7?Zu-IwxwxFcӨ*Wѯ &m񦣿;[ޯo< l-NQnV.F kV2bď>pb!(sO@RWJu>9uk-ЕSǠ:,c?..qp wгk l,,CnpF͂0C L,Cm6 ΥYz3C¯IȶС߷lu =pj;]d2lkZ4mc3fvisļ/"۞ۚٶܾk177ii8aw;2}[[{o^_ىNnM};r4;>lWC=j} Ra/;ݑ;~o8>Խ!zSV;/w9糦nkbKcO^X1O<~$=s vE{]հLֻˣ-:k#;N,eCn-Pv{N%')S]-=LF3T0M\]vCQ4h9*gD:iI*${ĘWۄe+(5ץR@ W& aN]yt?(_ +صnUeSBWsDqHŬ(FY7*(EnD)(uK&C83f3nLLx,jwg>i=iT^r6LY REC՘:ޭlu`m CAGzG8*OO8=X<б0uvߛN}}w?k;雹39~:i VߗSgr7;?o: 9e ouLߞv2xRv᳍l=3Pxuf,{sak˯Ͻ6d+CgzvN<9x2Zt&M*lF{]sMoz:@θ6j3&Ռ42LӥUJTFRTRQiTD bQۄCY"Cەx=J@L1B~Nd&oN+p+ʇpA|VZѕ_ҀI.#'_n9Q  [y ש[1:ueS [m03szEDA+XT]!G@g %zpЙRa!7I~5YZk6KV~? l66Y?7w{S>;"ivX+4FeluglΘuخ=)fӝCۭקc-4{`O<:ڎff'AhNVu9͹XK'ય ^/ow9b/ϤOzt/6I^jԀq kyVwpz:nџ.DէÊQ呐hH%mW[>Oj7*OZn}kWH%8dTJA("Qa&˰sLĄ 6 q;Բ<.rYߢ$RBAof֋ἒ :icZfx 㺄|5#\1\Fr_RxJKpVR9FRQWoO} U|Mq" 9t4D``W{0S(#F7TѬE4LVq UfuHh"ѢZEUi-aO>jZYZޥ)hƼr\:ɀ/\-Hg<OI'U [̕=BҜ۸dV,NNދ;ژ/ on)oX~4jxb\QŷvK>ľ6gXzQQ݆:-8붼mzVvd=7Z rsÙH\z$,:]|sa[p"$9rK9UAסhx79zPSi]J1.%)kaKJl4¼ۢQ6 /G 2"Cy @ٍ aA%^i1U9VprZ7צ j991@ΊSKU:rG#urk2j3xjbQT*ܬl^mf(#[Y 1EE`7㐚I A4(!aarNQ6#̸ zE .0( CތMԧjaS #T`['ouq}4b|o;|0#; ެA19tt.[H﷣_M>n~`n_9gswfuhd9rQ~Etn׭V۳mki~J͜bܸl>laSݼҭ,o¬dzq @ `J&A%k7i4˄9)FSQO]b(!GZE"s yQx]6MVEU>By!ߎ (dRpӮz$⛕%@{8WFi tNS<Pgy+ރWUw l@|Ȫ܈(C.}SPRN՚^ms~5 YDLĥb/s}y篿<0H&59cPBSq-w*|)VA6UŮ4hlmdl ŧ ~&;+'?[^ Ogc)V>9&(Y/nwy}0yWLhՂr+ؿdIqh%>6~6fjK~'CZ>0ף}Cz>&ər2JgХ,vIxN}=*y"yp-#=aCC).߰âƌ.9>AH~(F6h4fٙ|ic %;|jONx\:'6&GR",CִsgV{-i,h;;٧l ~) RBX sox׿V>R*Fmϟ?c̠q8HƢi+1gfV獲OK[®ʲ}J<#$ZRzT;&@UX \[w%7̇Ҿ9wBUһ:Nbʂ3u>RЭ`AYN(amp8\ ]ٮtoFD%gW ziՍqO6ogo572ȗ͟wh|Я~ Al=[gRl.;}iQ~:_Fv[EՅlBr!hz\FK]-' O40.Dǜiz 0ͲGbl>2ZZ1֨cgQt$jQQ1tuQufeV%Oa VbD:Ց9x9TE`ZZXY.فA{լMgY\/6 W.N@oqB݃}?N"U\KG27Y oU- L0+p!RMYɫ>NJ1gsP$uxP[N!ƔaH.؋jCfтw&#CL_ivEr֘6`VpѤ1; rRx^ {B瑂NM;'Vq/T³ ƻW7EO[7i?jU_,%{xMr3D<`܊0v>t~1p@r#zfnn.g#OFow_jS|kL !7q¯4_ndjqwĄ"q6Dy`RuY|H2>)A5 `v8K+lSq>yЪlaF avj)3"^a;W/`f'x`zxT/ĖN ZOc(íMp DH S T1J(XK^nrn9 BjΕz+cR2r7b=e,.e1\em̦iJjrB*m" &ܠpS(%&1VIm( $\~i EƸEۂ;DV K&h[Ĝ=-h H=舚?CƔi56.E۝9|S:_/gMlg{ݾsɓͭQONF1@{=O/6 8;Y+Eݺ'ꑇg+A vv|?6[]Weovot*iAǽa[H6bwómWFrg-7;]W[̧땗ra٥TXWl !=ܯz!5'p@ݰV)w ڕ8ciݮ(V9ҫX$EpX'>/YY'E<,hE5Biii! \ߪ Txi >Mνr-'e ޤ^4iMZbҪXI'M }+r`mjjl';OdқgYuq/D)r lb3b+t#IYO}LlNx 9cw1OO][Y Flu%طsțлr >f͇]V:1v1Y!7EU%™3눃^Ud͘9Wh&)auI& Q[>ѭÁˋmj"G@om@۶IIJ 8 A0*ASKbb~cLxgv"]ԐvVu6)!tJVF!f?[lfy'\b~ uX6CycS=z(h .[y=i0̀:i. 3|F4iü// !qp9~ QG^P jvXHi5YzrjFzX'Tc 28}PWz H0!zXl/F^fӞpTfv0\P_*N5KVsbNq!?^ih_m"=À;y H;`G~+q=-}B&"|;Žb~Z^];':&t5_0V=)3OARF ]~ラ1~z_*nZـ J: `~9;UT<5~~ԈQ|Y2[oNx<?ۤC%R; -kڲ&m}%-|QtBxu#D\ H@"=!=Pϙ;Y牔ei!W']'aYIw5c!MosRnAmPeV (bەC&iQ bD\[QjCMJ,#70^W~ZJ1x?tٵc᧔RSV bA'<;^n_oIEzFa4 摋z6ΠjHY@Z˭L]9ueX*@Q G~W=-S2*YA2:&[QSnSPF:cG~`U[Ŕ JfAK̪q8+q0NP2 bh$r4Ҝ$-VF bAQV@#= xHjYh%j>sTGbMky'6q }H'Ls6;Wcvꪥ긇q<_qNvBBd`0 6#w>zXAxFp;}4ʺ] ȮBiE?t)qNd,¯nzczo|N7ؕk =:z?I&5K&tV3rnA epF,ؕ}1HŽ QOQ E'skr2 ) gt4WIjCmNL%s?xY:VYQC:<ٷ:ѻu>(藋\"̊aڀ fih ,qkBYLɀRW}G{{-U1j&$(-F̈́0(=mW`WT85ZEg |+RMTV BMP2|V`AAdHCQ ks2~N.4do2k!e1-,oď;cI΍=Ь$_MJċfU0gCA-@rwi=:ոC:nԯ+~92a) ~-!^`z{#o$ ` CT@]^)ʅ `QvJ.86X‰.!߁rR4&`$ F.sQ+A,P@AQq9":e_ WNԭCkɬZVm5S[]zuTk@JyV;T6covpk8yG"ئ`(,<0qAX Cd'0FmIMBau%^C)=lQ0LՎЩ_\3hK;4Xc&%pRעEiݫ&iZgYf@oq\9Fkhx]Ĉ@kV2w#~0q/ [ vJ쌄G)36Y1d&YIuA^z[xY9\/"b(H@$#& |G.qq-@˥J(͢a$1 ,(&WHh5fLUI#H&=Au0+o2(BţNF=ud=e! S&0!` 5̩16qj(3bQPUdS}%67PT+En4Yauٔv@"ڰ!  )XeehJ)Hō ib;#m2StCY4i=_LZAG>n+Y1?bvALANmƓaD~$^ s&EM=Ĉ2GP'gكh.D72ؓ9Z~==Ӫ|p==K/OEaək[@*c tJ ;:`4`r޴]11P]8K"P%aW QS/d%hF5F&DH cUn+$%pJt&6?MR[ M(dQP^TpZnT4fi6 nJ6Me(UFD ֖WUUG@и``cD|(")U赛K(` itVc*1VơȹT9L60O36o20+͜Z7q@L#^*rVÍA:]˦Yx @ٌ8MK:9bV R \tN 65u|ZT$X G2IB,Z P@*hg<uB~uހ.@a _I۹mF^ wj/I749BSL8$k Wrzm.rhG1ޭѪrt8n`ToW-IݰEХa!p(r&t:N틻ƍAO (a9:(GJJ/E1nB IɅ ->pZ^(R`9RQ^#0gW`Hʍs]Nt ZS6k7j6(DJSc4"= ln)ǐ r)UrBXTVfiM])-X C"6 xD"iLJ%_AޚL2ΡxRq(,QU8X\U%eThՀ=}FlPڨ9NVr&cxK3(/!FL$J~!=3(%!bX!%"A:i\*LHnO3)F& 'Qv'I&컃t@FID֔9H= 6|'W1q6(9ln–8 F'9누!Uy|Cg͗ SnY/t{&?QҮ'L{24ZU V5j.EFq'^u!Jҷ3a=`:eUiWr:iXĕ _N QK") D0!<Q$Uqiqݤ0Hf՛-v!\K.΁5Ka j >|F * W^qSL!Χi5bU[!W 5qRSP`Pj-_S]@jC@p 6l*(䂔VF^`Hx )YⲦu 2$*` N |2vcq]୨ PfyVt i!7}0%у-)&0NF á2"-B^K5HݑM+Yשтdcp9h#QLBܡz5даd^*pv9 v;oF<TL*y(-xurLK?R'\܇Be<;֐&cFlΫm,b:tpڥsC8T9ҎbHr({MCdвkkWR. M#\~  K!wѫ7"@`G&%U@1`P R61!(X1mD! (gQu AiZ;A29VL0 VUʦIitZ uE@w"\/X4̳ 8DF^e4/Œ(̮^.5 d e0qva$HNg@4EmnjP WЁv rn-Y#؅SO$wefg.1J{Cnjp':Q-۱aIߔ"M9szl@Ĝ2gg]2 \;?`ehw'rww]İdi{n!j  C4Iy=IFڡV)-OP4H eKRRF~ve g5k Ga`'Yj1]xNqڐJҠ5 ̊~92g!f&JIJƔ ﵢ-Q¬RS6rH$Du&CBa6(ESI>')-; E(3bVnRpf&(,&[Z bZ^DOcd^Uخy!҂|űW>y4gs;oS5_+27gaJ_b$rRT{9QIr6h^|6mPʄI;''\ԇ"PMV,J%ZB2jWo̙a ʪ.#j"UUn$VWU*ǣWЫI"n"Ǩ@"f X*91; G bBpjY7Ѡ|=*(QN *H)Fvu% @r A<>p#o ?-t^*xMʚB,b6b-_ khHy5[,mwpAh5ބ6+gl`)H'TI`p6e3Dh}BԄCΪ@Xa1`G޴BkY5RTs)Χ@JYS!2 0Հ ? ZA))@1Z Wb͚[Y#8UR-V=DFbir6ȵUJhˡԔ`yCF S@6V $ZmRz+C_qJ >9Q)媊R*"܁ؖ2V%`t`Y: ŽG'bԀ]1-*ոEMˠ3\%ba8pfy 1"ϧ1ɠavb4 kg*+m$ 􁱀ֵq:xM9V=TGǻݞxn6/Jb~|>>>ۿ?{x&U| uMW(e>h .u_ov7gK;>|uT@.}Iף~Pf2A@3%cG ʷn7}uT*q/Ulڎ#e\T_Hyt cS'Ԙmp6#:#<'>jEnQ* )|ƛVlQ{ʗ!648},` ѥg87vh}[*#7i3. 4I$M/(nu@}Njb՜o9a]؂ő k |ݖʾ ٵ"+ l%R%z"LO&GO8jYVM/rLqsTFrUn0Je^06L"G0bOzUOMbPLڥE|90qnU-nw6V}9̂%ġ-j: &OJeȹj(6E <<>[zܖ6{[;l9 v)َ\V.hZ8Mwb<:Eb]:gY7VMrj><_Nݰןla9frSmmyWMvo?\EX~UwdS(Rʫr^>Ռ٨ z]/?r\ P'kR)ܻx$1g8 ;܁N3N+ ^eAwiPY@л<((iU'jS70H)iJyaX zS87ܠgfn`odގ PBwU%ޡ7sQU`:6enܰ+GMw?YI^D L;vQ!$2GGq.K)Ep>iӝ (-fDeobׇ38-JS92dԵ/gI'_>9{y+i@:qha=:X+$ǘPGmXHG0?.E,~"H(kP&epk2 䳭4#(γ,?Ƙ--˵9czER['EyKlO] J3MڜGIh?m@nA]ס 8 +f$yp'b>qNZw܆MkMvC:\p+,װ qݡ#\CHTT/RP 95!:`eRAh3Wǎ0 0iը%w%5iZ%(i~u4mزpe &vt2CÌǣ|4.H 1ƹo_zӵt2`Tk ˺ߣTI, PAxdO/5j,"è-gGe3дJʊl3Pgb.)(lPΰi:QHI }F g3+'DQIB>,s( P]M4VJ*0}aqEk_:㔅 $Y! J?wp@H2E)Ldu1 i,y~A^7*Bn~@!MۦQ!E4m ùI[JJph)C'pbBqDV%mӈ{9LRmElT:a r߲ soq*z\Dƻ$v JMHmښ\ner rWmBμC 8vSS8ZFa x P}y5GQW֭i LK2Qt5YgzDzuvA-HB~)"bBI۰owAǤ6dSaIi3 N9~uKݡK< aW_l̦\epP(X>PW[# O1N;ueUAMpJOK^剧%5;{G\%r'!|2D({Fh;!d,jqztݾgIUr~'`WIn/g8f{^BPl4CW#6P9;LBAQ_ endstream endobj 24 0 obj <> stream xlwpSW{B%B* !!$@{`lS1.˒,7V:yoΝ;sؖt$UdMuyQMB3fVp[%Qbk@R3uαmMlE;{:pd Tyh" [sĘ-IiZT˞֖:@o Fl)%㵗-9!fhZӬq,yEW&" {bՂAͭ:nb8aO*fH O'nөC#?;χFe׍I#fAEAiUx- '9';o _HAͲl5Bu ̓&3J\: !{xG,HX=mAC XD u<+ T̃wٴygqDIL̒Lc J;2 gD®a_|N]@LH)&@V^)U ]ky*Od4xDP0E & egYw2YzwJNh9j\a|x֟{ dO6x=,ظ*[R-5f氿*b_.2s,zJ'=L*JRu",!H69DY9 * 7]OAi?#՟̗x[X| U*u^A0b=OQ2#AOYg*vLA$A(:͗:-~it0>[?%&x:,P U 4_dIetG_(ȢKZ/2@UQ$BdAiOU*LI]gjAVQd -Srt? DްkX!o?TwRi:8%N0Ob}(<JvdP9iPƵG)MOB;E/th_.[v >G&t&ZJFgHh[zm8,rE 7?s$C8JTH#) YK*װVb_Xseo mi @!=@6u0R_9&/ kH'}-UυXwk:Awwd/xU-wՈ ;Z#D: iP!*Lb|9YShծ]7@KBLIS>W\3 $ |{@&lOg΀Trx3/A\ϡw-[(. e$f}%v A0*% ʴZtpט=PC[0աdԈ@hYiY"Y𞫌/2Ok%A! KB5Y 8$DBj&d<*DiSatM">4)D@It6D(? 'cb t'roWkQuȖϓGZPBg9jtEHE$@^rETLx,m]62jV&ێz DO)",O~\[CŴ1P *wJczUp>EJp-Ii>-sfMvO5=~ !("u# 3 $~NCoonVOUXZьvXh&Ilxϣ\ۥA ]jW@_6U2SۣϚ|sʽ~- *Ě瓶ᔣh]} )*,4hR [ (k Vߦ6`̂WT;]}*hT샜֓#ȳūaLL ڼWbQe|5]eoV#4D1|\sT_>Qޮz4kj $*X#m rٗ7w^dMdWk=EY#p%6.W=kz6+z? cbUO<(AW43_ΏjO&RAtəҮXH" xMvM٘nָW^槜hZ2iojN [Q@ DtAA R7oP8Sӭ;^~wv q'\q_h8g^u <]"9-&qǴ%Bvz)GIB$QY;sWw\wN_<&Styh)YNlOg,^Q5rr߱cZLAd)O{|*WaR@ޫFiSyww=, 3Cr3k<󇩿:?Xq?h/A%YyK%\Q|l8@ϱ&~CN*HM\~Rw?DV@W 萘0<^d~[|vo׷~ar4wS^kE H͊hzvUBQtx `ˮ5^x](,/r,BnpR}^vPify< ֮GR)YJhIW ՛a" ":=2^53[S_65ͺk<ɖrWTt(܈ :t!vF{/`l^?DJq)(%Pov_.Tz4DV ma|p ^#MM(?J-7U!{i#/#cÄ)3=y㺷~c䲾Ɂo@ 9AhpUP_=Zџ v%~o}!W-tաD.ʡ-acAXe L]֛_Vѣ{6kZRaV-=el"id. *N_xx^|96z,n%=-ATPkv7Ru!ȚdRl”-nzQgd}؋P"DPJd$cKo޲k!cCӑ=}U9C8g@}`IE_ןa WҲn eH}u!͐3҃(xH%ʆX$ّ ,k2>njc1}QKoC*P'.{vMidJIx5?f x $Υ] w=*Aonaf"~V7빎僊3&?3 -xU@EԐt%^3U)D>kzl<3ׯǚ6aAq?%HɞpUp5(&%SЕJ{#N eB/M(1"Oئl=OE)o8w#dM g'3t R0|޷LJOwFVp5UGhW9,SxS_whПݕT0WM{3)oLئ+L_× H Ke>ႧzGU7Z`Ґyj9|vw'`R^: 4W\G.Y 5InR t5*oy< o^t"='>yI*EhSW<2v5 emhv nUn֡uqYlAS@K)[誐;ZˎZ/\0F KgZ ?.~ݷnOfH!W%"2&S&=j7?X#1=Ag;OIWmTHTDit<_$ 9 v UOo͸X=$h3of} .YAHb Q)`bJ-\]BYb]_.&C9SQ+?W`-sIK.nUg;VQ,9MS@װVgWNتs))P2ҰUugOXlt\҇+$eIRZ{U^n»ګ$gTsCSqW&\(Rّta[;4WiOM^gKЮRx&67-\63~ܷ~BV."ΕAx3Y!%sF |L`f*xn*&y;p=WdiY^|K?OˢA/%6=v>PT .)V ;ԇߴe,烠U v K<>k ^k RƑ%[dB^}yQ AkY-(*2VNV.Q=LKO.% -$4;+/E4:A/wt}ȖN+H |m +%tyN3 ړz$hX6OT"ؾY1Ǹ5gy2&t= r J{" E:v'Ў8@Qr!/^dnlǛ.Q-3'; +Ȧ ]-)AdnOvoIpR#)y\%3Mi x4X&i~s0gG0O^#ӿko.kPJ*f;܅3W";"ܝݦ'~[]6~Ի-z`?f0G/t!^3^zΗQ)'A,G믺cF eSٲ9lZ/"hkx5vU1]S\קnOzv׸E7r["cLX0HtIb]]6%0>tΆ[S~W+g>[|oBs6C,Os5Hx2P] -WrtvG1(v{!N*ǃ /鏐'wiX|̹{{==KDWrrYpdѶbbDv$W>c)cL%` (~yD8LޞH&n)ӄax,PE R*W*Бj~h{/smfX烚?`߫o@$CW.%kvmWA! oom zW,–%y x/j a#_m";}WǗMêx^5z6ޯܻ*L;emc#o B !yG4ki}(WRÉƇKn=zl3p΋:xz/Y!` bgصG[tѮ8ceg: =kԕ͆s:&"D0h%w`ek(Gȧذl`ͯuM/sKm7FІ  TPh,\Y!KLd̏Dc|o_b[-gʕ9k<_tU0WDkC)`ݣU'#><9t@\ޯTbQwPi$Oh:yC_G~KUvoB/9P瘆P%z3Tn1x%}ƙM_|nׇD"~8nRP~YdGf.qK;l>cG 7#m;F5ld A:O?lYz.8 GYW[#b${QDqN3~v?}Ik4K(QR[y3y G傿 G:n6W9S= Xo9CXsd֯ o4"vJ-D8+pepP~n毽 cqnCG5DMz X*ka(AHLnþr:ޟ6(\C p% q"[L}y~ #(ŗ-'w"23W<7 qұݪ>g tn~}æhHz&k7π5 <IpN1*uGbclKYBTw_4O=It]q3k2xuo("jYIY,gZcVt˹ 㮹ׅ4p9bgm%[-o׫ zٶ<)m]9w0Ⱦm_ݺtc$%M bj/MQK]G#]Q׆T`KV2ڃvHYtA6QBܦes?ª>Ys3{8~zdv+8kvh: G|)`l:As_=&b֥qlt[C_ư|K̩5,S-,I풷G}ֆqJu(?(AIY[Қܜ kg_. #߷y̋uݲ98 ]0jIu~{jN2xYl_rV(B@)FuW☲ *F"ԧ}ū##鑛K7ђy?^Y0 4i`Ah=*'xNSZ jO@ޯ0!wHa:G ^ʗ;ے=}4B#t8UhOUۡfKs|_YfnRQ6w^P8rxDD9ciXo}o ӑ^,P0s>%y]pLOl)~=-Յ*81I}HGwBWn/nIݑ k SN1.ףL#/<O/]tZ#(Ʒ׮f7|v> -vK¤{Qb*k]M;;Q>UEO;?)smLn^ez쨃!t빆 \"A%5jv(fxHh?I T@s$Pʶix]1/$7.ZWfRȑ#uԟp ]pjwAɊO!#޲_ȚkD[{C÷_*[wqd%˿~\zس0w f7?jB޶^?Qˢ2zZ"^]>$Rs-Wl5μٺ)]).tcjo nє5 |@ӆ?W /W}]7>ä//RԻshAqZP}X\OT|bb%&Iark;:fcˎ74 ٻrt/ gbo-\ztQ v5W԰YnVjw(jw{؎3Lh8D$Va5OE,fT+ge+Z9T;:pk1n֠3oG9Nrok|J꟦cv8җ>%w n:~kz;| 'E{YA^;ɨ{<\ T~/X^̔ +T4O>Ԝ*j/~ܼoDO}wwݒ_ _񽫧c/wnB[9IZv5?|3%kd{0e(oxȢ'kՇĎ+q 弍RAW}xٹ#8st y׸ѯpU%z]qEiw ޖdHPm Vlxsӑҫ+̾cH]H&*2RC!3cHJc"YOl"1g^fO6Ɯ9-fTܲ- _v!}'g9vv]m=f2_Eh> [&J5ȝ\n:OJo&r'] [Μ˔ 5xĉq@s%^]UfJEkPjH3ɝ}ѓJ`91C]5akqa{×$gTN_mql=wdRVn$+7[U;,vjv9ˡn7ē7Uン_-9KKVHLEkzRRK;ͥ/)]Rj) GXb;m<;(>jYJt4"qWb#LΤذ{o`cB/=DZ/inpk4} Z$SRGy#ߧRĖA*S39SU{T:n 5 = Ӿ7_jwhƏR++)Pr{]|㦀a]LNs}O2wlh#{jO׬uJJ"w3e+e>[HX3l,v B=ȦTюtTǎ`LO]tk;+V+|SQ{/;,MzDw1==7tK%tAz7۸n*7jW3񼉞O1P;u*_][ؙ>Ֆ3;_NeAs*wJ5'@s‹~>RL xˁ6Z[,ڏޓ7~VV9e}_"FQ3~ιeþܥ(JQ-hSE ϬLJط>G#kwՓs*Fl[1o-j( ,;ϙ86Y*G6N2ҳ݁k#)rek1ME,E3oK ݃bCno7w^$C:rr_H S3jh-lG WunA!и7G8Fg d4Kv?O&c* !մߡ^hWBt-E*?_fʏ\i乱x!3,}5l6nMmY,*&>&TWe֬i?؊\.#V?<0˾#h>s~\1eo5q5sӡg+ 6{z>8wtO?5_ybkpxoބJ?KVbm[o_%v/7oD7K(iةguĭv.j9_p|2s$%Mt~|99j1W>_?](͗Ƴ RU_OT-?BdT3 ˜y3)GgG9odOe~)~|XP,7}\ y˖fP?ˉxƳ}wUa9cpoy-׾٪ږ=C< <~ԭ4fx2<1}땖oEѱA͒tpЬ?˚Ag1mf8 2iaR 1ecǔʹG~  iC4(+᯴_{5ch%ͥ'Ozl7sȚ yhgAC ަ ׮cȧ?_ޑ=ur_[%W輱P >J{ϗ0;:JlW#FJsٚđa6{aH't<t(_p0+Wuwy\*hT/T ܩ5ѰooOr! {Z~W6d'x. .'#^|f/}TsX̗!_<ϟ{ɜÇ*ֱmy DzVS-~eחA=uDbס|qNLD `HDxP7=r|OdP=Wj7ChہKL&!JW1Fn#59!_t+ 3׾e8TOצt$-lJ[Q2_5[:G# 8:}?v11ݗ8Kſ3??~.ly<ဂT_'Pw'br-~+p(sΓy7a|;7Wbı/UYb,:sxo̻Y|bՓ}Q7n e a٘n=[0+/--p@pOL:W,tRkv !>O,y:_*5\1Q*'SQL:/Ĥվ=/X.hZ[`w0 Miy&z3W@ 簚XFwa_b-: Ƹo}wٮYy=+rʱl[\[x̫ȷC .e*=-4 O'\8{G/9rmTۅw' #׃_vqo:n1P@Ch]ݷXOlBG4>w|0s usm; c  f&}|d6Vϳ{}BB:ec..v =\?CiS߂z-ahpiz V 6gs[̥ў_q?#i>Z/̙*U,9tX(CĿ& %vL=z~73&+(g}i7<F|$k4dkOwR䷖Ko9oä}!%叒F(_aD5m+f:2sM05<@Aod\Ge㝯 tlbl5XZL~/C?] n[eenM%O S=DZ$|[5,g?w8[_3HBb!=ZhJ۷ЁjhjRU:(E=DNוu$ys8c_B!=sْ%LIAo{Wf`ETDiȝUN,α'Iïȱۮ([?;0bM|`{b=)l]sK׍UlY}doJ)2Fˡfh=Q,N8fСa%fSL xi׶YG/{:'̠BGOP?Kpo(3H6g\}"MHZv$T$~ Rmt#3  {aTuJM'+A:d ]QJYApy7'@q??P}}?vK'J#i۹7X7/`ώ3c@3h=G{WZ=1J ƒ[ br-+*odxgiwn O[8C4b4\ޙB!Wajәc%jyEn'TwE=xg f8L'.&:q,:t Ekz4͎aGKe1*s/iX]~P+SA}h Sa Y^6\ CF(^X#^mTX/NFĔu X8xxGڛLh؊BP= (Zm?Z`Pd5{"Uyݧ@ٟk[HR b, K \y(7TR;n[0R{⇂dCwm ̛ :o-?<^~ZB/[jS+5DRZGR6AK>p>ۮ~&4\jOVt8j/}t]˙8ܭc[G >{B|囄K߻.N{+T|4Jq*Qo!6P`?i$2ȥCN~ÎRvZv4G(+G$3}7@D!xW{^;(|006rIj+z凚n+sM>:M6}g<*Wd]AkRmҜDߚ _'r[a.t]ع9OD'wT/ 7Fȍ}[궋N=rjsC Η53enh:җmSX!^c4Y :VWlhl{)/^v}儠d@$]4[ 9PzMfӺ ӭo7>xmM*ϧP[8jEߴxT~J|e+th^MaK:_y7fBeP=c+l(piN(~3Mv*h?]k 0NCk|Ř˦3o*/CCGqdqcVVWy_ɕ.T,$k`]}Gžɡ_Eк{Ď( C7Wm܋\-RMziv…I/ڙ{[$xŘgL8ͼ[4ƆR,H$?< H(}]ȬX U|&!*LTM\x _!µa{bdվ$AD*}:R~ԏ p=JR:o]lT IL*̿\{u*aiw4[kpbF#A|)ߛCS0;HbG#x#8e'fҪzsnC)srL=ꮣĹ.Й]a O'='nuLuC%~u@cѢ iĉH@s]v"zw=wuU#y*=Zi T-|Pv#sO2U}wmt }Q.=2jKUk<.|0kXh+X:0f&d'H8aPoTpV{XپH "i *Cl^&ĺc [U[H)V ~xA jp~݃0݂"/u>Ao"H1;Bu$* /0`UTua }8V'h*yJhy@KY7sh6J|!M8By<]5`?{Lj *\b%'V2-4&2Hzޙa5FN%Ӂ_/u݈֬V|Lh9|& ]w4/B5+r 3?~m4xjq$c~;tyjRUo zWa&Ӯ0঩TU╰j &ghOg*=#N=iQ5ĊTb7(r}iĊ&pO87 Fo.vhQ9Hs8X ~$XhȶE<ՕgR=_Ygle2B 깁?$K i 8Y5'{A;;C5хTL ^{="}FuI̫p*݀ɨBx|ò?4tG{kxNٳ,^ !Zm 6JePVCb%_Ƌ̛UߚcgS^B*>Hp@ nkN)4@vZwS碤fzo)k>fM>BIZT:Mլ)]*4^zQ|&1Ұ!+k5|[7FR{?sazȕ[oB:u5htvܑ} %Az(qȠ ɏBP?u_0&Oǩ;%_V @j:0|O^ D}N nt3s~,iK4!]^OcW?}UHHuc&k)ar@ 9ix+HJӀ ` }䤎tL5ӲK pxFG2= [n+TFIct"U׫_w ^[@6M 3{ceFx347@t 26DJ&r3e8iEbP_$b>`OK]5kDY;Ÿ}xVYLIK7PšOeI$|]a:qi2̛jPf鸊Dy3 鑣#{5zVmO4&Bd5KC%‰.^D?qIGH uFה8+08>|?^0٘l䴟;Օ9ѕ GNqe@]꣇1 8-V}_B+_ߓ&>/; ~CEyǨ3Pѧ%J=B iS2c)-|*QT޻O. NrUV;z`6Nw͝͝We&@fwMWӭ\~xoi:Rn, ]^)է5 eQvɎh"D 1:z QhbFpGk|A` y\v.6# q]M¦7VoO"'c9++k}0z_|sw:%iug,lfIA|=9NT>PpSd/j9Fbȃڟg?UbFXuU'×U7E~uGw=RcPLuHQXx BBZo(`"=|)M^!?R%j5u2 ~?7?T`]B{QA/-Xb2 bdsZw KMi{Ow;F;qp[h*@z 8z 86AM2p\f0Tv&ćN"&L h P}XԸAd@sQ[Ե uoDS׵"hrf8>;2&Li<2`~(s?{0>pxeߍo;:).PSh4J~8I(FPj*/}ߒ3"`fyъh$Bb&ŽSO@*a&1FSi&cIң m>QPJ)!č?yqtW0MP"đCQgj{sY1RiTJġQ [+`l'L9l:~j[o{MRH蹄Sh k+J޲C~JU8e_8.?/۹"Ċ V/_ ?Ol^ ˶* !Ri Z7M+zY@yVp(;b6^(;1\ LB'Zq8-!>sE~ ;v) D]W4Z@%h<_rLZ}\y x&9#} \L֝6L~M&jOc W4̀OIsU1xxfML$~8D#5Pc"F D n.G?% u!މ5=t|xTlze (2HB4Əbem!߾TCj |࿲rɶmBk ¿A`<{M-;t|E-!"kT ԷA Wy3=o>YymWD]C 8[>lC+ZgKPb!A9} ״\-3rL p;M}i0æ#V2¿ִBppShr"ckC%?(8P\9Taj:X oH`qx>~(q">puG/mCU#&^uœ[n#LWىDxoox&5ʏX 0%=_7/Xpx &,̱NYs+Xޞ2^jt%bՕђ1f55ӪPbyzu &_4~:+}a%)'NzoYѢ{w˦CW柙*{ *OsY%UR'O_n2(tl5D ruu-ن⭛7K l@A 'w(x3$vk0ƁޛPgq: ua6CC* DBqTuW]Wէ+  !ϠEfhPvS~v{֗HShoRl$tݵǑpdhy_,w oWn\Uɽ8jIT|L3 m5Whq ){ԃfdoMWoi{g.Uvo3kh D PUImm}ѝFm3kgKS;jm 5^vQx޺bΕ6y_=9u峹7r_ N'4y 0APym۝%אjס΋ %V#83uU9͢fۅeDô擬{q.'\q 3LTm!B.Ca? Gxb?7U_9)Q.&Ne)'][0*[^Yeй;S \M]ult,M]l g4kCG##K'{sK ty"}ZOTA#lu$ |Z 13ץ/f[9j鬥 o{tnr{'ƶ,TSZZ+MWYP^t~$-m0Q?cg-`]>fֺ c5;EK,Wo2V A3U60'kZX۩+n?tc {xoḱz?VLo2o'կ8Im8!9|uA2WnkK0L3; ḒbGęD*Vaӄ "70Ib#\p$Q"cOG#e;XH)uKgt<]R~_BgP5k= ٯZfOMYķԋV:zZvzV  x9jXXY{X9/51SPirGl4RK~6VE֌E k5\=|}<ȪKmM9Ԣ\ab؇nZd7 ;F凝vKu =W1-ME쵧+=Ч2Z9xjvwY: 7sdEo AZlO\18@\EZ))p/OI[ʂht9UK%z9$'uEuP|Sj,njgJEti@  L뎠8~Dsx4%Ha&;dU5B14jnHT^"|2Es:P{,7Fe=?^:*f-,j;y;;JL 1172vu[vW}!lt+g]5vˬ͍mtnzjN͋LL6K+-\\Z]ock# Q#/.c%.{}~={(]M#WgEVrذm >m+J.W.v7uneMZ a"}xjxZBEM2^KGMCCͮz/;W4h 7\ +30M[xy;EZ5o:w+_9KgJadgGɜ4\ S52?NO咽Ψ @'bݰX&Ѻ[itQ@"]wEDssMKo0YT BZ4H5@3ȼ$QzJC($pSe)ho^;,.HC:=t<,uIW:漩y39=Kk%S]'&q&ygV`rJڣBS҃&Ay%du-pV h|[+nW6gpj'q$?l-Q H7xxȻcr N}qHeA."2'=&(;q)U0|mA.wSmeS+C[&V1fm,xӊ뚼LT♴ ֩J%HVٷd*}P($L"?n]ޞ֌2/-oiNZm~o3Uz;]_ ̕A {GHﯗTy@gϟ+,n8Wt*sJ:>3ƾڝ GWvjr#Q$ J_T"1hV4&~`-W}B;nʏu}Uf *wlBm7|m:#6JOJBW3D}%!I%妼LI^83 KJ4yftuOi#xx .WKQ?F,y]jsx!XP_,mj`ǺGUejN*vm@}%v*UWuktal1 e@aR_tF 4{gZ^W/ށtZ"5uĒːz=V_ʌFƝumy잹 =귳Ӵ<mƊhZHEE&L_ђ,X5EOsow]%ػ6UK}L^# p#-?D^l #AzزUZ$]M U:jJ:XNBo4$j?SPɴ3d`6D@Lyժ[4xMi9Vgai=>FYIudI$c+sAAиjM|pÈb2A^En ϴj'ꥈ a5iƊH{Kq%ޥfkmb:`LgXMVN軧x+]#=߆5;!;M݃dɍ #mCSxy2S5ۯ"`?G}/ 7AȦ 'ὡ0ωF ,đ*ur6]oWAC<#ӂMSj8sx9i)񱳄D.eo mPmZ-۴Q{6q!oÏ i+n|G_w] r gY;ghK3pn{ yK<<<]T]rgxez&Z;zk#n0Wۚ5uHbQ{'*\mJnlI iۧ项^Ve`QWU'9 UfcXFvSLQR4NzdǮM|t6j h ?Ox|xM:Bk߭S3Wb ?,ٷGLxLwn8@ExZlR'#ʼDQv"g_* ^(S- 2z EPaDJ|!kuҷG&*oU̢cy;M>hq{+zUZT'FE,LndTz'bhsvtWZ`&] W:6{&iTmOwR~_k:pbŷgu(uo$b٣дMQ6:EꖠfuSd-RkBqTl5 CKBx^J-ɂK|5?|,^upF7Zo*^mPo{iZdhԟWۢjKlM$c< _ 4Z2^h5YwI|1G%6{DZ/ U]&NGcң'57-"V'vKMWΤˍxdqA8Q=ޛRCW(ҁB04[4pM }**3'~7c͊w4q\{H|q0`؋CV\VKQاr)\:vi:g&luN (^URÛt;IxvwyE$xhfq`u6d‹efQ͔vYAr{Tq6Di~Bu6 ;iwYѪ*4IuF #Qn@ڵr x!Lϵ]{Xa27xdL츥6gPJl7~A"dB7>n-whK[AcKVp޸q*qFppzfڅoBC{7Z7I6K7Iu 5Itxɲ8(Hs1^}/E%Zd2G3;H)s)6Kt[H:轶HkSԺ"<3t@OO.޻V]N 8po*ܷmVomX}t YoQBz@=̺v1->:4B7+$2uqbr4{a::˶y6:*MOVu?֨J5̴d*H&+صG֊'Of:T%A-;rÁv6@U@ SRadvj7)3BOAIngz)zYZ9ZE+Pz|rLjsW)7yk~hgAe c;~$Pm&$0QO8$Ph2GSEXaSs u,*B;/T'lOHwO0Au3Ln\w:J0HCL?"`4hqMȨVoaj "]wB-Z֬U6KZ\՛Rn)[dF}GǿדhDVRk3e&]kxJvg6[~GqojKK|a#@,Z'yb7S '" Q_Qޥ-pp`Ej0SE7eOhCedԞ >ȞÐ]{-{r)<OpDS#D8XyԏU]| h|R5iWKm#NV*coX%=Y}lw;T%־QLs))9뽏|=,k) HLG_T}@~3qG*З}ch bCzbv¿c)Hԛ!] 苑gLLCN}֏oTSQ;2chO#KPH%ffrԯƟ)ʥZ^EjhO)vk4GT!'A[J{>2S׌~Cʟ?h#iŨmYz~bo΍ oL|0BcYzj47o:σLX'#VPrURuܵU>k +zrW#>QGrЛA`z4UxŹ&ɽ ܖ :dRqh8M%9yH2?Qd"8 V0\ bXtGM*"p~{*y4u;2W5.So!l#Ԅ{]Չc0~Օ#^oI6FA6 ٨<ܐAV)&kSQO\ȊFARGu:J\&--M Pw?y'=TTGn9I@j9smE{#1} 48H'Ye+֡^rvIaѿMԏwUQeOx(9աLSXߥu/s`_l"|"69Ψ|D/mD21j\(#8LqGC+ERzWJ{ȴ9I9z=]_,QFfJ(߆.Jh=ScPĉEqw5HBJ lrM +*y'͓UP9]Km妰zqıE%hg^z |9ɏZ5@ 1Sy41Hߨ.BO՞>nQIS@3C)ʡ҅ZjblUtPuUsVSHuZMuǃܳNo7.^@=MS&f,fw߳wUdGsAC;fyE 1žIǨӔ(sx>xIb۲k C9ޏ+$^'Ofsu,-'˗ \6i:O[kHrԶ uNa?%ѷVUJ ?r"`Nezka._.y|QVh54y$u@t4֢UhI .34ԵN ;@'^G.K oTRZɕ}ިԝ5xUňM~r; 1l/&YPΊX1WS::igZ ߶R#R#RRՑ@ބŔRG'DZݫ)=59;j\>ejS ́5hODg-EG6Jr3t%:_mpbe#[xoQQ>`D/?owAfC!ׁo0x@)I"GT1^dYC٤<.%&w1=K ,7V;rUm 9sO/TqXYp w4jnpWuCaF.e1bן>IG94mQX]~Q3Cz*~Gc18axjX*UًdڼD**B4%4/cT2}"-:{yOw@C((eRP7s]TmcЪh- j1XA_kosSk]>bkm,:l :wPX"1>i}wOў͊&kh^&VQ.HoW*7ɱG6Pz|2fش=KԟO0Oeqh`/T,؏ c2 "G5PJ #8"/#73sbb%rlw%r%*K :}\ad 3r>㟗:+ W"4 ٹIrqg@Y66Liʨ@[gWgQB4%`kodʧe2; 2xEŒR@U:_UXO9R-U ] 02rdMyiQjCf S,KUHU哙}|Z/:[? d>눧)"2$=M ]QsxyDBCsԿ[=Y)%w&].[l%KVRWJ軮GT4>!юU;)B$2!n$v iDOjCF[QAѹ Mi8d0_F5=N]6? *qWv/ŒkΦE2h>kl~,BCZG]E&Ok毪5uiM?ׯ]-?*mCvw Kwbww,T[nDJ$[9{8=wg}{ܟi+X+42^F^jUՒgB=mAVڪf @J;1M2z];^7SY1-#!gqtV`N'0a-|ڹ/g`P6dtv^.Q5j\Z *m$RV9vĞ]*Tivj Z9CV]\XA4+Exĺ[mXn z ex)w,Qw35>lS_r֘]-m2Vbo`֍bNK Wʏfb%*L..R }Ds_@> P-2AQɓwLV*4d.b6>Jf.{{f7׬e&O`6SqYKRULT_+V'p%BJeR[ dz䷺` 3>A^ 7r~Fꭈ=NA'Aũ3sAXPCՊsdC{s+0CΗ)1}{sO`,y̞!vlQ7;-1Τjr!^Y-P0F[tFztT8qs$t!+@K2 ݵ@oҧu/_͔u$V¦Ar֧C: F&$*(m,+q3$4jmZfPΥJ\)S&FДX[2p>&AjDL7{ᝍ  T%J$rQ\q+9E3r64VeK9llI)29^Y)THU(1Dvpr]uU|GcҗCMacK [{-ޅzsFΛj0xE-H*vĒ3DȨ_67#K|i:+-n2T|F2 xk^VT/"Uo0]F/52FM,Z#>Cv#Gwrb%+O7a^MR~ RYb*Uh ǥ d<<QfcRVWBæX"tZHT i Pbč +ٳ5jf,Еv&l,qfaتbr&͒?1 ̞ߜक़k%)g;}Y%U#Hɹ,f#rM_l C KSfB գ4l2@4 'TcW8,V`k` ߭U+/Zt@κFQ*)R~}GڶQ";v+= >s3gk-GzqJCY^Vjꤔv=N4 V%$eR@5ufn1(\yR2@κI5 ջuU1l6V G4hƮ⫷ z9ؠf~(2^KL6}_*V,ѣ%|J -T.Rin1*6(nsA <<ebLh.*c QʭTV%,44!5JeJ0T!ݱIA\B9N `l ݆LbZǗ q29Q׳OQkPf괤ti~ub5@Huؚ>BZ@gmi!Rqv ~H夸w;ma0s?qKs/ĵgTaصz/iG.9_v#wlC4 #RIV,V1FJmC5^WU+Nrȕ V>h!ZxV&bH^CTգPX \_ךɅN'K@H#M@hr֎gżYo2bJ>! V/&DcʥfB ⤯~^f A8 iTj.^GVku(DR&9/,@FnsTeh)I1t\=rVDQXSm]99{o,Ni//:rL0"F hcf2"mXv Q)w[;BL:Zjڱ^]|I<>Xf #io{Uf"'b#z[MS5rLݶhbF0@Zt\)%P@JPBTp jϳ@Z`j bd-vp|L;$oLzAbr91vA"ڳҖʑ3'rA^IIڬt-[HgKTs͵Y<]:Y`MZu;0m'a}h]Pb'+ ӘDf<%\d!?rʯAAaKgL]|=*N@À uP Tɣ&B6#Ǖo'/*y) T1IbWgjY@moC:,tqtLHDHR YATLJ'%U⍕o[H$̤-oȶ0xg (=(#AS,#gCŻPVF5hTtbyͯ=x2mt>\ E |{F:]Xvy Xsr&ǯ;$Rxo_=`5\5ۜAbrp tlw0xnP^<W(NDN-#OyкUϫ,+@0k jj*H"usȸz]V!O[c'VK4sBLsmb>X̤zC\TX\%ժ$&)VLəwZ[A#nd4TQqyx|EVWX6 qE$`iHK)[,i,?ݐΥ%iz‡D'L,PLAI  = 3`S0\ g؃0 _9V|< ;[ꚍUo*VznĉQ+ ^#5/J1f| H`eށwO\T_^{ۮ/70&uf.g2^r'c=x-b 0~-1|PmPNqJ:#eܖ1bBIk9'1)Gܸ Y9N<$[9=OLb35V,B>M)eMuDJ E 2W.1 s*EBdCz**.mQg֗ki4DQLHMb$%7<^LcixR&J"(Wd oC%H&|xx /_A]2%B͉0̣ C~?Ye2@Ek$[He#fA$Ϡ)$4# CjڷUz54Z)BD倧yÎ]6:S\f9KF4tf &,3u:rzP+YP 1KwxۢXC(T[ܽ^m[m]}h!-{QZQa\"p4ٳĭ%-F NxjUC5O EϙZ1RW J$E *6"5$t5&'`r,!)GB,!Pu$>N3ApK[PT99\}}=BUM&"M*oV#D:JHGh}iLD8EBz^j^ǭ  k";tp$hvMb!NtVZlyMilb12 r=RgYP#%e&+lڤ:Gi`{٨Z6qs.)RPȔ̵Gx2v߁a#.NN}rmP[?f٭M6qs<0g0^v5>bs,lY9e,GLc& Z8O-X@fLS(0+uRS)K%l6o6ck[O)p>y:ͺEU b>XM5/#X uK'H<EC!*4>lMLÉUIkjEKiN8ْǜ0-dIOȣ`hZi, s@1&#S n*H JࡡjZv ?@)# mfwKt|TIT2mC]I<n=RFShpz/cex!s 57-ī?-Cвi".1^kp9,; v\4؈"'1"6S s1n7%WU*\T_stUQϮ2bXqزv.}shgዺg/KcGJM7/sc'+Ճ>41ޤڴn5rY!&JY^1ۓ4q:L\.iA6v5*J-LdA.I m ]Q4$T#REBjV(/,} BX%N[@^.`gmF40!FAA0DaI-Pҙr|gu%x2]ݲH4Ƥ zH3߈k3 0@BXE&PUR@BeQ2i9j+֡!2p]F{~TpO>m,wgmCD'dsi%LB&ˬe7h8veN~芆p~逿s?F}t_!i5j ]]YKa7K kh5 ѹB [CahH2Z%i  hb26+mK c׻>n7^@)h%z$Dq]~ ^[M%8|9T4DP@O8ڬGta8aK2JW>;xٻ5 L7L =1{,ՂƐpN"Hw6sehGk;as{a-6u:l Og߽ѻB dP|l`Iw{s 7Z5@[)VD1:9SC\?\vL@_g| dS̛_~_3{Ҟk@Bő#عs={YaPƓ2eVtm3S`頵@" I 2* ;aTϱlTC; '=mt,&rUDJtn+<7p!LKtxFs HW|.TZ_Y+ʏ hg,_]n!ڻ+ݙ'Ǔ.^<~qlm<1ݝL_ٹcuڌZn]K%ůmyy %r>|m%-ٗK Txpޭc#L_A MS*t ;S,qƼQ'Bk x1;fo̴DžE)/c4> N+g-}{ko*uj;ݕm1_=a~hx$*굠k+~]K 9˖\|*{kO+67?J-V,6h`"hMe6 %lԲK8vf (U,Yk0aΒ輰N¦O}Au8?oŠ0v|EH1MA g x pu3_Bw!Lku7g)bz+Zsù^oXuUժv(?hg{ch\UBk-I!Эs?a*SRVNA&G%2t' [=tC8}r]Xp \@5xs汐tPKN%Ȕ$>̅ϸN}ˁ H(l ;f닛h)m 6 kY)վ͝cÆ쨷~cm;`ݮ.kM=Qf;,4"VH(Vhc2((]Z'䠂nw77>5V c{}6?W+ ʌA-}hqM.yV,el8Zyn$w[>EQ>铻oCգ⾌>ڳ]8 >@=v `gw}oY4}oM6|TB8_>^0# 7 R =aY&| w}e40 [:Hce\*[̒< ,a!bi ]x)jmG``Q;ߧgGw緆AZoׂ'8VعޓxpvɆ ʮ)>ǂ;WKl+8ݩ+Z,RH3+ylȨ 32q`;WwmE0G"ptADi <ƀso=lBi?ʷ3+b`E~W]؞bme\cZYEmzǞ'+-mu bQLܼo9ZT a'T`'zCぷ-g"/s>+WI cF뱯T6gѶ_[ooM|g+sP܂)OatpJ(Cw|ij72H!1=܈zϭL_nV֚ݯùO`]HVZBSsR1o8=68z)1NL_7tT}m̘J~5d4ڑ}^K[q.x ͽ. E\d,CB"(hX,(8"AI Gl0D%.U)fsqhD"H#c.71 N,+O r\˂T3s-j ݧi7z Vz&ô pZjW9$\4saə}E GT[7ɢ(XI\/My.2PN#WyS6p]) ù#6dak2tB2g|}py HNηwomi vL/򽝜肿F;'BSFlժj b_d/݄&_&!|eqd"[iTr[3O`Ӿ3Rӧ!x 0tiez!~vjxIaQ lY.$Z;(x1@M KfbaW= O05W`{kD\*T5%y7`**kC1.8ʗ7\kSe q`WV{*wi:]P0sm07 dwʎƠ'`xu)+Q6ǟ,{Z2y4:ӟ+c))hÂ; 2 Cf{K>p}5ZK=1l񥢿7"Fsѣ/ ~#5'&S'9E_҂ `!CF[௯ּ;lzˊ{wJ:Gu.svyަ*'h^Km>1_ E%pyK++e.*7q\I7,Vdri)8TB*Jb1*%'RKxo~2pAN\m Wk7<ٰCȐ # 8`X h pO9g^[0/얧}oj4 9uMl oӽU}lL?@6:H6p1>{s[؉Gn^]x8GܹL8_{xݷ yUeyO΢VSł2ݪglЪEp~j̾ ?L@X?|_| hп4+#.߽?~8C4Cܿx\uN-ԾP^^6~۲}L1AL3~\@N2KO=v¸Ćд;pz(mBB.9V}l"-Q94)6ig' JOt@%{ø[mp 6p>gl_o(BV-sxiX4ndwqaϡ{C_m\'ٲ Q?3.n|/gp8 Nf;Vr_ksCWAS6S+n^duŃV%߇F'yq }@bN2$15 7v?_d8[ f,s/ρp*CΠm;rpaǑJ]Nbۆ`dgQ|mI|~"VZJʀ ș Vۍ[ xn8`zu'r zh~5 ?Omp~~}:"A{)s1ʌ'őS8 Q 0wl?<ȅw.E cCo ݻ!mȰLC\'w&-E[L2/o+ϐd~b,t /¡+CirX8֟vkuԻ֘zr5fZۓ|%q]anOo"n*&[J%N: SӢ>dv\;"gQRL3(KTP4R+mF<&F Ɯp1Әei!5 }ypƜgF ~~:=36\gԭk>:{9{wXJKD[vP[ 1o hϲoECp0џ!,‡%ĮSNg^耊m9B<>m/c8JQfs0 GZlxz+nN8}[>'N?r_,\a5zc"&M71ґqL787j)I٧}kۧEL8j;=GK-o~.<`eA [z):LF5Dž=nf_SEՍ//ZxA禇MX[÷_aǧb4zs\5Vp&;NԼ9ʑ;K*}+,},c\&"I@Df>#rQXր7ۺOx ?]3]\rb~K81[¥N7&X퍥mNᝏVQ1m~BW;[T{}UۓGỄ9o -O?Ƚ6g9ػʇ WS'73!fqWQ瀒w(=@6VüYMW&nV'6ۆryRd>&EW#a)kNY747yՃ6u>a,`M}.ˀ9#utV7#p1Bɘ ܾ`М($iͯu9J=X]T5U?S{沋ltZk%eqx)|{6c/Jջ |q‡@;-=vl!KziE٥L| ,|4.D{rgW;% ye<􀃷`FݺXc*X Pc zz Z3[VSMw@qw{vua8 =8ǃ61û?'{b^n 1ep+.y9B}کb?c7WQ+Aw1|Ngp8i&Eg1*@G9|l Ǚ{{HՐB%%\,wᛃ_J 3O/%?$hHͫTR)jń&ƀp3 "ux/XbMXWb{H;v꙽:_\ϤoZA˰Sft?:(F RC2JNĺT^C].=g[k>%o\z{m3~),SZ-%7VoHYcQ$O*j $Q&Pgڑk#F 6|)ZpI ;x< ?o/w S"ka~; 3cѰ>}5e[ɿz K5@߭9v:_2!^(Rwa({Xm|h}=7[H ʳ GuQ>N0{^f [Fs4T(d*8!"?[Rp(#Ͻ.`"fY{<{xj,](rV3n-B1#K 3N(x/XFnU1bO=8oy xθ;~wuA/;bc7S_"\L^9!!0l:/(2ku[`egnE8]?,?\pATEyTI;s0lmCp F6J)Y|H.X(n6܎9j(b$đ^+9n[xܼW5 A/γ ]SM~Ώ~m+:a5u!_% 9⊋රAjkX/`WQl>98 hF&zQ,U-~+a;'G+z~a[:r.`˦)V敇0՛^vZ+:&"OCKG_b炳6FmS{>:IXww̱g`虿gw;>0'š0>Z$z/Gn\N%=xLId]F')~vL᪾f*GWߟޛ׶+u ,JP% }R￸+b@q6LZ]TT]}t73 0ݍH (%-HRHXfY3>{lϙ=)V|ULූ#13%^@  Y4b43b ベ]Sg"oᅴcE76<oB " <y(}W%. ib=JP l^7'.xL4xrdⳡ8ftY@A|o'QWif;o~wr]oںHCM )Ȼ"I!"(|n|f8@fRpt0ɒ6cTpB(pCc8Xp91$}&t!6\WXvV( 'KZ`AaKObP̘G)ahYjj|Kb|t! Zvr32Su}-;~>JUy<~_* 1)_g*# u'w_^m_UF}{x|^x(񤷵 J$KU-;0z;LLN(bbT1Qi>9`ΣR,!ܨ]µk?l ov2m5D8 )zAyJDƌd:Dc-+Oz'(~z \R2Ij7iHpFA((o(Mp*C{\Bp[oʷ R4Po`Ry|휢,ij*[QAiX33Yh#5c/'ԄwքuR?VvI/sZ=^m ׏?w0q b-{z5R@~\]R68pKwݬeLR Ln=wL0L& cZޏ: Z!fKZP< 7yRg w-N(͹vA}Ꮡjnz=hQ;%w_௛O ϖW3zT * %7),pNQ8k,H@yvjF]Dl mU{Dp~1t5}Xu2pm͔ͬjEǁ>#Z̽T͡ڣs<SU\TK=7Vrˇ[ 豙,)2ISa<\ !&2@(0'RH[ՁNO E[$#$p^jZGЯow"[)}\qIGTPD(%yxy~ z̟ß&r{#gw) 7SxOI <(a\-TP0FW i^[4o*0 {/Nlvjp#cK˙7J՝Y:mklY'5olF?HTk9izg{ sҦ;c:FYذ3լX&U]XT)*Ze! %NnE^nil ~{jՔrUE5h8|MF3+Ѿ.x.⬓YnEh~F:q\LI 7Q`F1Gөn@h,im3ᣱ}~t>b%1yZ"%5r>e䀾僶aH盟Uei[.ˎ3?&Kg&[g7ݿs㣥w'3KK|-+.-~D USx\>Y` =VIVȑrI!z {G}Uŵ_w__8\^VqeOݚLs&;^ColUh>~!%ͱ'BbOAdSIw1 D ȒuNJpRƲ' soK?p%i"h"xԟjknvIT0'Hib-4 W;YEU,?/~~bEF!w1g]r&aB|@CMДS]7s~{/m+vGZB)uIrJ_ 8ҾY}2mo4ujo4?ptڥ7ܵyQ/֨!0H̆x3y$Qł!<V?X<\> ~7{o;7-wXl}c˅?n<vp8~3/Y[͚ղ\žg'߭brBFQu!,'p?&,F ~_|_ٵ3>aGlr@p݄9-ׅ0@^v `PGqG1q&-GӼHA}%'4 b+3@08n{;!/rz[]+;nNk UWYx%.LkUSvCfT-MKQSu_G'\^'ݍ*rD}_b\Fg_rNfeцk# 2p_'+j_`[.'(pV?c'HH@HsOh3EyڍH+'3gꊾ/|ݞrss-;]%k Y;߬v, n~s+՛)'W bcVFn֝\~~/>0|!m2%CM 2Q /wZ~9}o p㿷߿޻s4x!'ܻ,a/O=G }ڐ$] qf>zђpm',ว )ԽZ1`րvس/sX`VnI;o|V >ͯ^HI}~3`fg8m](PJo7i>lخ)ѬªfwK,ylKq{k;b/s%BgbXv5J,w'g_~i/-R% ~~ڸC2ʉ@ (E`9PeLf,t7ACRMJpSgcR6#o֧̹;^>q}틾タO>x.EiO2S|ׯ<ٜ[:7ޒ7V|/-][pנ܈(s~y;V[11)?;QSUؓ>|޸f v;Ԑ M`;1DCy!$&ȱjQ~5tIrv,Ў} 4`A7 ՟fF2bepiS@Ma3R1nea [ɞ ty`>D5/<֐,[^/TUNsV{D#۽TsB)s,x6Nwg]/~S8&dY䰑'-m_x{o>xHrKdvh}Bnr""2e7{b=Ӌ}3wGRti˺ЛZmpP+tTq ʖM(~=(0KKftYizS,녓N>/0aa N >k-NV8di.q;c7nkVl 9mdxGܭ<%y;=I嶔'}]_Wwێux&e~{'Zr'я &㙸yFi85Ag*gc{U ۣ.ͤڭfyld-gT:`<+Vߞv/?Zۑz=ܹ׆o_ܽ2nn;=ϥ.v{Ӈ `W̪i5ZYoV߿gÓ{wWܘXtQ'F.(u(({hw"d S!@0Ўb+9E$8džA0/W4Ϝ΃iL b/W>MJ2ŅjՕ5ܧ:V*6khWHh7jMMẊwnnbh4 *?oI~֞=u&j-vnׂ~CW=>~YB|ZٲB؞p~鬵 Mm`DԔ2IVPbEX䜽jVb(#;'w7nv=i&jZhƓV<^rݽ5#_|snǥzTl/;5詮Wd]~ѻo/|ﷇ;{%'N2_)F۞}ow|l9*H.2 q.Uꯩ/F VJI"qQedp _X%+g^ͦ4{Xj|Lƺ^p=sV~IŨ[qOZSyq'!i[Sk%>|Aّ~YV2u5o5}]{|waOkFDKo g?=|띹קtSy`-t]咓ʜ%OҸ/+;zя_?A*}N9|Q_wWޮޛl\|Zv`^P=5ϛ)L)q^N\ ]?m [O٬eBݪTu+XuU:zj(aڴJêѠꂗݔFxjڄcwczc\_{Lzݳ73\slWS9ZO>ac"H%A I$JT/ד)R-UIa0JT:GL8'5<)ehR X2cNj7S3V|$n>%SoŚ>5rH|,H`a*NxykkGq9<~՝7V]xgcg{Q}q縫ojMy/?7}{o^ɱ{mfN<͹3U R'SF*_?md*E<?~?<6M NIL~CpsBO5Q>ev}s7mT,T: {]mU9Q6OKitzZ4{NEyE.!vnʃ: 'l&,&:.]99yXj*@l9Nuߑ@~k%;4o:7-+ZaW+ɕ)rv1s14qտ;l٢%Ң.Ԧ)r^kN}d#Kk~RTŁ>v-y w2y}!G૛~b>ݥ{+vL_~Zɒ'CoN=~>8i(c!l!gv\IsO~|ݚP8.y1zϻk>lw_;Z0Y=Ryr) of˽[v}TgB+  {ug #L) @Z}I]3)Fg_z5rYc}!jyeֆͶz\.'߭JZωx\ }\{=IbEkPb,6o?5b/=js-H~ouKqQ D#eXT(HH7hZp r܎ \ h4Q:_\QDҡ'wTuӒf;S{&r+z[g.:f8_Ks$"B>Q7'n=C]:>d bcb$cꄍW_tR7+w.9 w5 jDy šBŒZkɘC+3BgoVM7ߙ]H,im{2q |u yܲҕޗߕ:_Z{:a{u1 ?_l/MS(DF/JTt[BQh4lr*&A3FsC`RXniO2^;*e|ϺY+ۜm ӁJ#fbVuRWХhMX zpZ{[UDЁbAl(i$`Z+gf{Hk1A/N rPh/mj:><φ 꿑8~@ C-G$'</MzQ<>nm=X>̊WO6sLj_Dս+MV^=ͻX|]&wP+VFHB* %B| =.H;Ms1?Wb^_m Xuxs(Ao\{3e"0pEg+mt=: y6M/<c/q_1r׾d|ZQ_(DNRBTD֠l8v0IsKy!ya"/ۏ0iV8!HM _VoV":հm >Etޕ(r3֘C2`#PtIZ9a='Kaigl$Y wRaВY­fF-5&o~%ks·œ/8+w;+ yD]k{5Rs5\qOp ͂ooܹ,tR_ִw>^~VlhXm!}A:Cn*-b6kG욍ʍ%5] kﯴn]ڛ+;sh>x)x/ҧ jV||3Vr5n[f)뵧{Nzlן~?V57k ~MikƆ% ̕F/*qtxc}u )"0ƌ2V#>{l$^ (wd5#fH[n 32gW vj05JyĀà8{_osZJ`K&jA v z\jΐ!H+)n BI 2~8tAAx[U'|o]2WkVtHh3tŻw; ٬ܪY(:1vl9B/qTvYnSf+ |e?4g&Q MUj =&wtŝ3}ۭc+ewö3q=GV<-ifЄR' 'C'b)G H5;H8-o6tT]ͪ-$K_,/Uz3>zV͢n-n&rz lEV$ޜTe\6=4e<ՠ(Fxio w;j}{ G0hH~GNRu v"efE fFk  ;D-xYZpLG1YYih$֗oqFECf kv%\y~~w K 촫  zAgZHHIVlL>ep x8Ť7K{3CEÅ#w.|?O*_\~vtͮ⾏8`"nu)G 3^1#,y!$N˚x%04UBBfqqs=3 C$ KK2 Dj,5:Թī(űɒbbTe ZQ{U]V \ u|S^ >דpЁAy(ǃCCH8~y2Ғ'!2d;'8Li + 7K[gMP*̴kYg$BLZ%M:"XHR_65RY8RKbܾӾ빡5q&+>]ݏ= dDPŔ[c4ޫ?v-c!ɹWrJ\\ G*ܔ*+*2y&ņt=_hNoZ-7>ۗQTrvO.9^-zyVIjf8Dž֌>[M/ǖ Q@[-c@D[20d Ī+%ecUlL6E%`(0!\ q)XyQ_Gq\A՗>D1 0FDwr?i 8}5eBԁ@_{3Z=x&7!!!4gax> -1e5mn]jg`hÀ#y#b0'T%'H!x+B@ǻӱn"(>!~GA7/UfŮ4g֚2.u u[,_%bCH3sT\fR.%ٯx\Ivxb|:"+RCa JӠcEyx[R[/mlao μ|>\;fcVE#gˏ>h:7m'?aYȃߋD -2}R4ܟ3D)rs!Ph)1( ?&5NU; ax(qP4 WE,X? Q/9 &DeXT9mْ%nzИt8դwZΔfux( P4DɫXbyD fׂCOtm(3qu(,{:bיA;WA KDP @' x@t)LrVg.upr(u4h=9 a0RT3 Of}z5Jɚ0iAwa`..1Q8Uq,<:ySc.tGse=|ԓ7FSܥ}=ǍpzRz#3rj|)&YTmA)cMtl ӽ^}.ιgCE'/W&[.&̲I2b>Ī5BEZpxE8"LYXXÂx~ F)I ?@"b(4'-i,- "Ĉ6BH8+EIb"0@O(Dyy]p]9>s1!}ӡn K;KZl-8z@Q!8$ Axa(BBd03"v?7#xAtj#Fg`\)SBR0@ Ҳ#ڲ7á:T=N/ς[+`Bz`o&x%fIhˁr#.By>&VNK&CPh% Q?7$"X GAØp~祓bLCX(BP N&Bp`EQ hX bM8$I=`9{w1+>9,Gv1{tÿ{  @ P:·6Eh4 QU!\jEAc}X :q*J6||nD~,׀!(d ^N`#I,CKalR]A Z&PL @Zwp͘؈;"Z ~J' $|Ж]@xE,Db0T9wsN:Af]yjM κJz'[3 @fEGkN.i;{Vؙl $DIoc"@20u@^-)` Cay!"` I$ E$ah8Ax8ȡ픥 uTT64b\) 0*gNő}h"W^lFm!ӥCNCnX=1mr#(M ]@D$ѹB0A(2OBsحBB [ZBX@aW ,L!dc1DCqZ8" 8 =OW欁XxTEB}Bg-KmM]HeIJ4c+K JA4TcatBp$ F b,^XrvR>=H$SW([C@G7eda%3XH[PjS>dm9^ϵ>m0,?p@<] ȓ<8b(!D# X^~$EŢDC`HNp0K؇.q0˱1"T6Kq0*CX  ãd4#e$tFܬ]g.:6ԽNC$U0\8`Ђ(CP( 9'd8#Ep,|NC<'#)(59JfEIbd'$hGYD_ X&0+O;%fj沥*5f#W^\fRXc$x, $n~A4I"8Mc)x 'h )X-<+UIUS\m]f&>H P4@ s@POS{,|5}2i*7p+=xfTB) QkuX8ppQHA"AH8AP> `6'j}&3A.1,|%A4 EI8 NA!H$'Ybu~Kq+qKQ[iq>s.u!U*G+t.@P"A`p".D HXViF2Z@yL%Z KѤIUSeI d&(r[Xtaa8[p3;UƼu΂0e, r8,&p8qqO &G\]bJ\q6{M)W: Q8&PE!dW!F+OUh Uݢ>nV-#.`\15GP(D H, BC9悉ƈ"haFSus*V>ii40g541]S 4Q^}4AW21\&%IdIfDG:U(mBW͌aBX&OEcX,D(ŰT9:]BpPTaȆ屡) %9ڴFgeP9r$aFƆ)'kI J7ԗJ~OGًr[M1 )Rz릨P> /W [0 [777.832] 1 18 250 19 28 500 37 [666.9922] 42 [722.168 777.832 389.1602 0 0 610.8398 889.1602] 53 [666.9922 556.1523 0 0 666.9922] 68 69 500 70 [443.8477 500 443.8477 333.0078 0 556.1523 277.832 0 500 277.832 777.832 556.1523 500 0 0 389.1602 389.1602 277.832 556.1523 443.8477 0 500 443.8477 389.1602] 124 [500] 129 [556.1523] 178 [500]] >> endobj 41 0 obj <> stream x`TU?|}ozI&L/It2@ 0(`(T%(kö.1"@T(Q"k\ few>fl/ ipH oqe!a g6]H#݃>/HޖAd*w!'l]8}ۆ}pDR%Wۥe§ȿDynYE3| qFR#%AX8;ga4#a8_1ֈ~C=!r x͇29>d-{gXJaD'<}gc~qY }W`C:N^CyN@|@1m UY'3Byވ= u2L߻42If,A[禀eڱzt2¹{+,8a6bu1̀QXcSR>{&+H0;,~wXQ̯1vgHd} ߊa&1B-Uuxu&e9$9bqY졹]XDc7[X>eƈIR2KȯɪCyNx۪_m܅&B%}}N, 3dP iN,O 'Ÿɢ 좏SL *?>)1və?}Zo;߿6twgM%wnlmi?bnE=~c6~ŸP?DŽ|/Xϱr8]ȅŸo#ϋx<ӏy) WcygPK\_=q b|'ȋ7a۾qe|x>-x n\e>˕fv=8sA>Կ7"_O_|vpQӹ]߻e'槱Xvm۱X|:eq,&1b/˘Og1 1bWc1z}wN֜{c*Q&GP&5[#0x\Ÿ3qYu"c8Ɵ-~. k$#чo5uZGqitvW_SVˣyb)asȶDq3~oZW̟rIZ6_#QX?+Qn+z8?jHyoqy"ɲe25Bٍ6Xy-_92{3scoѣ+dHaV*6 1|?N%F௑r8W+UaRǺZ[1X?}qs0/~b5] d$<(˃c/!V  'N!HO碍 {,Bl1_Y~%Xn|;p[[wԭBꍘGb6tp<@8K/%ۂFᖟqWGc\'q1M)1@.HAnH.^enk7a/w0/ "!2"#Fb'N&^AHBRDJH ҇TZҟ "H2\Fx2\G'7-r/yQ19N:?i hQ'ͧEBw#%sRN)95 \8?䲹\.+檹ܵ bn9w'{{krq}ܿx={5G'KM5u}C 7s79r#_Q{S^=Bw. 9TL\&#j/k[ڻ{[ǽm^79P#ʷv~3 w?ʟO_; Sgm ͎W;;t88888[NpZ.g?簸;:7:_uns<;Z~%-]u} +/a~כwtǗ.B,B`}9%]=!F>{cƉuN<1u'v≎'9hGw:`G鉖: NQQѷwGeG򎲎bΎPG EG@ עck=zlݱ,->iQ؟?XXO|K>] /O)G8½4Z;\ xύyX<<󓶞st~>TdPE#x~_~Rܱ+vo/0o&=1!Z'i1琅VRR-d pw"mS8'2{\lE3|D.I8Rr{e,pE$C$\~ 'zyHx =HEHB$"̾ Au -ݨ @O|C71$8/|Ѓ“"!3EdٗV8!>|H#(|L>!ȧ89A:go$"'_N%|MEN3$o@!|GG$@~F9kYED@ %. T7OO%0A' #'|#ubpXr9i$xf_4+NnO#y9̌`jz.Þb&[̉ &Aj*B.JxȬLp&o~Y;+&W1ՅU5iuM.l.j$zW9geֽU^W;3wUy\byXOnUmQj%\խ53WO6UlG2+6;*XϭwM3,XV5[%^^_]es2[IIӪˈ.**]f ׋ۖٮI2SS&^^Ml`0d&yZn8a9w]3~/RĩkP4+N'w..no%c.zlSլf,W;c (sVqjlڵ|TZi6LJy1++Y,7Qdzgfm9v{/Tbǐ#5폳huMv8bvZ 'c34VE LZ _\X31^#Vd ^.fdQ?SQM8^bEaVfs;Gj`qbCiv3h$vI62Zve[+=O"7.7UO7g\*^óV"!w7aq;T7Ə 3pxRn,fjP5(rSj\3м_xaN}dLd_5@vjNa$(?m;GvX6dW>j'y~}m:'x>_X)߭mMnlKHtN%_svuBu{Um;fa4Y)myX;? l<%2uȇy\mMdtdȬm;j%luG>19DY1)^?Q><b;{IF羦yνM6瞴v9wj'<%VC?Q9|%t¹"/TZOaxЕΧͿy Q[=^EY"z9oq¹S{\⥹16gxUg۰޽Ii)ڜ1666;a!:gw}#Dؔuq2mbo sA}6OlANW+Zz+-Tˎw1=R.kY'ky@ֲ\rVR#k%k Ze-!KzV+rT)ZBp, R5gG^,i f{) mAN@Z6H7#݈t|f±ݛrz}x#!^y]/-Y,z=yM賄wJmkŌ fssPRS3 cƹ>d{kP NrV<%Q,xHֶEl"m۶m[Xۖ{j y i6ь5o"2ZW=YU6*6CUǚ1h)bͰTl6&lEVBl\Kl##ZJ]51,C!Mlf.>ȇ}PYiЗL*ŵ _s'UQ=zd d %έ+!qF1xm2»%wq} IF< xⅅ4~I>q"!9$7U/l/<)GA_$j&mP0y>H/=pZxAxp{$X`~ ߁հ;/p>DKd5ـ5bompN8>j&2Q"P;ɢ3ׅむ=Y#eز? 8f. n8=o%i 18R~>q_;I)&%&OQydMx8e QVЇiBx$b#!R@z1ArH3i/z9͹9:aiQŽGeC18pAw 72 Cm= Oqg|c<'P>Q7o7Q()89҆c@^Qr&gh:NG>H׹n w3ww==uo{1ɒ+$ $oHKG Z0L%'5b("8#dd,olsJ>DɃu8 .QKak1AԒd4?$Wkd!YL&YN^!;.+H쩸/nAZ@+t-}>Mt+Fߥh'~䜖 p8n 7[=ý梼^~ow_J#i@Z %}KOJO@6Mv>GNP_DD| t*|J:6e` o8 +((/ɔ4 ;}):NEkWCv> XYI-WB$ѓ`OyF$'+?% @)G:{T4 JA/Αo'|UMI>$?9#Q}8p\gΑelT22 Ҡ)]D]Kt.%D]Kt.%D_LMy@N:ȆR!D?^N+7kpR %:{ɘ9[$幎 je('.o3ˤ̩-k'W`}ph{qjAGܚIW^w@2D*d?9JYm֙SH:TOitzsα(ifn$a)y>1و 94 ε374F 5s>p M.7ױ5 g-zEе͹Otrw7V}n`nN^p@ɢXj[.Wwh4TR^Bs#>z4Q#afcJҎ[,YZ-ŞshIHo9XacDLvVwC]2Q9Ch؜.,\dn9lHI6 mו{QV+wnhaxuy:G<}ovrzӁsy[2tyjSmMr$5OSgҼigJQi$bq'i,]hX:Ci)vN>CvnJqflF֊5*2fELkEZ" ^,)\3ަ==ě>3zcOw~'_zxH`Wߦ<ЯPUȣC1XsE@i[.]<=eg>唗*D&jUzto=I6`^2Qh4^Γ&BfxtEVFF:5d=eAG/,ml}|vs S,/6i=J`'E.w2B6,Kh7<7JJ@,dKJPb}'ģL-/ׁE{8`T]5Zf1[O#tա2aМ_}ɅoΟXR^ Wd[Ϊ.Yu=<[c]*̎tM7\q]vYTv& Vܚ.)\#=ك<+>3]@{ȁ]|/Mfr., }Si .>o=ْcafy"ө絻Ng%#2u$K *n$s#r[_^}.0BIWStj8@42`[詺,V}F qWeyFюѐE'l0RyRI\"095$$5 ƘDɤו0`rsѿ]^CŌ2lw=ivbc:ptcrIu슨+=Cn,Gd7^}IJ%oS(FWX|d(7 d2-zk,PI_Xo]:S565cjYg[S:?y6R0$7=>hq%vz}l !C=nw111 `~XĦE|"r>сWhLx-0YvKX͓C}'1K*BsYN!g r}8'bH{L*WE6n֔}Ҕ_Fv\M'm5)3 Mخ[=-YO/זFG('gRI\ڜ4G|@í?s5?Rg Tx1x DYUgsTONw/ݯ_^K$fxJKJ(LDBTv;3 ɔCROrXl.z%LtT45pr• \YP7l57pk&pF+fbVT’ӎPg fqs%K8X3;6v,E og#,H3wsuy8b{1ID ^526AuQJ 'tGu31cJḬ[,Wm,K]h^c>W@E@)uBip娺p4#S fF ڵB%eB܍JDMf+>"ʏ #y$ⶻ]:qm&KI#ui6K)'K ZKd8EV۷eD^U6okWJND 2,$-"% 9mgQE>t~VEtg#cbӝbz'[O2D\Ԑ5ÄH pfĪ9D Ӑj@Te˾z3g>YTpV 47}->)[T`Mxy5ط'U2A"sd}W]QWҧ&'gLJ;Mdd ښ$ȉLɨ4؍) " k -͈2KrrȨP(mB.:M&0)lxn *IދnWhOc%%b8c,EG xI(9:0m ^.]y"gXjȲKj3=1jSҤ<tj 3a_0 rr/JPxe)V^_)sҾ#I?&IɩOԥ&i.^fiVuPY;52kyg!6Bm(2YlI5ʔBU׺*Mog+U"ť%=,]}9yOlelO$z1 ޼h~S=Ln,NYHMB(TI8ѹǗEI%eQX*:kzy׌ +^i$*s# [$K1?|~cn?BtG;:g.iڒUTY dļ>IրaedpPK))RIjjS+۩ڞbJv[HSlRh4@]27Uez}-RofXl ef L m=9gMF|K2.Cn xB ŝOOD=nHtWJ$z+ru EVoݱ+if"2 D*-"w2tT-J*r 3zqk*k$-G R4#;pv`Qe瞘'3vo N*~%Ɨ_I\s N!WM6drdʫRw\6[u*`:Uy'6/Qz.}w.ŹjvQCbgY8 p#H\ n'nǕۙEE.?{0kd{VK2H!3I.nQ[&7Aiʟ,d/=[=V_\@[7E~.]cO*\u+rrO<v *}}kOxq ѷ &]^.)  sffMWsI1uBt\nKiI I>P^h6Lnw(kz讼cQcHN!i9|_JkUkU$7`U.$&$+;B ,2ZZM-﯂BIVʠT%[ k i%YneUig$z@)>xЫ"'[\K{BMG}ӹ4~pRZXdEnO4wt̏G[1qIpQ<`ݭs{a}HD]Zsii~|Z2.Ac BY1ӊG3(2T#00tmh`6kfU]+%Nލx%gsS֖ݒj7Mu.A\zٹyZ;'ydy^S*7ەKx!Vrɝ Gx[ipL $M<(I,D`6.tąqXƃbhl$3)(&,K~X7B7ŝђز]LSIK$ yi͚G6-.&<֚*OS>+jݙ:l02T ^6#\qp䪤8t8P̋n܈fB@rVuCԢX|J.l%VkfV!Ͱ9nj~ y.+j0M㚹%j bNqSTbL8b=hfA̅ƹ,aTB^^~` G/kLq̒-Ŷn']3 7$jgۗ DySR[ jHHťe£+Owջ=RoT:r=uBԆ. M- Iu;'&%sM7<]Ʋ~dUe0``F]ԋX jY([!9U٘`yæ=]+B:%g[5(4tKhw=rI2A\_^)*ٶ"EXl] REՒ EOkw+rzb0 ˻7i5)]@%p=MCFi:1Ii/Hqg *|Z!eTI+-GkY)Ґ٭r],yzNmP~#eSni}}apcyuI94/9IN39wȝ(lM)g߭PaciVY>kb~!}"s?skdx^ ZMgܓr\tL.3͋եK."B 2[(El`7* jcq}u+]TX [u+H…^ǷY  ]K8/ob>l]9\KdRI}}׍me;fYe+)wzei4V>[/L04,OEs+5$m;T+d:Tg>@Tzy3?xYØDiH"Ab4KeCCʌ^BG|,(-U/?U+T6Z?i IbjGS#!$X1w⚂f:{quKۆRqLI%) K*3t&Jzs>=0nj~ae,REOV8'rHBe%TŪb]afPAϫTۓ7[Bd}Pg &p ^ 6o!/X#f}C|TR%F s1w${Xj]#!n7<qܚОC7l`弚hW|a8hP"5usrBojE1v8hXCº z<(hLʬɜ4/I^[`-O/}ls悭%ۓV%{K*3E2YY=\d򘗠XV[#:Qx;|]Fǡp+kdZ*7bc*ãWP]CGIqPIyFx&y=<=g< ڽ)fֱ֝es`,7ʍ̄\>pN= |yZL;%XۚIަjT]-Ws@1nLMN{6ۗHlҺ#\f]rsiW<3&@[d6;̾5ٲu+jGi+k0%֤fX~$ۙ[=n(bM30.ĽkHt\P L.Yl2eA#&ġh_ѢxLqL)5د5?*iZbo*O|Ɋ2&qY5Y X4ި`2N3"}7{urnpռ1}rIWfjo^_oR^ad`MOO eGFJnL}9(Y4bs.cqqXSe}Uke(NƚׂQ@qJylб*R&56nyo?]%˱I'.o15aCCoy )(U֓Fļg%K"Dy+ uM#LL ^lRz%c ;jY}$ȎLc9RtC7SiUL<9 \foxQ`[T[?hv"/8ZȂ"Lދ7 `id\"MiIT4Quy^*L2fZYA m JJOiv> L$;\s&AzQ  9Ϥ}wu}z|QQZ5N½T|{!E!C )*P_7^e {be)(Pd,g ?Y \:@*sWDl;'h({F*)cI 4ڱ~F VA. &EI8+4[MX/ȄcR,1FS.䒃驙ηd9DaXr,J¼eP9*/yxh C7 >1I["Q1XCT_b$HĜS(Jfwi -z§k%+tvWxW*1x>Z䍮暠z^啼WW,~ݷ 1\OwQrY~6Yw{7]^mat|xtkMF<, endstream endobj 40 0 obj <> endobj 39 0 obj <> stream x]Mn0tMHR$Qi"c53@"=?3Vvde #kZ, J`5[qT+Dž+x30BW1í<*WV:.@zdLA:ThʭwȜ^`* w=)KrhoLߕUR(C1ReDg$!gH'̉bND=‘x[bq%tⲦíO$k KBTBYC1&yxE1O3"{t#1^NE|Mʇnxp$[ u3] ϳ endstream endobj xref 0 67 0000000000 65535 f 0000242138 00000 n 0000241900 00000 n 0000238283 00000 n 0000462181 00000 n 0000242202 00000 n 0000238523 00000 n 0000254820 00000 n 0000238763 00000 n 0000270287 00000 n 0000239003 00000 n 0000286101 00000 n 0000239245 00000 n 0000304971 00000 n 0000239487 00000 n 0000462323 00000 n 0000320776 00000 n 0000239744 00000 n 0000528800 00000 n 0000335841 00000 n 0000240001 00000 n 0000242017 00000 n 0000349970 00000 n 0000240234 00000 n 0000588938 00000 n 0000364969 00000 n 0000240492 00000 n 0000380577 00000 n 0000240735 00000 n 0000395248 00000 n 0000240968 00000 n 0000401442 00000 n 0000241201 00000 n 0000411819 00000 n 0000241434 00000 n 0000430570 00000 n 0000241667 00000 n 0000450406 00000 n 0000653689 00000 n 0000673477 00000 n 0000673248 00000 n 0000654237 00000 n 0000000015 00000 n 0000000064 00000 n 0000010601 00000 n 0000010696 00000 n 0000133329 00000 n 0000133466 00000 n 0000133604 00000 n 0000133743 00000 n 0000000321 00000 n 0000133882 00000 n 0000156755 00000 n 0000157175 00000 n 0000186907 00000 n 0000187348 00000 n 0000211149 00000 n 0000211546 00000 n 0000237859 00000 n 0000156535 00000 n 0000134516 00000 n 0000186687 00000 n 0000158103 00000 n 0000210916 00000 n 0000187884 00000 n 0000237634 00000 n 0000212334 00000 n trailer <> startxref 673892 %%EOFPK!g2<abilian/sbe/apps/documents/tests/data/dummy_files/random.binQ$tH헀_V#<<$B4"P wyeG0}4 -ew+_V[Oj)R)uOr~x%hO%ԸZgGnMAbS&O8M @Zى VKP8z,CS^z驭*0=1( PK!8ԮBabilian/sbe/apps/documents/tests/data/dummy_files/wikipedia-fr.txtWikipédia est un projet d’encyclopédie collective établie sur Internet, universelle, multilingue et fonctionnant sur le principe du wiki. Wikipédia a pour objectif d’offrir un contenu librement réutilisable, objectif et vérifiable, que chacun peut modifier et améliorer. Le cadre du projet est défini par des principes fondateurs. Son contenu est sous licence Creative Commons by-sa et peut être copié et réutilisé sous la même licence — même à des fins commerciales — sous réserve d'en respecter les conditions. Actuellement, Wikipédia en français compte plus de deux mille articles distingués comme « articles de qualité » ou comme « bons articles ».PK!<<3abilian/sbe/apps/documents/tests/data/propfind1.xml PK! /NVV3abilian/sbe/apps/documents/tests/data/propfind2.xml PK!d[[3abilian/sbe/apps/documents/tests/data/propfind3.xml PK!vm.a2abilian/sbe/apps/documents/tests/test_documents.py# coding=utf-8 """""" from __future__ import absolute_import, division, print_function, \ unicode_literals import sys from pathlib import Path import pytest from abilian.core.models.subjects import User from abilian.testing.util import login from abilian.sbe.apps.documents.views.folders import explore_archive from ..models import Document, Folder def open_file(filename): path = Path(__file__).parent / "data" / "dummy_files" / filename return path.open("rb") def test_document(app, session, req_ctx): root = Folder(title="root") doc = Document(parent=root, title="test") data = open_file("onepage.pdf").read() doc.set_content(data, "application/pdf") session.add(doc) session.commit() # coverage app.config["ANTIVIRUS_CHECK_REQUIRED"] = True doc.ensure_antivirus_scheduled() def test_antivirus_properties(app, session, req_ctx): root = Folder(title="root") doc = Document(parent=root, title="test") doc.set_content(b"content", "text/plain") appcfg = app.config meta = doc.content_blob.meta # 1: not check required ################ # no data in meta appcfg["ANTIVIRUS_CHECK_REQUIRED"] = False assert doc.antivirus_scanned is False assert doc.antivirus_status is None assert doc.antivirus_required is False assert doc.antivirus_ok is True # antivirus was run, but no result meta["antivirus"] = None assert doc.antivirus_scanned is True assert doc.antivirus_status is None assert doc.antivirus_required is False assert doc.antivirus_ok is True # virus detected meta["antivirus"] = False assert doc.antivirus_scanned is True assert doc.antivirus_status is False assert doc.antivirus_required is False assert doc.antivirus_ok is False # virus free meta["antivirus"] = True assert doc.antivirus_scanned is True assert doc.antivirus_status is True assert doc.antivirus_required is False assert doc.antivirus_ok is True # 2: check required ################## # no data in meta del meta["antivirus"] appcfg["ANTIVIRUS_CHECK_REQUIRED"] = True assert doc.antivirus_scanned is False assert doc.antivirus_status is None assert doc.antivirus_required is True assert doc.antivirus_ok is False # antivirus was run, but no result meta["antivirus"] = None assert doc.antivirus_scanned is True assert doc.antivirus_status is None assert doc.antivirus_required is True assert doc.antivirus_ok is False # virus detected meta["antivirus"] = False assert doc.antivirus_scanned is True assert doc.antivirus_status is False assert doc.antivirus_required is False assert doc.antivirus_ok is False # virus free meta["antivirus"] = True assert doc.antivirus_scanned is True assert doc.antivirus_status is True assert doc.antivirus_required is False assert doc.antivirus_ok is True def test_folder_indexed(app, session, community1, community2, req_ctx): index_service = app.services["indexing"] index_service.start() security_service = app.services["security"] security_service.start() folder = Folder(title="Folder 1", parent=community1.folder) session.add(folder) folder_other = Folder(title="Folder 2: other", parent=community2.folder) session.add(folder_other) user_no_community = User(email="no_community@example.com", can_login=True) session.add(user_no_community) session.commit() svc = index_service obj_types = (Folder.entity_type,) with login(user_no_community): res = svc.search("folder", object_types=obj_types) assert len(res) == 0 with login(community1.test_user): res = svc.search("folder", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == folder.object_key assert hit["community_slug"] == community1.slug with login(community2.test_user): res = svc.search("folder", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == folder_other.object_key assert hit["community_slug"] == community2.slug @pytest.mark.skipif(sys.version_info >= (3, 0), reason="Doesn't work yet on Py3k") def test_explore_archive(): fd = open_file("content.zip") result = [("/".join(path), f) for path, f in explore_archive(fd)] assert result == [("", fd)] fd = open_file("content.zip") archive_content = explore_archive(fd, uncompress=True) result = {"/".join(path) + "/" + f.filename for path, f in archive_content} assert result == { "existing-doc/file.txt", "existing-doc/subfolder_in_renamed/doc.txt", "folder 1/doc.txt", "folder 1/dos cp437: é.txt", "folder 1/osx: utf-8: é.txt", } PK!/~<++6abilian/sbe/apps/documents/tests/test_documents_web.py# coding=utf-8 """""" from __future__ import absolute_import, division, print_function, \ unicode_literals from io import BytesIO from pathlib import Path from zipfile import ZipFile import flask_mail import pytest from abilian.testing.util import client_login, path_from_url from abilian.web.util import url_for from flask import g, get_flashed_messages from pytest import fixture from toolz import first from werkzeug.datastructures import FileStorage from abilian.sbe.apps.communities.models import WRITER from abilian.sbe.apps.communities.presenters import CommunityPresenter from ..models import Folder from ..views import util as view_util def open_file(filename): path = Path(__file__).parent / "data" / "dummy_files" / filename return path.open("rb") def uid_from_url(url): return int(url.split("/")[-1]) @fixture def community(community1, db): community = community1 user = community.test_user root_folder = Folder(title="root") db.session.add(root_folder) db.session.flush() community.type = "participative" community.set_membership(user, WRITER) db.session.commit() return community def test_util_create(app, client, db, community, req_ctx): folder = community.folder user = community.test_user with client_login(client, user): g.community = CommunityPresenter(community) name = "document" fs = FileStorage(BytesIO(b"content"), filename=name, content_type="text/plain") doc = view_util.create_document(folder, fs) db.session.flush() assert doc.parent == folder assert doc.name == name # test upload with same name: should be renamed fs = FileStorage(BytesIO(b"content"), filename=name, content_type="text/plain") doc2 = view_util.create_document(folder, fs) db.session.flush() assert doc2.parent == folder assert len(folder.children) == 2 assert doc2.name == name + "-1" messages = get_flashed_messages() assert len(messages) == 1 def test_home(app, client, db, community, req_ctx): folder = community.folder user = community.test_user with client_login(client, user): response = client.get(url_for("documents.index", community_id=community.slug)) assert response.status_code == 302 path = path_from_url(response.location) expected = "/communities/{}/docs/folder/{}".format(community.slug, folder.id) assert path == expected def _test_upload( community, client, title, content_type, test_preview=True, assert_preview_available=True, ): data = {"file": (open_file(title), title, content_type), "action": "upload"} folder = community.folder url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) response = client.post(url, data=data) assert response.status_code == 302 doc = folder.children[0] assert doc.title == title url = url_for("documents.document_view", community_id=community.slug, doc_id=doc.id) response = client.get(url) assert response.status_code == 200 url = url_for( "documents.document_download", community_id=community.slug, doc_id=doc.id ) response = client.get(url) assert response.status_code == 200 assert response.headers["Content-Type"] == content_type content = open_file(title).read() assert response.data == content if test_preview: url = url_for( "documents.document_preview_image", community_id=community.slug, doc_id=doc.id, size=500, ) response = client.get(url) if assert_preview_available: assert response.status_code == 200 assert response.headers["Content-Type"] == "image/jpeg" else: # redirect to 'missing image' assert response.status_code == 302 assert response.headers["Cache-Control"] == "no-cache" url = url_for( "documents.document_delete", community_id=community.slug, doc_id=doc.id ) response = client.post(url) assert response.status_code == 302 url = url_for("documents.document_view", community_id=community.slug, doc_id=doc.id) response = client.get(url) assert response.status_code == 404 def test_text_upload(client, community, req_ctx): name = "wikipedia-fr.txt" user = community.test_user with client_login(client, user): _test_upload(community, client, name, "text/plain", test_preview=False) def test_pdf_upload(client, community, req_ctx): name = "onepage.pdf" user = community.test_user with client_login(client, user): _test_upload(community, client, name, "application/pdf") def test_image_upload(client, community, req_ctx): name = "picture.jpg" user = community.test_user with client_login(client, user): _test_upload(community, client, name, "image/jpeg") @pytest.mark.skip() # FIXME: magic detection mismatch? def test_binary_upload(client, community, req_ctx): name = "random.bin" user = community.test_user with client_login(client, user): _test_upload( community, client, name, "application/octet-stream", assert_preview_available=False, ) def test_zip_upload_uncompress(community, db, client, req_ctx): subfolder = Folder(title="folder 1", parent=community.folder) db.session.add(subfolder) db.session.flush() folder = community.folder files = [] files.append((BytesIO(b"A document"), "existing-doc", "text/plain")) files.append((open_file("content.zip"), "content.zip", "application/zip")) data = {"file": files, "action": "upload", "uncompress_files": True} url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) user = community.test_user with client_login(client, user): response = client.post(url, data=data) assert response.status_code == 302 expected = {"existing-doc", "folder 1", "existing-doc-1"} assert expected == {f.title for f in folder.children} expected = {"folder 1", "existing-doc-1"} assert expected == {f.title for f in folder.subfolders} def test_zip(community, client, req_ctx): user = community.test_user with client_login(client, user): title = "onepage.pdf" content_type = "application/pdf" data = {"file": (open_file(title), title, content_type), "action": "upload"} folder = community.folder url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) response = client.post(url, data=data) assert response.status_code == 302 doc = folder.children[0] data = {"action": "download", "object-selected": ["cmis:document:%d" % doc.id]} url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) response = client.post(url, data=data) assert response.status_code == 200 assert response.content_type == "application/zip" zipfile = ZipFile(BytesIO(response.data)) assert [zipfile.namelist()[0]] == [title] def test_recursive_zip(community, client, req_ctx): user = community.test_user with client_login(client, user): data = {"action": "new", "title": "my folder"} folder = community.folder url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) response = client.post(url, data=data) assert response.status_code == 302 my_folder = folder.children[0] title = "onepage.pdf" content_type = "application/pdf" data = {"file": (open_file(title), title, content_type), "action": "upload"} url = url_for( "documents.folder_post", community_id=community.slug, folder_id=my_folder.id ) response = client.post(url, data=data) assert response.status_code == 302 data = { "action": "download", "object-selected": ["cmis:folder:%d" % my_folder.id], } url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id ) response = client.post(url, data=data) assert response.status_code == 200 assert response.content_type == "application/zip" zipfile = ZipFile(BytesIO(response.data)) assert zipfile.namelist() == ["my folder/" + title] def test_document_send_by_mail(app, community, client, req_ctx): mail = app.extensions["mail"] folder = community.folder user = community.test_user with client_login(client, user): # upload files for filename in ("ascii title.txt", "utf-8 est arrivé!.txt"): content_type = "text/plain" data = { "file": (BytesIO(b"file content"), filename, content_type), "action": "upload", } url = url_for( "documents.folder_post", community_id=community.slug, folder_id=folder.id, ) client.post(url, data=data) ascii_doc = folder.children[0] unicode_doc = folder.children[1] def get_send_url(doc_id): return url_for( "documents.document_send", community_id=community.slug, doc_id=doc_id ) # mail ascii filename with mail.record_messages() as outbox: url = get_send_url(ascii_doc.id) response = client.post( url, data={"recipient": "dest@example.com", "message": "Voilà un fichier"}, ) assert response.status_code == 302 assert len(outbox) == 1 msg = outbox[0] assert msg.subject == "[Abilian Test] Unknown sent you a file" assert msg.recipients == ["dest@example.com"] assert len(msg.attachments) == 1 attachment = first(msg.attachments) assert isinstance(attachment, flask_mail.Attachment) assert attachment.filename == "ascii title.txt" # mail unicode filename with mail.record_messages() as outbox: url = get_send_url(unicode_doc.id) response = client.post( url, data={"recipient": "dest@example.com", "message": "Voilà un fichier"}, ) assert response.status_code == 302 assert len(outbox) == 1 msg = outbox[0] assert isinstance(msg, flask_mail.Message) assert msg.subject == "[Abilian Test] Unknown sent you a file" assert msg.recipients == ["dest@example.com"] assert len(msg.attachments) == 1 attachment = first(msg.attachments) assert isinstance(attachment, flask_mail.Attachment) assert attachment.filename == "utf-8 est arrivé!.txt" PK!Z| 1abilian/sbe/apps/documents/tests/test_entities.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from pytest import fixture from abilian.sbe.app import create_app from abilian.sbe.apps.documents.models import Document, Folder, icon_for @fixture def app(config): return create_app(config=config) def check_editable(object): if hasattr(object, "__editable__"): for k in object.__editable__: assert hasattr(object, k) def test_title_prevails(): f = Folder(name="name", title="title") assert f.title == "title" assert f.name == "title" f = Folder(name="name", title=None) assert f.title == "name" assert f.name == "name" f = Folder(name="name") assert f.title == "name" assert f.name == "name" def test_folder_editables(): root = Folder(title="/") check_editable(root) def test_folder_can_create_documents(): root = Folder(title="/") document = root.create_document("doc") assert len(root.children) == 1 assert document.title == "doc" assert document.path == "/doc" assert document in root.documents assert document in root.children assert document.parent == root def test_folder_can_create_subfolders(): root = Folder(title="/") subfolder = root.create_subfolder("folder") assert len(root.children) == 1 assert subfolder.title == "folder" assert subfolder.path == "/folder" assert subfolder in root.subfolders assert subfolder in root.children assert subfolder.parent == root def test_nested_subobjects(): root = Folder(title="/") subfolder = root.create_subfolder("folder1") subsubfolder = subfolder.create_subfolder("folder2") document = subfolder.create_document("doc") assert len(root.children) == 1 assert len(subfolder.children) == 2 assert subsubfolder.title == "folder2" assert subsubfolder.path == "/folder1/folder2" assert document.title == "doc" assert document.path == "/folder1/doc" assert root.get_object_by_path("/folder1") == subfolder assert root.get_object_by_path("/folder1/folder2") == subsubfolder assert root.get_object_by_path("/folder1/doc") == document def test_folder_is_clonable(): root = Folder(title="/") clone = root.clone() assert clone.title == root.title assert clone.path == root.path def test_document_editables(): doc = Document() check_editable(doc) def test_content_length(session): doc = Document(title="toto") doc.set_content(b"tototiti", "application/binary") assert doc.content_length == len("tototiti") def test_document_is_clonable(session): root = Folder(title="/") doc = root.create_document(title="toto") doc.content = b"tototiti" clone = doc.clone() assert clone.title == doc.title assert clone.content == doc.content def test_document_has_an_icon(app_context): root = Folder(title="/") doc = root.create_document(title="toto") doc.content_type = "image/jpeg" filename = doc.icon.split("/")[-1] assert filename in ("jpg.png", "jpeg.png"), doc.icon def test_icon_from_mime_type(app_context): icon = icon_for("text/html") filename = icon.split("/")[-1] assert filename in ("html.png", "htm.png"), icon PK!k-abilian/sbe/apps/documents/tests/test_lock.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime, timedelta import mock from abilian.core.models.subjects import User from flask_login import login_user from pytz import UTC from abilian.sbe.apps.documents import lock from abilian.sbe.apps.documents.lock import Lock def test_lock(): date = datetime(2015, 10, 22, 14, 58, 42, tzinfo=UTC) l = Lock(user_id=3, user="Joe Smith", date=date) d = l.as_dict() assert d == {"user_id": 3, "user": "Joe Smith", "date": "2015-10-22T14:58:42+00:00"} l = Lock.from_dict(d) assert l.user_id == 3 assert l.user == "Joe Smith" assert l.date == date def test_lock2(app, session, req_ctx): user = User( email="test@example.com", first_name="Joe", last_name="Smith", can_login=True ) other = User(email="other@exemple.com") session.add(user) session.add(other) session.commit() # set 30s lifetime app.config["SBE_LOCK_LIFETIME"] = 30 dt_patcher = mock.patch.object(lock, "utcnow", mock.Mock(wraps=lock.utcnow)) with dt_patcher as mocked: created_at = datetime(2015, 10, 22, 14, 58, 42, tzinfo=UTC) mocked.return_value = created_at login_user(user) l = Lock.new() assert l.user_id == user.id assert l.user == "Joe Smith" assert l.date == created_at assert l.is_owner() assert l.is_owner(user) assert not l.is_owner(other) assert l.lifetime == 30 mocked.return_value = created_at + timedelta(seconds=40) assert l.expired mocked.return_value = created_at + timedelta(seconds=20) assert not l.expired login_user(other) assert not l.is_owner() PK! .|/abilian/sbe/apps/documents/tests/test_parser.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from ..cmis.parser import Entry XML_ENTRY = b"""\ Toto Titi cmis:folder Toto Titi """ XML_ENTRY_WITH_CONTENT = b"""\ text/plain VGVzdCBjb250ZW50IHN0cmluZw== testDocument cmis:document testDocument """ def test_parse_folder_entry(): e = Entry(XML_ENTRY) assert e.name == "Toto Titi" assert e.type == "cmis:folder" def test_parse_document_entry(): e = Entry(XML_ENTRY_WITH_CONTENT) assert e.name == "testDocument" assert e.type == "cmis:document" assert e.content == b"Test content string" PK! Repository repository = Repository() repository.init_app(app) return repository @fixture def root(session): # type: (Any) -> Folder root = Folder(title="") session.add(root) session.flush() return root def test_create_doc(root, session): doc = root.create_document("doc") session.flush() assert doc.path == "/doc" assert len(root.children) == 1 assert not doc.is_root_folder def test_create_folder(root, session): folder = root.create_subfolder("folder") session.flush() assert folder.title == "folder" assert folder.name == "folder" assert folder.path == "/folder" assert len(root.children) == 1 assert len(folder.children) == 0 assert not folder.is_root_folder def test_move(root, repository): doc = root.create_document("doc") folder = root.create_subfolder("folder") repository.move_object(doc, folder, "newdoc") assert doc in folder.children assert doc not in root.children assert len(root.children) == 1 assert len(folder.children) == 1 assert doc.title == "newdoc" assert doc.path == "/folder/newdoc" def test_copy(root, repository): doc = root.create_document("doc") folder = root.create_subfolder("folder") doc_copy = repository.copy_object(doc, folder, "copydoc") assert len(root.children) == 2 assert len(folder.children) == 1 assert doc_copy.title == "copydoc" assert doc_copy.name == "copydoc" assert doc_copy.path == "/folder/copydoc" assert doc_copy in folder.children def test_copy_nested_folders(root, repository): folder1 = root.create_subfolder("folder1") folder2 = root.create_subfolder("folder2") subfolder = folder1.create_subfolder("subfolder") folder1_copy = repository.copy_object(folder1, folder2) assert len(root.children) == 2 assert folder1 in root.children assert folder2 in root.children assert len(folder1.children) == 1 assert subfolder in folder1.children assert len(folder2.children) == 1 assert folder1_copy in folder2.children def test_move_nested_folders(root, repository): folder1 = root.create_subfolder("folder1") folder2 = root.create_subfolder("folder2") subfolder = folder1.create_subfolder("subfolder") # noqa repository.move_object(folder1, folder2) assert len(root.children) == 1 assert len(folder1.children) == 1 def test_rename(root, repository): folder = root.create_subfolder("folder") doc = folder.create_document("doc") repository.rename_object(doc, "doc1") assert doc.title == "doc1" assert doc.name == "doc1" assert doc.path == "/folder/doc1" repository.rename_object(folder, "folder1") assert folder.title == "folder1" assert folder.name == "folder1" assert folder.path == "/folder1" assert doc.path == "/folder1/doc1" def test_delete(root, repository, session): doc = root.create_document("doc") folder = root.create_subfolder("folder") session.flush() assert doc.parent == root assert folder.parent == root assert doc in root.children assert folder in root.children assert not doc.is_root_folder assert not folder.is_root_folder doc_id = doc.id folder_id = folder.id repository.delete_object(doc) repository.delete_object(folder) assert len(root.children) == 0 session.flush() assert Folder.query.get(folder_id) is None assert Document.query.get(doc_id) is None # test delete tree folder = root.create_subfolder("folder") sub = folder.create_subfolder("subfolder") # noqa doc = folder.create_document("doc") # noqa session.flush() repository.delete_object(folder) assert len(root.children) == 0 assert Folder.query.all() == [root] assert Document.query.all() == [] def test_no_duplicate_name(root, session): root.create_subfolder("folder_1") root.create_subfolder("folder_1") with pytest.raises(IntegrityError): session.flush() PK!Ea3abilian/sbe/apps/documents/tests/test_webdav_xml.py# coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals from pathlib import Path from lxml import etree from ..webdav.constants import DAV_PROPS from ..webdav.xml import MultiStatus, Propfind def test_propfind_sample1(): xml = (Path(__file__).parent / "data" / "propfind1.xml").open("rb").read() propfind = Propfind(xml) assert propfind.mode == "prop" def test_propfind_sample2(): xml = (Path(__file__).parent / "data" / "propfind2.xml").open("rb").read() propfind = Propfind(xml) assert propfind.mode == "prop" def test_propfind_sample3(): xml = (Path(__file__).parent / "data" / "propfind3.xml").open("rb").read() propfind = Propfind(xml) assert propfind.mode == "allprop" def test_empty_multistatus(): m = MultiStatus() result = m.to_string() # Check XML is weel-formed etree.fromstring(result) def test_multistatus(): class Obj(object): pass obj = Obj() obj.name = "some name" obj.is_folder = True m = MultiStatus() m.add_response_for("http://example.com/", obj, DAV_PROPS) result = m.to_string() # Check XML is weel-formed etree.fromstring(result) PK! ,abilian/sbe/apps/documents/views/__init__.py# coding=utf-8 from __future__ import absolute_import from . import documents, folders from .views import blueprint __all__ = ("blueprint", "documents", "folders") PK!{r++-abilian/sbe/apps/documents/views/documents.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime import sqlalchemy as sa import sqlalchemy.orm from abilian.core.extensions import db, mail from abilian.core.signals import activity from abilian.core.util import unwrap from abilian.i18n import _, render_template_i18n from abilian.services import audit_service from abilian.services.conversion import converter from abilian.services.image import FIT, resize from abilian.services.viewtracker import viewtracker from abilian.web import csrf, url_for from abilian.web.action import actions from abilian.web.frontend import add_to_recent_items from abilian.web.views import default_view from flask import current_app, flash, g, make_response, redirect, \ render_template, request from flask_login import current_user from flask_mail import Message from six.moves.urllib.parse import quote from werkzeug.exceptions import BadRequest, NotFound from abilian.sbe.apps.communities.common import object_viewers from abilian.sbe.apps.communities.views import default_view_kw from ..models import Document from ..repository import repository from ..tasks import convert_document_content, preview_document from .util import breadcrumbs_for, check_manage_access, check_read_access, \ check_write_access, edit_object, get_document, get_folder, match from .views import blueprint route = blueprint.route MAX_PREVIEW_SIZE = 1000 __all__ = () @default_view(blueprint, Document, id_attr="doc_id", kw_func=default_view_kw) @route("/doc/") def document_view(doc_id): doc = get_document(doc_id) check_read_access(doc) doc.ensure_antivirus_scheduled() db.session.commit() bc = breadcrumbs_for(doc) actions.context["object"] = doc if doc.content_type.startswith("image/"): add_to_recent_items(doc, "image") else: add_to_recent_items(doc, "document") has_preview = doc.has_preview() audit_entries = audit_service.entries_for(doc) viewtracker.record_hit(entity=doc, user=current_user) ctx = { "doc": doc, "audit_entries": audit_entries, "breadcrumbs": bc, "folder": doc.parent, "has_preview": has_preview, "viewers": object_viewers(doc), } return render_template("documents/document.html", **ctx) # # Actions on documents # @route("/doc//", methods=["POST"]) @route("/doc///", methods=["POST"]) @csrf.protect # TODO: URL doesn't seem right def document_edit(doc_id, folder_id=None): doc = get_document(doc_id) if folder_id: folder = get_folder(folder_id) else: folder = None check_write_access(doc) changed = edit_object(doc) if changed: db.session.commit() flash(_("Document properties successfully edited."), "success") else: flash(_("You didn't change any property."), "success") if folder: return redirect(url_for(folder)) else: return redirect(url_for(doc)) @route("/doc//viewers", methods=["GET"]) def document_viewers(doc_id): doc = get_document(doc_id) check_read_access(doc) doc.ensure_antivirus_scheduled() # db.session.commit() bc = breadcrumbs_for(doc) actions.context["object"] = doc """if doc.content_type.startswith("image/"): add_to_recent_items(doc, "image") else: add_to_recent_items(doc, "document")""" has_preview = doc.has_preview() audit_entries = audit_service.entries_for(doc) ctx = { "doc": doc, "audit_entries": audit_entries, "breadcrumbs": bc, "folder": doc.parent, "has_preview": has_preview, "viewers": object_viewers(doc), } return render_template("documents/document_viewers.html", **ctx) @route("/doc//delete", methods=["POST"]) @csrf.protect def document_delete(doc_id): doc = get_document(doc_id) check_write_access(doc) parent_folder = doc.parent repository.delete_object(doc) db.session.commit() flash(_("File successfully deleted."), "success") return redirect(url_for(parent_folder)) @route("/doc//upload", methods=["POST"]) @csrf.protect def document_upload(doc_id): doc = get_document(doc_id) check_write_access(doc) fd = request.files["file"] doc.set_content(fd.read(), fd.content_type) del doc.lock self = unwrap(current_app) activity.send(self, actor=current_user, verb="update", object=doc) db.session.commit() flash(_("New version successfully uploaded"), "success") return redirect(url_for(doc)) @route("/doc//download") def document_download(doc_id, attach=None): """Download the file content.""" doc = get_document(doc_id) response = make_response(doc.content) response.headers["content-length"] = doc.content_length response.headers["content-type"] = doc.content_type if attach is None: attach = request.args.get("attach") if attach or not match( doc.content_type, ("text/plain", "application/pdf", "image/*") ): # Note: we omit text/html for security reasons. quoted_filename = quote(doc.title.encode("utf8")) response.headers["content-disposition"] = 'attachment;filename="{}"'.format( quoted_filename ) return response @route("/doc//checkin_checkout", methods=["POST"]) def checkin_checkout(doc_id): doc = get_document(doc_id) action = request.form.get("action") if action not in ("checkout", "lock", "unlock"): raise BadRequest("Unknown action: %r" % action) session = sa.orm.object_session(doc) if action in ("lock", "checkout"): doc.lock = current_user d = doc.updated_at # prevent change of last modification date doc.updated_at = datetime.utcnow() session.flush() doc.updated_at = d session.commit() if action == "lock": return redirect(url_for(doc)) elif action == "checkout": return document_download(doc_id, attach=True) if action == "unlock": del doc.lock d = doc.updated_at # prevent change of last modification date doc.updated_at = datetime.utcnow() session.flush() doc.updated_at = d session.commit() return redirect(url_for(doc)) def preview_missing_image(): response = redirect( url_for("abilian_sbe_static", filename="images/preview_missing.png") ) response.headers["Cache-Control"] = "no-cache" return response @route("/doc//preview_image") def document_preview_image(doc_id): """Returns a preview (image) for the file given by its id.""" doc = get_document(doc_id) if not doc.antivirus_ok: return preview_missing_image() size = int(request.args.get("size", 0)) # Just in case if size > MAX_PREVIEW_SIZE: size = MAX_PREVIEW_SIZE # compute image if size != standard document size get_image = converter.get_image if size == doc.preview_size else converter.to_image content_type = "image/jpeg" if doc.content_type.startswith("image/svg"): image = doc.content content_type = doc.content_type elif doc.content_type.startswith("image/"): image = doc.content if size: image = resize(image, size, size, mode=FIT) else: page = int(request.args.get("page", 0)) try: image = get_image(doc.digest, doc.content, doc.content_type, page, size) except BaseException: # TODO: use generic "conversion failed" image image = "" if not image: return preview_missing_image() response = make_response(image) response.headers["content-type"] = content_type return response @route("/doc//refresh_preview") def refresh_preview(doc_id): """Force to compute a new preview.""" doc = get_document(doc_id) if not doc: raise NotFound() ct = doc.find_content_type(doc.content_type) if ct != doc.content_type: doc.content_type = ct db.session.commit() check_manage_access(doc) convert_document_content.apply([doc_id]) preview_document.apply([doc_id]) return redirect(url_for(doc)) @route("/doc//send", methods=["POST"]) @csrf.protect def document_send(doc_id): doc = get_document(doc_id) recipient = request.form.get("recipient") user_msg = request.form.get("message") site_name = "[{}] ".format(current_app.config["SITE_NAME"]) sender_name = current_user.name subject = site_name + _("{sender} sent you a file").format(sender=sender_name) msg = Message(subject) msg.sender = current_user.email msg.recipients = [recipient] msg.body = render_template_i18n( "documents/mail_file_sent.txt", sender_name=sender_name, message=user_msg, document_url=url_for(doc), filename=doc.title, ) filename = doc.title msg.attach(filename, doc.content_type, doc.content) mail.send(msg) flash(_("Email successfully sent"), "success") return redirect(url_for(doc)) @route("/doc//preview") def document_preview(doc_id): doc = get_document(doc_id) if not doc.antivirus_ok: return "Waiting for antivirus to finish" if doc.content_type == "application/pdf": return redirect( url_for(".document_view_pdf", community_id=g.community.slug, doc_id=doc.id) ) else: return redirect( url_for(".document_download", community_id=g.community.slug, doc_id=doc.id) ) @route("/doc//view_pdf") def document_view_pdf(doc_id): doc = get_document(doc_id) if not doc.antivirus_ok: return "Waiting for antivirus to finish" return render_template( "documents/view_pdf.html", pdf_url=url_for( ".document_download", community_id=g.community.slug, doc_id=doc.id ), ) # # Tagging (currently not used!) # # @route("/tag") # def tag(): # tag = request.args.get("tag") # if not tag: # return redirect("/dm/") # # bc = [dict(path="/", label="Home"), dict(path="/dm/", label="DM")] # bc += [dict(path=request.path, label="Filter by tag")] # # TODO ... # docs = Document.query.filter(Document.tags.like("%" + tag + "%")) # docs = list(docs.all()) # docs = [f for f in docs if tag in f.tags.split(",")] # title = "Files filtered by tag: %s" % tag # return render_template("dm/home.html", title=title, breadcrumbs=bc, # files=docs) # # # @route("//tag", methods=['POST']) # def tag_post(file_id): # doc = get_document(file_id) # tags = request.form.get("tags") # # doc.tags = tags # self = unwrap(current_app) # activity.send(self, actor=g.user, verb="tag", object=doc) # # db.session.commit() # # flash("Tags successfully successfully updated", "success") # return redirect(url_for(".document_view", doc_id=doc.id)) PK!+abilian/sbe/apps/documents/views/folders.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals import fnmatch import itertools import logging import os import re import tempfile from datetime import datetime from functools import partial from io import StringIO from typing import Any, List from zipfile import ZipFile, is_zipfile import sqlalchemy as sa import whoosh.query as wq from abilian.core.extensions import db from abilian.core.models.subjects import Group, User from abilian.core.signals import activity from abilian.core.util import unwrap from abilian.i18n import _, _n from abilian.services import get_service from abilian.services.security import READ, WRITE, Role, security from abilian.web import csrf, http, url_for from abilian.web.action import actions from abilian.web.views import default_view from flask import Markup, Response, current_app, flash, g, jsonify, \ make_response, redirect, render_template, render_template_string, \ request, send_file, session from flask_login import current_user from six import text_type from six.moves.urllib.parse import quote from sqlalchemy import func from werkzeug.exceptions import InternalServerError from xlwt import Workbook, easyxf from abilian.sbe.apps.communities.views import default_view_kw from abilian.sbe.apps.documents.models import Document, Folder, icon_for, \ icon_url from abilian.sbe.apps.documents.repository import repository from abilian.sbe.apps.documents.search import reindex_tree from .util import breadcrumbs_for, check_manage_access, check_read_access, \ check_write_access, create_document, edit_object, get_document, \ get_folder, get_new_filename, get_selected_objects from .views import blueprint route = blueprint.route __all__ = () @route("/") def index(): folder = g.community.folder url = url_for(folder) return redirect(url) @default_view(blueprint, Folder, id_attr="folder_id", kw_func=default_view_kw) @route("/folder/") def folder_view(folder_id): folder = get_folder(folder_id) bc = breadcrumbs_for(folder) actions.context["object"] = folder ctx = {"folder": folder, "children": folder.filtered_children, "breadcrumbs": bc} view_style = session.get("sbe_doc_view_style", "thumbnail_view") if view_style == "thumbnail_view": resp = render_template("documents/folder.html", **ctx) elif view_style == "gallery_view": resp = render_template("documents/folder_gallery_view.html", **ctx) else: raise InternalServerError("Unknown value for sbe_doc_view_style") return resp @route("/folder/change_view_style/", methods=["GET", "POST"]) @csrf.protect def change_view_style(folder_id): folder = get_folder(folder_id) if request.method == "POST": view_style = request.form["view_style"] if view_style == "gallery_view": session["sbe_doc_view_style"] = "gallery_view" else: session["sbe_doc_view_style"] = "thumbnail_view" return redirect( url_for( ".folder_view", folder_id=folder_id, community_id=folder.community.slug ) ) else: return redirect( url_for( ".folder_view", folder_id=folder_id, community_id=folder.community.slug ) ) @route("/folder//json") def folder_json(folder_id): """Return parent folder + subfolders.""" folder = get_folder(folder_id) folder_url = partial(url_for, ".folder_json") result = {} has_permission = security.has_permission result["current_folder_selectable"] = has_permission( current_user, WRITE, folder, inherit=True ) folders = result["folders"] = [] bc = result["breadcrumbs"] = [] subfolders = sorted( ( f for f in folder.subfolders if has_permission(current_user, READ, f, inherit=True) ), key=lambda f: f.title, ) parent = folder # breadcrumbs for parent in reversed(list(folder._iter_to_root())): if parent.is_root_folder: continue data = { "id": parent.id, "url": folder_url(folder_id=parent.id, community_id=parent.community.slug), "title": parent.title, } bc.append(data) if folder.parent is not None and not folder.parent.is_root_folder: # not at root folder: allow to go 1 level up data = bc[-2].copy() data["title"] = ".." folders.append(data) else: result["current_folder_selectable"] = False for folder in subfolders: data = { "id": folder.id, "url": folder_url(folder_id=folder.id, community_id=folder.community.slug), "title": folder.title, } folders.append(data) return jsonify(result) @route("/folder//members") def members(folder_id): folder = get_folder(folder_id) bc = breadcrumbs_for(folder) actions.context["object"] = folder members = folder.members() ctx = {"folder": folder, "members": members, "breadcrumbs": bc} return render_template("community/members.html", **ctx) @route("/folder//permissions") @http.nocache def permissions(folder_id): folder = get_folder(folder_id) check_manage_access(folder) bc = breadcrumbs_for(folder) actions.context["object"] = folder local_roles_assignments = folder.get_local_roles_assignments() principals = {p for p, r in local_roles_assignments} security._fill_role_cache_batch(principals) users_and_local_roles = [ (user, role, repository.has_access(user, folder)) for user, role in local_roles_assignments if isinstance(user, User) ] groups_and_local_roles = [ t for t in local_roles_assignments if isinstance(t[0], Group) ] users_and_inherited_roles = groups_and_inherited_roles = () if folder.inherit_security: inherited_roles_assignments = folder.get_inherited_roles_assignments() users_and_inherited_roles = [ (user, role, False) for user, role in inherited_roles_assignments if isinstance(user, User) ] groups_and_inherited_roles = [ t for t in inherited_roles_assignments if isinstance(t[0], Group) ] query = Group.query query = query.order_by(func.lower(Group.name)) all_groups = query.all() class EntryPresenter(object): _USER_FMT = ( '' "{{ user.name }}" ) _GROUP_FMT = ( '{{ group.name }}' ) def __init__(self, e): render = render_template_string self.entry = e self.date = e.happened_at.strftime("%Y-%m-%d %H:%M") self.manager = render( '' '' "{{ e.manager.name }}", e=e, ) if e.op == e.SET_INHERIT: msg = _("On {date}, {manager} has activated inheritance") elif e.op == e.UNSET_INHERIT: msg = _("On {date}, {manager} has deactivated inheritance") elif e.op == e.GRANT: msg = _('On {date}, {manager} has given role "{role}" to {principal}') elif e.op == e.REVOKE: msg = _( 'On {date}, {manager} has revoked role "{role}" from ' "{principal}" ) else: raise Exception("Unknown audit entry type %s" % e.op) principal = "" if self.entry.user: principal = render(self._USER_FMT, user=self.entry.user) elif self.entry.group: principal = render(self._GROUP_FMT, group=self.entry.group) self.msg = Markup( msg.format( date=self.date, manager=self.manager, role=self.entry.role, principal=principal, ) ) audit_entries = [EntryPresenter(e) for e in security.entries_for(folder)] ctx = { "folder": folder, "users_and_local_roles": users_and_local_roles, "users_and_inherited_roles": users_and_inherited_roles, "groups_and_local_roles": groups_and_local_roles, "groups_and_inherited_roles": groups_and_inherited_roles, "audit_entries": audit_entries, "all_groups": all_groups, "breadcrumbs": bc, } return render_template("documents/permissions.html", **ctx) @route("/folder//permissions", methods=["POST"]) @csrf.protect def permissions_update(folder_id): folder = repository.get_folder_by_id(folder_id) check_manage_access(folder) has_permission = security.has_permission action = request.form.get("action") if action in ("activate_inheritance", "deactivate_inheritance"): inherit_security = action == "activate_inheritance" if not ( inherit_security or has_permission(current_user, "manage", folder, inherit=False) ): # don't let user shoot himself in the foot flash( _( 'You must have the "manager" local role on this folder in ' "order to deactivate inheritance." ), "error", ) return redirect( url_for( ".permissions", folder_id=folder_id, community_id=folder.community.slug, ) ) security.set_inherit_security(folder, inherit_security) db.session.add(folder) reindex_tree(folder) db.session.commit() return redirect( url_for( ".permissions", folder_id=folder_id, community_id=folder.community.slug ) ) elif action == "add-user-role": role = request.form.get("role").lower() user_id = int(request.form.get("user")) user = User.query.get(user_id) security.grant_role(user, role, folder) reindex_tree(folder) db.session.commit() return redirect( url_for( ".permissions", folder_id=folder_id, community_id=folder.community.slug ) ) elif action == "add-group-role": role = request.form.get("role").lower() group_id = int(request.form.get("group")) group = Group.query.get(group_id) security.grant_role(group, role, folder) reindex_tree(folder) db.session.commit() return redirect( url_for( ".permissions", folder_id=folder_id, community_id=folder.community.slug ) ) else: action, args = request.form.items()[0] role, object_id = args.split(":") role = role.lower() object_id = int(object_id) if action == "delete-user-role": user = User.query.get(object_id) # remove role in a subtransaction, to prevent manager shoot himself in the # foot transaction = db.session.begin_nested() security.ungrant_role(user, role, folder) if ( user == current_user and role == "manager" and not has_permission(current_user, "manage", folder, inherit=True) ): transaction.rollback() flash( _( 'Cannot remove "manager" local role for yourself: you ' 'don\'t have "manager" role (either by security inheritance ' "or by group membership)" ), "error", ) else: reindex_tree(folder) transaction.commit() flash( _("Role {role} for user {user} removed on folder {folder}").format( role=role, user=user.name, folder=folder.name ), "success", ) elif action == "delete-group-role": group = Group.query.get(object_id) # remove role in a subtransaction, to prevent manager shoot himself in the # foot transaction = db.session.begin_nested() security.ungrant_role(group, role, folder) if role == "manager" and not has_permission( current_user, "manage", folder, inherit=True ): transaction.rollback() flash( _( 'Cannot remove "manager" local role for group "{group}": you' ' don\'t have "manager" role by security inheritance or by ' "local role" ).format(group=group.name), "error", ) else: flash( _( "Role {role} for group {group} removed on folder {folder}" ).format(role=role, group=group.name, folder=folder.name), "success", ) reindex_tree(folder) transaction.commit() db.session.commit() return redirect( url_for( ".permissions", folder_id=folder_id, community_id=folder.community.slug ) ) @route("/folder//permissions_export") @http.nocache def permissions_export(folder_id): folder = repository.get_folder_by_id(folder_id) check_manage_access(folder) wb = Workbook() ws = wb.add_sheet("Sheet 1") cols = [ ("Accès", 20), ("Identifiant", 40), ("Prénom", 14), ("Nom", 20), ("Rôle", None), ("Local", None), ("Héritage", None), ("Communauté", 60), ] # styling # from xlwt doc: width unit is 1/256 of '0' from first font in excel file for c, width in enumerate((c[1] for c in cols)): if width is not None: ws.col(c).width = 256 * width ws.row(0).height = 256 + 128 ws.panes_frozen = True ws.remove_splits = True ws.horz_split_pos = 1 header_style = easyxf( "font: bold true;" "alignment: horizontal center, vertical center;" ) for c, val in enumerate((c[0] for c in cols)): ws.write(0, c, val, header_style) # data permissions = iter_permissions(folder, current_user) row_offset = 0 current_community = None for r, row in enumerate(permissions, 1): if current_community is None: current_community = row[-1] if current_community != row[-1]: current_community = row[-1] row_offset += 1 # data grouping enter (refered as 'outline' in xlwt documention) ws.row(r + row_offset).level = 1 for c, value in enumerate(row): if isinstance(value, Role): value = text_type(value) ws.write(r + row_offset, c, value) # data grouping exit ws.row(r + row_offset).level = 1 debug = request.args.get("debug_sql") if debug: # useful only in DEBUG mode, to get the debug toolbar in browser return "Exported" fd = StringIO() wb.save(fd) response = make_response(fd.getvalue()) response.headers["content-type"] = "application/ms-excel" folder_name = folder.title.replace(" ", "_") file_date = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") filename = "permissions-{}-{}.xls".format(folder_name, file_date) content_disposition = 'attachment;filename="{}"'.format(filename) response.headers["content-disposition"] = content_disposition return response def iter_permissions(folder, user): """Iterator returning permissions settings on folder and its subfolders tree.""" if not security.has_permission(user, "manage", folder, inherit=True): return community = folder.path local_roles = frozenset(folder.get_local_roles_assignments()) inherited_roles = frozenset( (folder.get_inherited_roles_assignments() if folder.inherit_security else []) ) result = {} for principal, role in local_roles | inherited_roles: data = result.setdefault((principal, role), {}) data["local"] = (principal, role) in local_roles data["inherit"] = (principal, role) in inherited_roles def _sort_key(item): """Sorts by name, groups first.""" principal = item[0][0] is_user = isinstance(principal, User) item_key = [is_user] # type: List[Any] if is_user: last_name = principal.last_name or "" first_name = principal.first_name or "" item_key.append(last_name.lower()) item_key.append(first_name.lower()) else: item_key.append(principal.name) return item_key for (p, role), data in sorted(result.items(), key=_sort_key): is_user = isinstance(p, User) has_access = False if is_user else "*" identifier = p.email if is_user else "* Group *" first_name = p.first_name if is_user else "-" last_name = p.last_name if is_user else p.name local = data["local"] inherit = data["inherit"] yield ( has_access, identifier, first_name, last_name, role, local, inherit, community, ) subfolders = ( f for f in folder.subfolders if security.has_permission(user, "manage", folder) ) for subfolder in subfolders: for permission in iter_permissions(subfolder, user): yield permission # # Actions on folders # @route("/folder/", methods=["POST"]) @csrf.protect def folder_post(folder_id): """A POST on a folder can result on several different actions (depending on the `action` parameter).""" folder = get_folder(folder_id) action = request.form.get("action") if action == "edit": return folder_edit(folder) elif action == "upload": return upload_new(folder) elif action == "download": return download_multiple(folder) elif action == "delete": return delete_multiple(folder) elif action == "new": return create_subfolder(folder) elif action == "move": return move_multiple(folder) elif action == "change-owner": return change_owner(folder) else: # Probably an error or a hack attempt. # Logger will inform sentry if enabled logger = logging.getLogger(__name__) logger.error("Unknown folder action.", extra={"stack": True}) flash(_("Unknown action."), "error") return redirect(url_for(folder)) def folder_edit(folder): check_write_access(folder) changed = edit_object(folder) if changed: db.session.commit() flash(_("Folder properties successfully edited."), "success") else: flash(_("You didn't change any property."), "success") return redirect(url_for(folder)) ARCHIVE_IGNORE_FILES_GLOBS = {"__MACOSX/*", ".DS_Store"} # translates patterns to match with any parent directory ((*/)?pattern should # match) ARCHIVE_IGNORE_FILES = { re.compile("(?:.*\\/)?" + fnmatch.translate(pattern)) for pattern in ARCHIVE_IGNORE_FILES_GLOBS } # skip directory names. Directory will be created only if they contains files ARCHIVE_IGNORE_FILES.add(re.compile(fnmatch.translate("*/"))) def explore_archive(fd, uncompress=False): """Given an uploaded file descriptor, return it or a list of archive content. Yield tuple(filepath, file-like object), where filepath is a list whose components are directories and last one is filename. """ if not uncompress: yield [], fd return if not is_zipfile(fd): yield [], fd return # XXX: workaround https://bugs.python.org/issue26175 in Python 3.7 # TODO: Remove when it's fixed fd.seekable = lambda: True with ZipFile(fd, "r") as archive: for zipinfo in archive.infolist(): filename = zipinfo.filename if isinstance(filename, bytes): # not unicode: try to convert from utf-8 (OSX case: unicode flag not # set), then legacy cp437 # http://stackoverflow.com/questions/13261347/correctly-decoding-zip-entry-file-names-cp437-utf-8-or try: filename = filename.decode("utf-8") except UnicodeDecodeError: filename = filename.decode("cp437") if any( pattern.match(filename) is not None for pattern in ARCHIVE_IGNORE_FILES ): continue filepath = filename.split("/") filename = filepath.pop() zip_fd = archive.open(zipinfo, "r") setattr(zip_fd, "filename", filename) setattr(zip_fd, "content_type", None) yield filepath, zip_fd def upload_new(folder): check_write_access(folder) session = db.session() base_folder = folder uncompress_files = "uncompress_files" in request.form fds = request.files.getlist("file") created_count = 0 path_cache = {} # mapping folder path in zip: folder instance for upload_fd in fds: for filepath, fd in explore_archive(upload_fd, uncompress=uncompress_files): folder = base_folder parts = [] # traverse to final directory, create intermediate if necessary. Folders # may be renamed if a file already exists, path_cache is used to keep # track of this for subfolder_name in filepath: parts.append(subfolder_name) path = "/".join(parts) if path in path_cache: folder = path_cache[path] continue subfolders = {f.title: f for f in folder.subfolders} if subfolder_name in subfolders: folder = subfolders[subfolder_name] path_cache[path] = folder continue subfolder_name = get_new_filename(folder, subfolder_name) folder = folder.create_subfolder(subfolder_name) session.flush() path_cache[path] = folder create_document(folder, fd) created_count += 1 flash( _n( "One new document successfully uploaded", "%(num)d new document successfully uploaded", num=created_count, ), "success", ) session.commit() return redirect(url_for(folder)) def download_multiple(folder): folders, docs = get_selected_objects(folder) if not folders: folders = [folder] def rel_path(path, content): return "{}/{}".format(path, content.title) def zip_folder(zipfile, folder, path=""): for doc in folder.documents: doc_path = rel_path(path, doc) zipfile.writestr(doc_path, doc.content or b"") for subfolder in folder.filtered_subfolders: zip_folder(zipfile, subfolder, rel_path(path, subfolder)) return zipfile # if using upstream send file: just create a temps file. # if app is streaming itself: use NamedTemporaryFile so that file is removed # on close() temp_factory = ( tempfile.mktemp if current_app.use_x_sendfile else tempfile.NamedTemporaryFile ) zip_fn = temp_factory(prefix="tmp-" + current_app.name + "-", suffix=".zip") with ZipFile(zip_fn, "w") as zipfile: for doc in docs: zipfile.writestr(doc.title, doc.content or b"") for subfolder in folders: zip_folder(zipfile, subfolder, subfolder.title) if not isinstance(zip_fn, str): zip_fn.seek(0, os.SEEK_END) size = zip_fn.tell() zip_fn.seek(0) else: size = os.path.getsize(zip_fn) resp = send_file( zip_fn, mimetype="application/zip", as_attachment=True, attachment_filename=quote(folder.title.encode("utf8") + b".zip"), ) resp.headers.add("Content-Length", str(size)) return resp def delete_multiple(folder): check_write_access(folder) folders, docs = get_selected_objects(folder) for obj in docs + folders: app = unwrap(current_app) community = g.community._model activity.send( app, actor=current_user, verb="delete", object=obj, target=community ) repository.delete_object(obj) if docs + folders: db.session.commit() if docs and folders: msg = _( "%(file_num)d files and %(folder_num)d folders sucessfully " "deleted.", file_num=len(docs), folder_num=len(folders), ) elif docs and not folders: msg = _n( "1 file sucessfully deleted.", "%(num)d files sucessfully deleted.", num=len(docs), ) else: msg = _n( "1 folder sucessfully deleted.", "%(num)d folders sucessfully deleted.", num=len(folders), ) flash(msg, "success") else: flash(_("No object deleted"), "error") return redirect(url_for(folder)) def move_multiple(folder): # type: (Folder) -> Response folders, docs = get_selected_objects(folder) objects = folders + docs count_f = len(folders) count_d = len(docs) current_folder_url = url_for(folder) if not (count_f + count_d): flash(_("Move elements: no elements selected."), "info") return redirect(current_folder_url) try: target_folder_id = int(request.form.get("target-folder")) except ValueError: flash(_("Move elements: no destination folder selected. Aborted."), "error") return redirect(current_folder_url) target_folder = repository.get_folder_by_id(target_folder_id) if folder == target_folder: flash( _( "Move elements: source and destination folder are identical," " nothing done." ), "error", ) return redirect(current_folder_url) if not security.has_permission(current_user, "write", folder, inherit=True): # this should not happen: this is just defensive programming flash(_("You are not allowed to move elements from this folder"), "error") return redirect(current_folder_url) if not security.has_permission(current_user, "write", target_folder, inherit=True): flash( _('You are not allowed to write in folder "{folder}"').format( folder=target_folder.title ), "error", ) return redirect(current_folder_url) for item in objects: # FIXME: maybe too brutal check_write_access(item) # verify we are not trying to move a folder inside itself or one of its # descendants f = target_folder while f: if f in folders: flash( _( "Move elements: destination folder is included in moved " "elements. Moved nothing." ), "error", ) return redirect(url_for(folder)) f = f.parent exist_in_dest = objects_which_exist_in_dest(objects, target_folder) if exist_in_dest: # items existing in destination: cancel operation db.session.rollback() msg = _( "Move elements: canceled, some elements exists in destination " "folder: {elements}" ) elements = ", ".join('"{}"'.format(i.title) for i in exist_in_dest) flash(msg.format(elements=elements), "error") return redirect(current_folder_url) db.session.commit() msg_f = ( _n("1 folder", "{count} folders", count_f) if count_f else _("0 folder") ).format(count=count_f) msg_d = ( _n("1 document", "{count} documents", count_d) if count_d else _("0 document") ).format(count=count_d) msg = _("{folders} and {documents} moved to {target}").format( folders=msg_f, documents=msg_d, target=target_folder.title ) flash(msg, "success") return redirect(url_for(folder)) def objects_which_exist_in_dest(objects, dest): exist_in_dest = [] for item in objects: try: with db.session.begin_nested(): item.parent = dest except sa.exc.IntegrityError: exist_in_dest.append(item) return exist_in_dest def create_subfolder(folder): check_write_access(folder) title = request.form.get("title", "") description = request.form.get("description", "") subfolder = folder.create_subfolder(title) subfolder.description = description db.session.commit() return redirect(url_for(folder)) def change_owner(folder): check_manage_access(folder) items = itertools.chain(*get_selected_objects(folder)) user_id = request.form.get("new_owner", type=int) user = User.query.get(user_id) for item in items: item.owner = user db.session.commit() return redirect(url_for(folder)) @route("/folder/check_valid_name") @http.nocache def check_valid_name(): """Check if name is valid for content creation in this folder.""" object_id = int(request.args.get("object_id")) action = request.args.get("action") title = request.args.get("title") get_object = get_document if action == "document-edit" else get_folder obj = get_object(object_id) check_read_access(obj) if action == "new": parent = obj help_text = _('An element named "{name}" is already present in folder') elif action in ("folder-edit", "document-edit"): parent = obj.parent help_text = _('Cannot rename: "{name}" is already present in parent ' "folder") else: raise InternalServerError() existing = {e.title for e in parent.children} if action in ("folder-edit", "document-edit"): try: existing.remove(obj.title) except KeyError: pass result = {} valid = result["valid"] = title not in existing if not valid: result["help_text"] = help_text.format(name=title) return jsonify(result) @route("/folder//descendants") def descendants_view(folder_id): folder = get_folder(folder_id) bc = breadcrumbs_for(folder) actions.context["object"] = folder root_path_ids = folder._indexable_parent_ids + "/{}".format(folder.id) index_service = get_service("indexing") filters = wq.And( [ wq.Term("community_id", folder.community.id), wq.Term("parent_ids", root_path_ids), wq.Or( [ wq.Term("object_type", Folder.entity_type), wq.Term("object_type", Document.entity_type), ] ), ] ) results = index_service.search("", filter=filters, limit=None) by_path = {} owner_ids = set() for hit in results: by_path.setdefault(hit["parent_ids"], []).append(hit) owner_type, owner_id = hit["owner"].split(":") if owner_type == "user": try: owner_id = int(owner_id) owner_ids.add(owner_id) except ValueError: pass for children in by_path.values(): children.sort( key=lambda hit: ( hit["object_type"] != Folder.entity_type, hit["name"].lower(), ) ) descendants = [] def visit(path_id, level=0): children = by_path.get(path_id, ()) for child in children: is_folder = child["object_type"] == Folder.entity_type type_letter = "F" if is_folder else "D" descendants.append((level, type_letter, child)) if is_folder: path_id = child["parent_ids"] + "/{}".format(child["id"]) visit(path_id, level + 1) visit(root_path_ids, 0) owners = {} owners_query = User.query.filter(User.id.in_(owner_ids)).add_column( sa.sql.func.concat("user:", User.id).label("key") ) for user, key in owners_query: owners[key] = user ctx = { "folder": folder, "descendants": descendants, "owners": owners, "breadcrumbs": bc, "get_icon": get_icon_for_hit, } return render_template("documents/descendants.html", **ctx) def get_icon_for_hit(hit): if hit["object_type"] == Folder.entity_type: return icon_url("folder.png") content_type = hit["content_type"] icon = icon_for(content_type) return icon PK!NgJEE(abilian/sbe/apps/documents/views/util.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals import re from typing import List, Tuple from abilian.core.signals import activity from abilian.core.util import unwrap from abilian.services.security import MANAGE, WRITE, Admin, security from abilian.web import url_for from flask import current_app, flash, g, request from flask_babel import gettext as _ from flask_login import current_user from six import text_type from werkzeug.exceptions import Forbidden, InternalServerError, NotFound from abilian.sbe.apps.documents.models import Document, Folder from abilian.sbe.apps.documents.repository import repository # # Utils # def breadcrumbs_for(object): if object is None: return [] bc = [{"label": object.title}] parent = object.parent while parent and not parent.is_root_folder: bc = [{"label": parent.title, "path": url_for(parent)}] + bc parent = parent.parent return bc def get_document(id): """Gets a document given its id. Will raise appropriates errors in case the document doesn't exist (404), or the current user doesn't have read access on the document (403). """ doc = repository.get_document_by_id(id) check_read_access(doc) return doc def get_folder(id): """Gets a folder given its id. Will raise appropriates errors in case the folder doesn't exist (404), or the current user doesn't have read access on the folder (403). """ folder = repository.get_folder_by_id(id) check_read_access(folder) return folder def get_new_filename(folder, name): """Given a desired name for a new content in folder, return a name suitable for new content. If name already exists, a numbered suffix is added. """ existing = {c.name for c in folder.children} renamed = name in existing if renamed: name = name.rsplit(".", 1) ext = ".{}".format(name[1]) if len(name) > 1 else "" name = name[0] prefix = "{}-".format(name) prefix_len = len(prefix) # find all numbered suffixes from name-1.ext, name-5.ext,... suffixes = ( n[prefix_len:].rsplit(".", 1)[0] for n in existing if n.startswith(prefix) and n.endswith(ext) ) suffixes = [int(val) for val in suffixes if re.match(r"^\d+$", val)] index = max(0, 0, *suffixes) + 1 # 0, 0: in case suffixes is empty name = "{}-{}{}".format(name, index, ext) return name def create_document(folder, fs): check_write_access(folder) if isinstance(fs.filename, text_type): name = fs.filename else: name = text_type(fs.filename, errors="ignore") if not name: flash(_("Document name can't be empty."), "error") return None original_name = name name = get_new_filename(folder, name) doc = folder.create_document(title=name) doc.set_content(fs.read(), fs.content_type) if original_name != name: # set message after document has been successfully created! flash( _('"{original}" already present in folder, ' 'renamed "{name}"').format( original=original_name, name=name ), "info", ) # Some unwrapping before posting event app = unwrap(current_app) community = g.community._model activity.send(app, actor=current_user, verb="post", object=doc, target=community) return doc def edit_object(obj): title = request.form.get("title", "") description = request.form.get("description", "") changed = False if title != obj.title: obj.title = title changed = True if description != obj.description: obj.description = description changed = True return changed def get_selected_objects(folder): # type: (Folder) -> Tuple[List[Folder], List[Document]] """Returns a tuple, (folders, docs), of folders and docs in the specified folder that have been selected from the UI.""" selected_ids = request.form.getlist("object-selected") doc_ids = [ int(x.split(":")[-1]) for x in selected_ids if x.startswith("cmis:document") ] folder_ids = [ int(x.split(":")[-1]) for x in selected_ids if x.startswith("cmis:folder") ] docs = list(map(get_document, doc_ids)) folders = list(map(get_folder, folder_ids)) for obj in docs + folders: if obj.parent != folder: raise InternalServerError() return folders, docs def check_read_access(obj): """Checks the current user has appropriate read access on the given object. Will raise appropriates errors in case the object doesn't exist (404), or the current user doesn't have read access on the object (403). """ if not obj: raise NotFound() if not security.running: return True if security.has_role(current_user, Admin): return True if repository.has_access(current_user, obj): return True raise Forbidden() def check_write_access(obj): """Checks the current user has appropriate write access on the given object. Will raise appropriates errors in case the object doesn't exist (404), or the current user doesn't have write access on the object (403). """ if not obj: raise NotFound() if not security.running: return if security.has_role(current_user, Admin): return if repository.has_access(current_user, obj) and repository.has_permission( current_user, WRITE, obj ): return raise Forbidden() def check_manage_access(obj): """Checks the current user has appropriate manage access on the given object. Will raise appropriates errors in case the object doesn't exist (404), or the current user doesn't have manage access on the object (403). """ if not obj: raise NotFound() if not security.running: return if security.has_role(current_user, Admin): return if repository.has_access(current_user, obj) and repository.has_permission( current_user, MANAGE, obj ): return raise Forbidden() def match(mime_type, patterns): if not mime_type: mime_type = "application/binary" for pat in patterns: pat = pat.replace("*", r"\w*") if re.match(pat, mime_type): return True return False PK!=)abilian/sbe/apps/documents/views/views.py# -*- coding: utf-8 -*- """Document management blueprint.""" from __future__ import absolute_import, print_function, unicode_literals from abilian.i18n import _l from abilian.web.action import Endpoint from abilian.web.nav import BreadcrumbItem from flask import g from abilian.sbe.apps.communities.blueprint import Blueprint from abilian.sbe.apps.communities.security import is_manager from ..actions import register_actions __all__ = ["blueprint"] blueprint = Blueprint( "documents", __name__, url_prefix="/docs", template_folder="../templates" ) route = blueprint.route blueprint.record_once(register_actions) @blueprint.url_value_preprocessor def init_document_values(endpoint, values): g.current_tab = "documents" g.is_manager = is_manager() g.breadcrumb.append( BreadcrumbItem( label=_l("Documents"), url=Endpoint("documents.index", community_id=g.community.slug), ) ) PK!lɖ/-abilian/sbe/apps/documents/webdav/__init__.py# coding=utf-8 """WebDAV interface to the document repository.""" from __future__ import absolute_import from .views import webdav # noqa PK!*++.abilian/sbe/apps/documents/webdav/constants.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals DAV_PROPS = [ "creationdate", "displayname", "getcontentlanguage", "getcontentlength", "getcontenttype", "getetag", "getlastmodified", "lockdiscovery", "resourcetype", "source", "supportedlock", ] # DAV level 1 # OPTIONS = 'GET, HEAD, COPY, MOVE, POST, PUT, PROPFIND, PROPPATCH, OPTIONS, '\ # 'MKCOL, DELETE, TRACE, REPORT' OPTIONS = ( "GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PROPFIND, " + "PROPPATCH, MKCOL, COPY, MOVE" ) # + ', LOCK, UNLOCK' HTTP_CONTINUE = 100 HTTP_SWITCHING_PROTOCOLS = 101 HTTP_PROCESSING = 102 HTTP_OK = 200 HTTP_CREATED = 201 HTTP_ACCEPTED = 202 HTTP_NON_AUTHORITATIVE_INFO = 203 HTTP_NO_CONTENT = 204 HTTP_RESET_CONTENT = 205 HTTP_PARTIAL_CONTENT = 206 HTTP_MULTI_STATUS = 207 HTTP_IM_USED = 226 HTTP_MULTIPLE_CHOICES = 300 HTTP_MOVED = 301 HTTP_FOUND = 302 HTTP_SEE_OTHER = 303 HTTP_NOT_MODIFIED = 304 HTTP_USE_PROXY = 305 HTTP_TEMP_REDIRECT = 307 HTTP_BAD_REQUEST = 400 HTTP_PAYMENT_REQUIRED = 402 HTTP_FORBIDDEN = 403 HTTP_NOT_FOUND = 404 HTTP_METHOD_NOT_ALLOWED = 405 HTTP_NOT_ACCEPTABLE = 406 HTTP_PROXY_AUTH_REQUIRED = 407 HTTP_REQUEST_TIMEOUT = 408 HTTP_CONFLICT = 409 HTTP_GONE = 410 HTTP_LENGTH_REQUIRED = 411 HTTP_PRECONDITION_FAILED = 412 HTTP_REQUEST_ENTITY_TOO_LARGE = 413 HTTP_REQUEST_URI_TOO_LONG = 414 HTTP_MEDIATYPE_NOT_SUPPORTED = 415 HTTP_RANGE_NOT_SATISFIABLE = 416 HTTP_EXPECTATION_FAILED = 417 HTTP_UNPROCESSABLE_ENTITY = 422 HTTP_LOCKED = 423 HTTP_FAILED_DEPENDENCY = 424 HTTP_UPGRADE_REQUIRED = 426 HTTP_INTERNAL_ERROR = 500 HTTP_NOT_IMPLEMENTED = 501 HTTP_BAD_GATEWAY = 502 HTTP_SERVICE_UNAVAILABLE = 503 HTTP_GATEWAY_TIMEOUT = 504 HTTP_VERSION_NOT_SUPPORTED = 505 HTTP_INSUFFICIENT_STORAGE = 507 HTTP_NOT_EXTENDED = 510 PK!0/<4$4$*abilian/sbe/apps/documents/webdav/views.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals import os.path import uuid from abilian.core.extensions import db from abilian.services import get_service from flask import Blueprint, request from flask_login import current_user from lxml.etree import XMLSyntaxError from werkzeug.datastructures import Headers from werkzeug.exceptions import Forbidden, NotFound from werkzeug.wrappers import BaseResponse as Response from .. import repository from .constants import DAV_PROPS, HTTP_BAD_REQUEST, HTTP_CONFLICT, \ HTTP_CREATED, HTTP_METHOD_NOT_ALLOWED, HTTP_MULTI_STATUS, \ HTTP_NO_CONTENT, HTTP_OK, HTTP_PRECONDITION_FAILED, OPTIONS from .xml import MultiStatus, Propfind webdav = Blueprint("webdav", __name__, url_prefix="/webdav") route = webdav.route __all__ = ["webdav"] # # Utils # # TODO: real logging class Logger(object): def debug(self, msg): print(msg) log = Logger() # XXX: temporary debug info. @webdav.before_request def log_request(): litmus_msg = request.headers.get("X-Litmus") if litmus_msg: print("") print(78 * "#") print(litmus_msg) print("{} on {}".format(request.method, request.path)) @webdav.before_request def only_admin(): security = get_service("security") if not security.has_role(current_user, "admin"): raise Forbidden() @webdav.after_request def log_response(response): print("Response: {}".format(response)) return response def normpath(path): path = os.path.normpath(path) if not path.startswith("/"): path = "/" + path return path def split_path(path): path = normpath(path) return os.path.dirname(path), os.path.basename(path) def get_object(path): obj = repository.get_object_by_path(path) if obj is None: raise NotFound() return obj @webdav.before_app_request def create_root_folder(): # TODO: create root folder on repository startup instead. # assert repository.root_folder pass # # HTTP endpoints # @route("/", methods=["OPTIONS"], defaults={"path": "/"}) @route("/", methods=["OPTIONS"]) def options(path): headers = { "Content-Type": "text/plain", "Content-Length": "0", "DAV": "1,2", "MS-Author-Via": "DAV", "Allow": OPTIONS, } print("Returning", headers) return "", HTTP_OK, headers @route("/", defaults={"path": "/"}) @route("/") def get(path): path = normpath(path) obj = get_object(path) file_name = obj.file_name.encode("utf8") headers = { "Content-Type": obj.content_type, "Content-Disposition": "attachment;filename=%s" % file_name, } return obj.content, HTTP_OK, headers @route("/", methods=["MKCOL"]) def mkcol(path): path = normpath(path) parent_path, name = split_path(path) if request.data: return "Request body must be empty.", 415, {} obj = repository.get_object_by_path(path) if obj is not None: return "Objet already exists.", HTTP_METHOD_NOT_ALLOWED, {} parent_folder = repository.get_folder_by_path(parent_path) if parent_folder is None: return "Parent collection doesn't exist.", HTTP_CONFLICT, {} new_folder = parent_folder.create_subfolder(name=name) # noqa db.session.commit() return "", HTTP_CREATED, {} @route("/", methods=["PUT"]) def put(path): path = normpath(path) parent_path, name = split_path(path) status = HTTP_CREATED obj = repository.get_object_by_path(path) if obj is not None: if not obj.is_document: return "", HTTP_METHOD_NOT_ALLOWED, {} else: status = HTTP_NO_CONTENT else: parent_folder = repository.get_folder_by_path(parent_path) if parent_folder is None: return "", HTTP_CONFLICT, {} obj = parent_folder.create_document(name=name) obj.content = request.data obj.content_type = request.content_type db.session.commit() return "", status, {} @route("/", methods=["DELETE"]) def delete(path): path = normpath(path) obj = get_object(path) db.session.delete(obj) db.session.commit() return "", HTTP_NO_CONTENT, {} @route("/", methods=["COPY", "MOVE"]) def copy_or_move(path): path = normpath(path) dest = request.headers.get("destination") dest_path = normpath(dest[len(request.url_root + "webdav") :]) dest_parent_path, dest_name = split_path(dest_path) overwrite = request.headers.get("overwrite") orig_obj = get_object(path) status = HTTP_CREATED dest_obj = repository.get_object_by_path(dest_path) if dest_obj: if overwrite == "F": return "", HTTP_PRECONDITION_FAILED, {} else: repository.delete_object(dest_obj) db.session.flush() status = HTTP_NO_CONTENT dest_folder = repository.get_folder_by_path(dest_parent_path) if dest_folder is None: return "", HTTP_CONFLICT, {} if request.method == "COPY": repository.copy_object(orig_obj, dest_folder, dest_name) else: if dest_folder == orig_obj.parent: repository.rename_object(orig_obj, dest_name) else: repository.move_object(orig_obj, dest_folder, dest_name) db.session.commit() return "", status, {} @route("/", defaults={"path": "/"}, methods=["PROPFIND"]) @route("/", methods=["PROPFIND"]) def propfind(path): path = normpath(path) depth = request.headers.get("depth", "1") print(request.headers) print(request.data) try: propfind = Propfind(request.data) # noqa except XMLSyntaxError: return "Malformed XML document.", HTTP_BAD_REQUEST, {} obj = get_object(path) m = MultiStatus() m.add_response_for(request.url, obj, DAV_PROPS) if depth == "1" and obj.is_folder: for child in obj.children: m.add_response_for(request.url + "/" + child.name, child, DAV_PROPS) print(m.to_string()) headers = {"Content-Type": "text/xml"} return m.to_string(), HTTP_MULTI_STATUS, headers @route("/", methods=["LOCK"]) def lock(path): path = normpath(path) obj = get_object(path) token = str(uuid.uuid1()) if repository.is_locked(obj): if not repository.can_unlock(obj): return "", 423, {} else: headers = {"Lock-Token": "urn:uuid:" + token} return "TODO", HTTP_OK, headers token = repository.lock(obj) xml = ( """ 0 Second-179 flora opaquelocktoken:%s """ % token ) hlist = [("Content-Type", "text/xml"), ("Lock-Token", "" % token)] return Response(xml, headers=Headers.linked(hlist)) # , status ='423 Locked' # public Response lock(@Context UriInfo uriInfo) throws Exception { # String token = null; # Prop prop = null; # if (backend.isLocked(doc.getRef())) { # if (!backend.canUnlock(doc.getRef())) { # return Response.status(423).build(); # } else { # token = backend.getCheckoutUser(doc.getRef()); # prop = new Prop(getLockDiscovery(doc, uriInfo)); # return Response.ok().entity(prop).header("Lock-Token", # "urn:uuid:" + token).build(); # } # } # # token = backend.lock(doc.getRef()); # if (READONLY_TOKEN.equals(token)) { # return Response.status(423).build(); # } else if (StringUtils.isEmpty(token)) { # return Response.status(400).build(); # } # # prop = new Prop(getLockDiscovery(doc, uriInfo)); # # backend.saveChanges(); # return Response.ok().entity(prop).header("Lock-Token", # "urn:uuid:" + token).build(); # } @route("/", methods=["UNLOCK"]) def unlock(path): path = normpath(path) obj = get_object(path) if repository.is_locked(obj): if not repository.can_unlock(obj): return "", 423, {} else: repository.unlock(obj) db.session.commit() return "", HTTP_NO_CONTENT, {} return "", HTTP_NO_CONTENT, {} # if (backend.isLocked(doc.getRef())) { # if (!backend.canUnlock(doc.getRef())) { # return Response.status(423).build(); # } else { # backend.unlock(doc.getRef()); # backend.saveChanges(); # return Response.status(HTTP_NO_CONTENT).build(); # } # } else { # // TODO: return an error # return Response.status(HTTP_NO_CONTENT).build(); # } # PK!^-66(abilian/sbe/apps/documents/webdav/xml.py# coding=utf-8 """Parses and produces XML documents specified by the standard.""" from __future__ import absolute_import, print_function, unicode_literals from lxml import etree, objectify from lxml.builder import ElementMaker E = ElementMaker(namespace="DAV:") class Propfind(object): def __init__(self, xml=""): self.mode = "" self.prop_names = [] if not xml: xml = "" self.parse(xml) def parse(self, xml): root = objectify.fromstring(xml) child = root.getchildren()[0] self.mode = child.tag[len("{DAV:}") :] if self.mode == "prop": for prop in child.getchildren(): self.prop_names.append(prop.tag) class MultiStatus(object): def __init__(self): self.responses = [] def add_response_for(self, href, obj, property_list): response = Response(href, obj, property_list) self.responses.append(response) def to_string(self): return etree.tostring(self.to_xml(), pretty_print=True) def to_xml(self): xml = E.multistatus() for response in self.responses: xml.append(response.to_xml()) return xml class Response(object): def __init__(self, href, obj, property_list): self.href = href self.property_list = property_list self.obj = obj def to_xml(self): obj = self.obj props = E.prop() for property_name in self.property_list: if property_name == "creationdate": props.append(E.creationdate("1997-12-01T17:42:21-08:00")) elif property_name == "displayname": props.append(E.displayname(obj.name)) elif property_name == "resourcetype": if obj.is_folder: props.append(E.resourcetype(E.collection())) else: props.append(E.resourcetype()) return E.response( E.href(self.href), E.propstat(props), E.status("HTTP/1.1 200 OK") ) PK!L"abilian/sbe/apps/forum/__init__.py# coding=utf-8 """Forum module.""" from __future__ import absolute_import from abilian.sbe.extension import sbe def register_plugin(app): app.config.setdefault("SBE_FORUM_REPLY_BY_MAIL", False) app.config.setdefault("MAIL_ADDRESS_TAG_CHAR", u"+") app.config.setdefault("INCOMING_MAIL_USE_MAILDIR", False) sbe.init_app(app) from .views import forum from .actions import register_actions from .models import ThreadIndexAdapter from .commands import manager from . import tasks forum.record_once(register_actions) app.register_blueprint(forum) app.services["indexing"].adapters_cls.insert(0, ThreadIndexAdapter) tasks.init_app(app) if app.script_manager: app.script_manager.add_command("forum", manager) PK!D !abilian/sbe/apps/forum/actions.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.i18n import _l from abilian.services import get_service from abilian.services.security import Admin from abilian.web.action import Action, FAIcon, ModalActionMixin, actions from flask import g, request, url_for from flask_login import current_user class ForumAction(Action): def is_current(self): return request.path == self.url() def is_filtered(self): filter_keys = ["today", "month", "year", "week"] current_url = request.path.split("/") filter = current_url[-1].strip() if filter not in filter_keys: return False if self.url() == "#filter": return True return False def url(self, context=None): if self._url or self.endpoint: return super(ForumAction, self).url(context=context) return url_for("." + self.name, community_id=g.community.slug) class ThreadAction(ForumAction): def pre_condition(self, context): thread = actions.context.get("object") return not not thread def is_admin(context): security = get_service("security") return security.has_role(current_user, Admin, object=context.get("object")) def is_in_thread(context): thread = context.get("object") return not thread def is_closed(context): thread = context.get("object") return thread.closed def not_closed(context): return not is_closed(context) class ForumModalAction(ModalActionMixin, ThreadAction): pass _close_template_action = """

{% endblock %} PK!_9= = 1abilian/sbe/apps/forum/templates/forum/index.html{% extends "forum/_base.html" %} {%- from "macros/user.html" import m_user_link, m_user_photo %} {% from "macros/box.html" import m_box_menu, m_box_content %} {% from "forum/_macros.html" import forum_menu %} {% block forumcontent %}
{{ _("Showing :") }} {%- set global_actions = actions.for_category('forum:global') %} {{ forum_menu(global_actions) }}
{% if threads %} {% for thread in threads %} {{ m_thread(thread) }} {% endfor %}
{{ _("Topic") }} {{ _("Participants") }} {{ _("Replies") }} {{ _("Viewers") }} {{ _("Views") }} {{ _("Activity") }}


{% else %}

{{ _("No message has been posted to this community yet.") }}

{% endif %} {% if has_more or True %}

{{ _("All conversations") }}

{% endif %}
{% endblock %} {% macro m_thread(thread) %} {%- set thread_href = url_for(thread) %} {%- set thread_length = thread.posts|length %}

{%- if thread.closed %} {%- endif %} {{ thread.title }}

{{ thread.posts[0].body_html|safe|striptags|truncate(155, False, '...', 0) }}

{% call m_user_link(thread.creator) %} {{ m_user_photo(thread.creator, size=40) }} {%- endcall %} {% for p in thread.get_frequent_posters(5) %} {% call m_user_link(p) %} {%- endcall %} {% endfor %}
{{ thread_length-1 }} {% if nb_viewed_posts[thread] %} +{{ nb_viewed_posts[thread] }} {% endif %}
{{ nb_viewers[thread] }}
{{ nb_viewed_times[thread] or 0 }}
{{ activity_time_format(thread.last_post_at) }}
{% endmacro %} PK!m++?abilian/sbe/apps/forum/templates/forum/mail/new_message.fr.html {%- set thread=post.thread %} {%- set community=thread.community %} {%- if SBE_FORUM_REPLY_BY_MAIL %}

{{ MAIL_REPLY_MARKER }}

{%- endif %}

{{ post.creator }} a posté un nouveau message dans la communauté {{ community.name }}.


{{ post.title }}

{{ post.body_html|safe }} {%- if post.attachments %}

Pièce(s) jointe(s)

    {%- for attachment in post.attachments %}
  • {{ attachment.name }} ({{ attachment.content_length|filesize }})
  • {%- endfor %}
{%- endif %}

Vous recevez ce message car vous êtes membres de la communauté {{ community.name }}

PK!AbV>abilian/sbe/apps/forum/templates/forum/mail/new_message.fr.txt{%- set thread=post.thread %} {%- set community=thread.community %} {%- if SBE_FORUM_REPLY_BY_MAIL%} {{ MAIL_REPLY_MARKER }} {%- endif %} {{ post.creator }} a posté un nouveau message dans la communauté {{ community.name }}: {{ post.title }}: ({{ url_for(post, _external=True) }}) « {%- set text_lines = post.body_html|safe %} {%- for line in text_lines.split('
') %} {{ line | striptags }} {%- endfor %} » {%- if post.attachments %} Pièce(s) jointe(s): {%- for attachment in post.attachments %} * {{ attachment.name }} ({{ attachment.content_length|filesize|striptags }}) ({{ url_for(attachment, _external=True) }}) {%- endfor %} {%- endif %} Vous recevez ce message car vous êtes membres de la communauté {{ community.name }}. PK!9'<abilian/sbe/apps/forum/templates/forum/mail/new_message.html {%- set thread=post.thread %} {%- set community=thread.community %} {%- if SBE_FORUM_REPLY_BY_MAIL %}

{{ MAIL_REPLY_MARKER }}

{%- endif %}

{{ post.creator }} posted in the {{ community.name }} community.


{{ post.title }}

{{ post.body_html|safe }} {%- if post.attachments %}

Attachment(s)

    {%- for attachment in post.attachments %}
  • {{ attachment.name }} ({{ attachment.content_length|filesize }})
  • {%- endfor %}
{%- endif %}

You received this message because you are a member of the {{ community.name }} community

PK!Zmog;abilian/sbe/apps/forum/templates/forum/mail/new_message.txt{%- set thread=post.thread %} {%- set community=thread.community %} {%- if SBE_FORUM_REPLY_BY_MAIL%} {{ MAIL_REPLY_MARKER }} {%- endif %} {{ post.creator }} posted in the {{ community.name }} community: {{ post.title }}: ({{ url_for(post, _external=True) }}) « {%- set text_lines = post.body_html|safe %} {%- for line in text_lines.split('
') %} {{ line | striptags }} {%- endfor %} » {%- if post.attachments %} Attachment(s): {%- for attachment in post.attachments %} * {{ attachment.name }} ({{ attachment.content_length|filesize|striptags }}) ({{ url_for(attachment, _external=True) }}) {%- endfor %} {%- endif %} You received this message because you are a member of the {{ community.name }} community. PK!K$((2abilian/sbe/apps/forum/templates/forum/thread.html{% extends "community/_base.html" %} {%- from "macros/user.html" import m_user_link, m_user_photo %} {%- from "macros/box.html" import m_box_content, m_box_menu -%} {%- from "macros/form.html" import m_field -%} {%- from "community/macros.html" import viewers_snapshot -%} {%- from "community/macros.html" import show_all_viewers -%} {%- block content %} {% call m_box_content() %} {# TODO #}

{{ _("Back to conversation list") }}


{%- set post = thread.posts[0] %}
{% call m_user_link(thread.creator) %} {{ m_user_photo(thread.creator, size=40) }} {% endcall %}

{{ thread.title }}

{{ _("Posted by") }} {{ m_user_link(thread.creator) }} {{ thread.created_at | age(date_threshold='day') }} {%- if is_closed %} {%- endif %} {{ m_post_edit_link(post) }}

{{ m_post_content(thread.posts[0]) }}

  • {{ _('created') }}
    {% call m_user_link(thread.creator) %} {{ m_user_photo(thread.creator, size=22) }} {% endcall %} {{ activity_time_format(thread.created_at) }}
  • {% set replies = thread.posts %} {% if replies|length > 1 %}
  • {{ _('last reply') }}
    {% call m_user_link(replies[-1].creator) %} {{ m_user_photo(replies[-1].creator, size=22) }} {% endcall %} {{ activity_time_format(replies[-1].created_at) }}
  • {% endif %}
  • {{ thread.posts|length-1 }}
    {{ _('replies') }}
  • {% if views[thread] %} {{ views[thread] }} {% else %} 0 {% endif %}
    {{ _('views') }}
  • {{ participants|length }}
    {{ _('participants') }}
  • {% for user in thread.get_frequent_posters(4) %} {% call m_user_link(user) %} {{ m_user_photo(user, size=32) }} {% endcall %} {% endfor %}
{% set frequent_posters = thread.get_frequent_posters(6) %} {% if is_manager or frequent_posters %} {% endif %}
{% if frequent_posters %}

{{ _('Frequent Posters') }}

{% for user in frequent_posters %} {% call m_user_link(user) %} {{ m_user_photo(user, size=32) }} {% endcall %} {% endfor %} {% endif %} {% if frequent_posters and is_manager %}
{% endif %} {% if is_manager %} {{ show_all_viewers(viewers) }} {% endif %}
{%- if thread.posts|length > 1 %}
{%- endif %}
    {%- for post in thread.posts[1:] %}
  • {{ m_post(post) }}
  • {%- endfor %}
{%- if not is_closed %}

{% call m_user_link(current_user) %} {{ m_user_photo(current_user, size=40) }} {% endcall %}

{{ _("Post a comment") }}

{{ form.csrf_token }} {{ m_field(form.message, class_="resizeable-vertical", rows=10) }} {{ m_field(form.attachments) }} {%- if g.community.type == 'participative' or is_manager %} {{ m_field(form.send_by_email) }} {%- endif %}
{%- endif %}
{% endcall %} {%- endblock %} {# macros #} {%- macro m_post_edit_link(post) %} {%- if not is_closed and (current_user == post.owner or g.is_manager) %} {{ _('Edit') }} {%- endif %} {%- endmacro %} {%- macro m_post_content(post) %}
{{ post.body_html|safe }}

{%- if post.attachments %}
    {%- for attachment in post.attachments %}
  • {{ attachment.name }} ({{ attachment.content_length | filesize }})
  • {%- endfor %}
{%- endif %} {%- for entry in post.history %}
{%- set date = entry.date|datetimeparse|age(date_threshold='day') %} {{ _('edited by %(user)s - %(date)s', user=entry.user, date=date) }} {%- if entry.reason %}{{ entry.reason }}{%- endif %}
{%- endfor %} {%- endmacro %} {%- macro m_post(post) %}
{% call m_user_link(post.creator) %} {{ m_user_photo(post.creator, size=40) }} {% endcall %}
{% call m_user_link(post.creator) %} {{ post.creator }} {% endcall %} {{ post.created_at | age(date_threshold='day') }} {{ m_post_edit_link(post) }}


{{ m_post_content(post) }}

{%- endmacro %} {%- block sidebar %} {%- call m_box_menu() %} {%- endcall %} {%- endblock %} {%- block modals %} {%- endblock %} PK![= >abilian/sbe/apps/forum/templates/forum/thread_attachments.html{% extends "community/_base.html" %} {%- from "macros/user.html" import m_user_link, m_user_photo %} {%- from "macros/box.html" import m_box_content, m_box_menu -%} {%- from "forum/_macros.html" import m_postattachments -%} {%- block content %} {# TODO #}

{{ _("Back to conversation list") }}

{% call m_user_link(thread.creator, css="pull-left") %} {{ m_user_photo(thread.creator, size=55) }} {% endcall %}
{{ m_user_link(thread.creator) }}
{{ thread.created_at | age(date_threshold='day') }}

{{ thread.title }}

{%- if thread.posts|length > 1 %}
{%- for post in thread.posts %} {{ m_postattachments(post) }} {%- endfor %}
{%- endif %} {%- endblock %} {%- block sidebar %} {%- call m_box_menu() %} {%- endcall %} {%- endblock %} {%- block modals %} {%- endblock %} PK! 9abilian/sbe/apps/forum/templates/forum/thread_create.html{% extends base_template %} {% from "macros/box.html" import m_box_content with context %} {% from "forum/_macros.html" import forum_menu %} {%- macro m_field(field, horizontal=False, label_width=2, field_width=8) %} {%- set is_hidden = field.is_hidden or field.type in ('CSRFTokenField', 'HiddenField') %} {%- set with_label = kwargs.pop('with_label', not is_hidden) %} {%- set placeholder = '' %} {%- if not with_label %} {%- set placeholder = field.label.text %} {%- endif %} {%- set css_class = kwargs.pop('class_', '') %} {%- set standard_field = field.type not in ('CSRFTokenField',) %} {%- if standard_field and field.type not in ('BooleanField',) %} {%- set css_class = 'form-control ' + css_class %} {%- endif %} {%- if field.flags.required %} {%- set css_class = css_class + ' required' %} {%- endif %} {%- if is_hidden %} {{ field(**kwargs) }} {%- else %}
{%- if with_label and not is_hidden %} {%- endif %} {%- if horizontal and standard_field %}
{%- endif %} {%- if field.type == 'CSRFTokenField' %} {{ field(**kwargs) }} {%- elif field.type == 'BooleanField' %} {%- else %} {%- if field.type == 'FileField' %} {%- set css_class = css_class + ' input-file' %} {%- endif %} {{ field(class_=css_class, placeholder=placeholder, **kwargs)|safe }} {%- endif %} {%- if field.errors and not field.form %} {%- if 'FieldList' in field.type %} {% for error in field.errors %} {% if 'dict' in error.__class__.__name__ %} {% if not loop.first %}
{% endif %} {% for key, val in error.items() %} {{ field._field_nameTolabel[key] }} : {{ val|join(', ') }} {% endfor %} {% else %} {{ error |safe }} {% endif %} {% endfor %}
{%- else %} {{ field.errors|join('
'|safe) }}
{%- endif %} {%- endif %} {%- if field.description %} {{ field.description|safe }} {%- endif %} {%- if standard_field and horizontal %}
{# when horizontal: close input div; else closes div.col-xs-12 before label #} {%- endif %}
{# form-group #} {%- endif %} {%- endmacro %} {%- macro m_form(form, fields=(), action="", method="POST", enctype="multipart/form-data", horizontal=False, id=None, role="form", label_width=2, field_width=6) %}
{%- for field in (fields or form) %} {{ m_field(field, horizontal=horizontal, label_width=label_width, field_width=field_width) }} {%- endfor %}
{%- for button in g.view.buttons %} {{ button.render() }} {%- endfor %}
{%- endmacro %} {% block content %} {%- block before_form %} {%- endblock %} {%- call m_box_content(view.title) %} {%- block sidebar %} {%- endblock %} {{ m_form(form, horizontal=True, action=request.path, id="edit-form") }} {%- endcall %} {%- block after_form %} {%- endblock %} {% endblock %} PK!e(abilian/sbe/apps/forum/tests/__init__.py# coding=utf-8 PK!:+/abilian/sbe/apps/forum/tests/data/defects.emailContent-Type: multipart/mixed; boundary="===============**1787111805686484372==" MIME-Version: 1.0 BREAK THE PARSER!!! Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFiJCdczKfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply _____Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

_____Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!# dž4abilian/sbe/apps/forum/tests/data/notification.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: user_1@example.com Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Reply-To: test+IjMvNC82LzEi.3VKQSnxS7tzNgLrpNzryheTjj2w@testcase.app.tld Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit _____Write above this line to post____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

_____Write above this line to post____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!_-abilian/sbe/apps/forum/tests/data/reply.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFiJCdczKfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply _____Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

_____Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!\ t6abilian/sbe/apps/forum/tests/data/reply_error_to.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply _____Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

_____Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!-7abilian/sbe/apps/forum/tests/data/reply_no_marker.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFiJCdczKfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply ___Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

__Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!ՓS19abilian/sbe/apps/forum/tests/data/reply_no_textpart.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFiJCdczKfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: other/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply _____Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: other/html; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

_____Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!FQ!Aabilian/sbe/apps/forum/tests/data/reply_nocharset_specified.emailContent-Type: multipart/mixed; boundary="===============1787111805686484372==" MIME-Version: 1.0 Subject: [My Community] Brand new thread From: test@testcase.app.tld To: test+IjMvNC81LzEi.jwBnvOMFiJCdczKfu3-1DS-k2zs@testcase.app.tld Date: Tue, 10 Mar 2015 18:33:57 +0100 Message-ID: <20150310173357.5815.25177@yvon-VirtualBox> Sender: test@testcase.app.tld --===============1787111805686484372== Content-Type: multipart/alternative; boundary="===============7630008712986776541==" MIME-Version: 1.0 --===============7630008712986776541== Content-Type: text/plain; MIME-Version: 1.0 Content-Transfer-Encoding: 8bit coucou here is a reply _____Write above this line to post_____ SYSTEM a posté un nouveau message dans la communauté My Community: Brand new thread: (http://localhost/communities/my-community/forum/4/#post_6) « my cherished post » Vous recevez ce message car vous êtes membres de la communauté My Community. --===============7630008712986776541== Content-Type: text/html; MIME-Version: 1.0 Content-Transfer-Encoding: 8bit

coucou here is a reply

_____Write above this line to post_____

SYSTEM a posté un nouveau message dans la communauté My Community.


Brand new thread

my cherished post

Vous recevez ce message car vous êtes membres de la communauté My Community

--===============7630008712986776541==-- --===============1787111805686484372==-- PK!<77(abilian/sbe/apps/forum/tests/test_web.py# coding=utf-8 """""" from __future__ import absolute_import, division, print_function, \ unicode_literals from datetime import datetime, timedelta from abilian.testing.util import client_login from flask import url_for from flask_login import login_user from mock import Mock, patch from pytest import mark from six import text_type from abilian.sbe.apps.communities.models import MANAGER, MEMBER from abilian.sbe.apps.forum.tests.util import get_string_from_file from ..commands import inject_email from ..models import Post, Thread from ..tasks import build_reply_email_address, extract_email_destination def test_posts_ordering(db, community1): thread = Thread(community=community1, title="test ordering") db.session.add(thread) t1 = datetime(2014, 6, 20, 15, 0, 0) p1 = Post(thread=thread, body_html="post 1", created_at=t1) t2 = datetime(2014, 6, 20, 15, 1, 0) p2 = Post(thread=thread, body_html="post 2", created_at=t2) db.session.flush() p1_id, p2_id = p1.id, p2.id assert [p.id for p in thread.posts] == [p1_id, p2_id] # set post1 created after post2 t1 = t1 + timedelta(minutes=2) p1.created_at = t1 db.session.flush() db.session.expire(thread) # force thread.posts refreshed from DB assert [p.id for p in thread.posts] == [p2_id, p1_id] def test_thread_indexed(app, db, community1, community2, req_ctx): index_svc = app.services["indexing"] index_svc.start() security_svc = app.services["security"] security_svc.start() thread1 = Thread(title="Community 1", community=community1) db.session.add(thread1) thread2 = Thread(title="Community 2: other", community=community2) db.session.add(thread2) db.session.commit() index_svc = app.services["indexing"] obj_types = (Thread.entity_type,) login_user(community1.test_user) res = index_svc.search("community", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == thread1.object_key login_user(community2.test_user) res = index_svc.search("community", object_types=obj_types) assert len(res) == 1 hit = res[0] assert hit["object_key"] == thread2.object_key def test_forum_home(client, community1, login_admin, req_ctx): response = client.get(url_for("forum.index", community_id=community1.slug)) assert response.status_code == 200 @mark.skip # TODO: fixme later def test_create_thread_informative(app, db, client, community1, req_ctx): """Test with 'informative' community. No mail sent, unless user is MANAGER """ user = community1.test_user assert community1.type == "informative" community1.set_membership(user, MEMBER) db.session.commit() title = "Brand new thread" content = "shiny thread message" url = url_for("forum.new_thread", community_id=community1.slug) data = {"title": title, "message": content} data["__action"] = "create" mail = app.extensions["mail"] with client_login(client, user): with mail.record_messages() as outbox: data["send_by_email"] = "y" # actually should not be in html form response = client.post(url, data=data) assert response.status_code == 302 assert len(outbox) == 0 community1.set_membership(user, MANAGER) db.session.commit() with mail.record_messages() as outbox: data["send_by_email"] = "y" # should be in html form response = client.post(url, data=data) assert response.status_code == 302 assert len(outbox) == 1 with mail.record_messages() as outbox: del data["send_by_email"] response = client.post(url, data=data) assert response.status_code == 302 assert len(outbox) == 0 def test_build_reply_email_address(app, req_ctx): app.config["MAIL_ADDRESS_TAG_CHAR"] = "+" post = Mock() post.id = 2 post.thread_id = 3 member = Mock() member.id = 4 result = build_reply_email_address("test", post, member, "example.com") expected = "test+P-en-3-4-a8f33983311589176c711111dc38d94d@example.com" assert result == expected def test_extract_mail_destination(app, req_ctx): app.config["MAIL_ADDRESS_TAG_CHAR"] = "+" # app.config['MAIL_SENDER'] = 'test@testcase.app.tld' test_address = "test+P-en-3-4-a8f33983311589176c711111dc38d94d@example.com" infos = extract_email_destination(test_address) assert infos == ("en", "3", "4") def test_create_thread_and_post(community1, client, app, db, req_ctx): community = community1 user = community.test_user # activate email reply app.config["SBE_FORUM_REPLY_BY_MAIL"] = True app.config["MAIL_ADDRESS_TAG_CHAR"] = "+" # create a new user, add him/her to the current community as a MANAGER community.set_membership(user, MANAGER) db.session.commit() client_login(client, user) mail = app.extensions["mail"] with mail.record_messages() as outbox: title = "Brand new thread" content = "shiny thread message" url = url_for("forum.new_thread", community_id=community.slug) data = {"title": title, "message": content} data["__action"] = "create" data["send_by_email"] = "y" response = client.post(url, data=data) assert response.status_code == 302 # extract the thread_id from the redirection url in response threadid = response.location.rsplit("/", 2)[1] # retrieve the new thread, make sur it has the message url = url_for( "forum.thread", thread_id=threadid, community_id=community.slug, title=title ) response = client.get(url) assert response.status_code == 200 assert content in response.get_data(as_text=True) # check the email was sent with the new thread assert len(outbox) == 1 assert outbox[0].subject == "[My Community] Brand new thread" # reset the outbox for checking threadpost email with mail.record_messages() as outbox: content = data["message"] = "my cherished post" del data["title"] response = client.post(url, data=data) assert response.status_code == 302 # retrieve the new thread, make sur it has the message url = url_for( "forum.thread", thread_id=threadid, community_id=community.slug, title=title ) response = client.get(url) assert response.status_code == 200 assert content in response.get_data(as_text=True) # check the email was sent with the new threadpost assert len(outbox) == 1 expected = "[My Community] Brand new thread" assert text_type(outbox[0].subject) == expected @patch("fileinput.input") @patch("abilian.sbe.apps.forum.commands.process_email") def test_parse_forum_email(mock_process_email, mock_email): """No processing is tested only parsing into a email.message and verifying inject_email() logic.""" # first load a test email returned by the mock_email mock_email.return_value = get_string_from_file("notification.email") # test the parsing function inject_email() # assert the email is read assert mock_email.called # assert a call on the celery task was made implying a message creation assert mock_process_email.delay.called ## mock_email.reset_mock() mock_process_email.delay.reset_mock() assert not mock_email.called assert not mock_process_email.delay.called mock_email.return_value = get_string_from_file("defects.email") inject_email() assert mock_email.called assert not mock_process_email.delay.called PK! *abilian/sbe/apps/forum/tests/unit_tests.py# coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals import pytest from pytest import raises from abilian.sbe.apps.forum.tests.util import get_email_message_from_file from ..models import Post, Thread, ThreadClosedError from ..tasks import process def test_create_post(): thread = Thread(title="Test thread") post = thread.create_post() assert post in thread.posts assert post.name == "Test thread" thread.title = "new title" assert thread.name == "new title" assert post.name == "new title" def test_closed_property(): thread = Thread(title="Test Thread") assert thread.closed is False thread.closed = True assert thread.closed is True thread.closed = 0 assert thread.closed is False thread.closed = 1 assert thread.closed is True assert thread.meta["abilian.sbe.forum"]["closed"] is True def test_thread_closed_guard(): thread = Thread(title="Test Thread") thread.create_post() thread.closed = True with pytest.raises(ThreadClosedError): thread.create_post() p = Post(body_html="ok") with pytest.raises(ThreadClosedError): p.thread = thread thread.closed = False p.thread = thread assert len(thread.posts) == 2 thread.closed = True with pytest.raises(ThreadClosedError): del thread.posts[0] assert len(thread.posts) == 2 with pytest.raises(ThreadClosedError): # actually thread.posts will be replaced by `[]` and we can't prevent # this, but exception has been raised thread.posts = [] thread.closed = False thread.posts = [p] thread.closed = True assert thread.posts == [p] assert p.thread is thread with pytest.raises(ThreadClosedError): p.thread = None def test_change_thread_copy_name(): thread = Thread(title="thread 1") thread2 = Thread(title="thread 2") post = Post(thread=thread, body_html="post content") assert post.name == thread.name post.thread = thread2 assert post.name == thread2.name def test_task_process_email(): """Test the process_email function.""" marker = "_____Write above this line to post_____" message = get_email_message_from_file("reply.email") newpost = process(message, marker)[0] assert newpost message = get_email_message_from_file("reply_nocharset_specified.email") newpost = process(message, marker)[0] assert newpost message = get_email_message_from_file("reply_no_marker.email") with raises(LookupError): process(message, marker) # dubious check message = get_email_message_from_file("reply_no_textpart.email") with raises(LookupError): process(message, marker) PK!d($abilian/sbe/apps/forum/tests/util.py# coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals from email.feedparser import FeedParser from pathlib import Path def get_string_from_file(filename="notification.email"): """Load a test email, return as string.""" filepath = Path(__file__).parent / "data" / filename with filepath.open("rt", encoding="utf-8") as email_file: email_string = email_file.read() return email_string def get_email_message_from_file(filename="notification.email"): """Load a mail and parse it into a email.message.""" email_string = get_string_from_file(filename) parser = FeedParser() parser.feed(email_string) email_message = parser.close() return email_message PK!,\EEabilian/sbe/apps/forum/views.py# coding=utf-8 """Forum views.""" from __future__ import absolute_import, print_function, unicode_literals from collections import Counter from datetime import date, datetime, timedelta from itertools import groupby import sqlalchemy as sa from abilian.core.util import utc_dt from abilian.i18n import _, _l from abilian.services.viewtracker import viewtracker from abilian.web import url_for, views from abilian.web.action import ButtonAction, Endpoint from abilian.web.nav import BreadcrumbItem from abilian.web.views import default_view from flask import current_app, flash, g, make_response, render_template, \ request from flask_babel import format_date from flask_login import current_user from six import text_type from six.moves.urllib.parse import quote from sqlalchemy.orm import joinedload from werkzeug.exceptions import BadRequest, NotFound from abilian.sbe.apps.communities.security import is_manager from ..communities.blueprint import Blueprint from ..communities.common import activity_time_format, object_viewers from ..communities.views import default_view_kw from .forms import PostEditForm, PostForm, ThreadForm from .models import Post, PostAttachment, Thread from .tasks import send_post_by_email # TODO: move to config MAX_THREADS = 30 forum = Blueprint("forum", __name__, url_prefix="/forum", template_folder="templates") route = forum.route def post_kw_view_func(kw, obj, obj_type, obj_id, **kwargs): """kwargs for Post default view.""" kw = default_view_kw(kw, obj.thread, obj_type, obj_id, **kwargs) kw["thread_id"] = obj.thread_id kw["_anchor"] = "post_{:d}".format(obj.id) return kw @forum.url_value_preprocessor def init_forum_values(endpoint, values): g.current_tab = "forum" g.breadcrumb.append( BreadcrumbItem( label=_l("Conversations"), url=Endpoint("forum.index", community_id=g.community.slug), ) ) def get_nb_viewers(entities): if entities: views = viewtracker.get_views(entities=entities) threads = [ thread.entity for thread in views if thread.user in g.community.members and thread.user != thread.entity.creator ] return Counter(threads) def get_viewed_posts(entities): if not entities: return views = viewtracker.get_views(entities=entities, user=current_user) all_hits = viewtracker.get_hits(views=views) nb_viewed_posts = {} for view in views: related_hits = [hit for hit in all_hits if hit.view_id == view.id] entity = view.entity if entity in entities: cutoff = related_hits[-1].viewed_at nb_viewed_posts[entity] = len( [post for post in entity.posts if post.created_at > cutoff] ) never_viewed = set(entities) - {view.entity for view in views} for entity in never_viewed: nb_viewed_posts[entity] = len(entity.posts) - 1 return nb_viewed_posts def get_viewed_times(entities): if entities: views = viewtracker.get_views(entities=entities) views = [ view for view in views if view.user != view.entity.creator and view.user in g.community.members ] all_hits = viewtracker.get_hits(views=views) views_id = [view.view_id for view in all_hits] viewed_times = Counter(views_id) entity_viewed_times = {} for view in views: if view.entity not in entity_viewed_times: entity_viewed_times[view.entity] = viewed_times[view.id] else: entity_viewed_times[view.entity] += viewed_times[view.id] return entity_viewed_times @route("/") @route("/") def index(filter=None): query = Thread.query.filter(Thread.community_id == g.community.id).order_by( Thread.last_post_at.desc() ) threads = query.all() has_more = False nb_viewed_times = get_viewed_times(threads) for thread in threads: thread.nb_views = nb_viewed_times.get(thread, 0) dt = None if filter == "today": dt = timedelta(days=1) elif filter == "week": dt = timedelta(days=7) elif filter == "month": dt = timedelta(days=31) elif filter == "year": dt = timedelta(days=365) elif filter: raise BadRequest() if dt: cutoff_date = datetime.utcnow() - dt threads = [thread for thread in threads if thread.created_at > cutoff_date] if dt: threads = sorted(threads, key=lambda thread: -thread.nb_views) else: has_more = query.count() > MAX_THREADS threads = query.limit(MAX_THREADS).all() nb_viewers = get_nb_viewers(threads) nb_viewed_posts = get_viewed_posts(threads) return render_template( "forum/index.html", threads=threads, has_more=has_more, nb_viewers=nb_viewers, nb_viewed_posts=nb_viewed_posts, nb_viewed_times=nb_viewed_times, activity_time_format=activity_time_format, ) def group_monthly(entities_list): # We're using Python's groupby instead of SA's group_by here # because it's easier to support both SQLite and Postgres this way. def grouper(entity): return entity.created_at.year, entity.created_at.month def format_month(year, month): month = format_date(date(year, month, 1), "MMMM").capitalize() return "{} {}".format(month, year) grouped_entities = groupby(entities_list, grouper) grouped_entities = [ (format_month(year, month), list(entities)) for (year, month), entities in grouped_entities ] return grouped_entities @route("/archives/") def archives(): all_threads = ( Thread.query.filter(Thread.community_id == g.community.id) .order_by(Thread.created_at.desc()) .all() ) grouped_threads = group_monthly(all_threads) return render_template("forum/archives.html", grouped_threads=grouped_threads) @route("/attachments/") def attachments(): all_threads = ( Thread.query.filter(Thread.community_id == g.community.id) .options(joinedload("posts")) .options(joinedload("posts.attachments")) .order_by(Thread.created_at.desc()) .all() ) posts_with_attachments = [] for thread in all_threads: for post in thread.posts: if getattr(post, "attachments", None): posts_with_attachments.append(post) posts_with_attachments.sort(key=lambda post: post.created_at) posts_with_attachments.reverse() grouped_posts = group_monthly(posts_with_attachments) return render_template("forum/attachments.html", grouped_posts=grouped_posts) class BaseThreadView(object): Model = Thread Form = ThreadForm pk = "thread_id" base_template = "community/_base.html" def can_send_by_mail(self): return g.community.type == "participative" or is_manager(user=current_user) def prepare_args(self, args, kwargs): args, kwargs = super(BaseThreadView, self).prepare_args(args, kwargs) self.send_by_email = False if not self.can_send_by_mail() and "send_by_email" in self.form: # remove from html form and avoid validation errors del self.form["send_by_email"] return args, kwargs def index_url(self): return url_for(".index", community_id=g.community.slug) def view_url(self): return url_for(self.obj) class ThreadView(BaseThreadView, views.ObjectView): methods = ["GET", "HEAD"] Form = PostForm template = "forum/thread.html" @property def template_kwargs(self): kw = super(ThreadView, self).template_kwargs kw["thread"] = self.obj kw["is_closed"] = self.obj.closed kw["is_manager"] = is_manager(user=current_user) kw["viewers"] = object_viewers(self.obj) kw["views"] = get_viewed_times([self.obj]) kw["participants"] = {post.creator for post in self.obj.posts} kw["activity_time_format"] = activity_time_format viewtracker.record_hit(entity=self.obj, user=current_user) return kw thread_view = ThreadView.as_view("thread") default_view(forum, Post, None, kw_func=post_kw_view_func)(thread_view) default_view(forum, Thread, "thread_id", kw_func=default_view_kw)(thread_view) route("//")(thread_view) route("//attachments")( ThreadView.as_view("thread_attachments", template="forum/thread_attachments.html") ) class ThreadCreate(BaseThreadView, views.ObjectCreate): base_template = "community/_forumbase.html" template = "forum/thread_create.html" POST_BUTTON = ButtonAction( "form", "create", btn_class="primary", title=_l("Post this message") ) title = _("New conversation") def init_object(self, args, kwargs): args, kwargs = super(ThreadCreate, self).init_object(args, kwargs) self.thread = self.obj return args, kwargs def before_populate_obj(self): del self.form["attachments"] self.message_body = self.form.message.data del self.form["message"] if "send_by_email" in self.form: self.send_by_email = ( self.can_send_by_mail() and self.form.send_by_email.data ) del self.form["send_by_email"] def after_populate_obj(self): if self.thread.community is None: self.thread.community = g.community._model self.post = self.thread.create_post(body_html=self.message_body) obj_meta = self.post.meta.setdefault("abilian.sbe.forum", {}) obj_meta["origin"] = "web" obj_meta["send_by_email"] = self.send_by_email session = sa.orm.object_session(self.thread) uploads = current_app.extensions["uploads"] for handle in request.form.getlist("attachments"): fileobj = uploads.get_file(current_user, handle) if fileobj is None: continue meta = uploads.get_metadata(current_user, handle) name = meta.get("filename", handle) mimetype = meta.get("mimetype") if not isinstance(name, text_type): name = text_type(name, encoding="utf-8", errors="ignore") if not name: continue attachment = PostAttachment(name=name) attachment.post = self.post with fileobj.open("rb") as f: attachment.set_content(f.read(), mimetype) session.add(attachment) def commit_success(self): if self.send_by_email: task = send_post_by_email.delay(self.post.id) meta = self.post.meta.setdefault("abilian.sbe.forum", {}) meta["send_post_by_email_task"] = task.id self.post.meta.changed() session = sa.orm.object_session(self.post) session.commit() @property def activity_target(self): return self.thread.community def get_form_buttons(self, *args, **kwargs): return [self.POST_BUTTON, views.object.CANCEL_BUTTON] route("/new_thread/")(ThreadCreate.as_view("new_thread", view_endpoint=".thread")) class ThreadPostCreate(ThreadCreate): """Add a new post to a thread.""" methods = ["POST"] Form = PostForm Model = Post def init_object(self, args, kwargs): # we DO want to skip ThreadCreate.init_object. hence super is not based on # ThreadPostCreate args, kwargs = super(ThreadPostCreate, self).init_object(args, kwargs) thread_id = kwargs.pop(self.pk, None) self.thread = Thread.query.get(thread_id) Thread.query.filter(Thread.id == thread_id).update( {Thread.last_post_at: datetime.utcnow()} ) return args, kwargs def after_populate_obj(self): super(ThreadPostCreate, self).after_populate_obj() session = sa.orm.object_session(self.obj) session.expunge(self.obj) self.obj = self.post class ThreadViewers(ThreadView): template = "forum/thread_viewers.html" route("//")( ThreadPostCreate.as_view("thread_post", view_endpoint=".thread") ) route("//viewers")(ThreadViewers.as_view("thread_viewers")) class ThreadDelete(BaseThreadView, views.ObjectDelete): methods = ["POST"] _message_success = _('Thread "{title}" deleted.') def message_success(self): return text_type(self._message_success).format(title=self.obj.title) route("//delete")(ThreadDelete.as_view("thread_delete")) class ThreadCloseView(BaseThreadView, views.object.BaseObjectView): """Close / Re-open a thread.""" methods = ["POST"] _VALID_ACTIONS = {"close", "reopen"} CLOSED_MSG = _l("The thread is now closed for edition and new " "contributions.") REOPENED_MSG = _l( "The thread is now re-opened for edition and new " "contributions." ) def prepare_args(self, args, kwargs): args, kwargs = super(ThreadCloseView, self).prepare_args(args, kwargs) action = kwargs["action"] = request.form.get("action") if action not in self._VALID_ACTIONS: raise BadRequest("Unknown action: {!r}".format(action)) return args, kwargs def post(self, action=None): is_closed = action == "close" self.obj.closed = is_closed sa.orm.object_session(self.obj).commit() msg = self.CLOSED_MSG if is_closed else self.REOPENED_MSG flash(text_type(msg)) return self.redirect(url_for(self.obj)) route("//close")(ThreadCloseView.as_view("thread_close")) class ThreadPostEdit(BaseThreadView, views.ObjectEdit): Form = PostEditForm Model = Post pk = "object_id" def can_send_by_mail(self): # post edit: don't notify every time return False def init_object(self, args, kwargs): # we DO want to skip ThreadCreate.init_object. hence super is not based on # ThreadPostCreate args, kwargs = super(ThreadPostEdit, self).init_object(args, kwargs) thread_id = kwargs.pop("thread_id", None) self.thread = self.obj.thread assert thread_id == self.thread.id return args, kwargs def get_form_kwargs(self): kwargs = super(ThreadPostEdit, self).get_form_kwargs() kwargs["message"] = self.obj.body_html return kwargs def before_populate_obj(self): self.message_body = self.form.message.data del self.form["message"] self.reason = self.form.reason.data self.send_by_email = False if "send_by_email" in self.form: del self.form["send_by_email"] self.attachments_to_remove = self.form["attachments"].delete_files_index del self.form["attachments"] def after_populate_obj(self): session = sa.orm.object_session(self.obj) uploads = current_app.extensions["uploads"] self.obj.body_html = self.message_body obj_meta = self.obj.meta.setdefault("abilian.sbe.forum", {}) history = obj_meta.setdefault("history", []) history.append( { "user_id": current_user.id, "user": text_type(current_user), "date": utc_dt(datetime.utcnow()).isoformat(), "reason": self.form.reason.data, } ) self.obj.meta["abilian.sbe.forum"] = obj_meta # trigger change for SA attachments_to_remove = [] for idx in self.attachments_to_remove: try: idx = int(idx) except ValueError: continue if idx > len(self.obj.attachments): continue attachments_to_remove.append(self.obj.attachments[idx]) for att in attachments_to_remove: session.delete(att) for handle in request.form.getlist("attachments"): fileobj = uploads.get_file(current_user, handle) if fileobj is None: continue meta = uploads.get_metadata(current_user, handle) name = meta.get("filename", handle) mimetype = meta.get("mimetype") if not isinstance(name, text_type): name = text_type(name, encoding="utf-8", errors="ignore") if not name: continue attachment = PostAttachment(name=name, post=self.obj) with fileobj.open("rb") as f: attachment.set_content(f.read(), mimetype) session.add(attachment) route("///edit")(ThreadPostEdit.as_view("post_edit")) def attachment_kw_view_func(kw, obj, obj_type, obj_id, **kwargs): post = obj.post kw = default_view_kw(kw, post.thread, obj_type, obj_id, **kwargs) kw["thread_id"] = post.thread_id kw["post_id"] = post.id return kw @route("//posts//attachment/") @default_view(forum, PostAttachment, "attachment_id", kw_func=attachment_kw_view_func) def attachment_download(thread_id, post_id, attachment_id): thread = Thread.query.get(thread_id) post = Post.query.get(post_id) attachment = PostAttachment.query.get(attachment_id) if ( not (thread and post and attachment) or post.thread is not thread or attachment.post is not post ): raise NotFound() response = make_response(attachment.content) response.headers["content-length"] = attachment.content_length response.headers["content-type"] = attachment.content_type content_disposition = 'attachment;filename="{}"'.format( quote(attachment.name.encode("utf8")) ) response.headers["content-disposition"] = content_disposition return response PK!PYY!abilian/sbe/apps/main/__init__.py# coding=utf-8 """Register extensions as a plugin. NOTE: panels are currently loaded and registered manually. This may change in the future. """ from __future__ import absolute_import from abilian.sbe.extension import sbe def register_plugin(app): sbe.init_app(app) from .main import blueprint app.register_blueprint(blueprint) PK!abilian/sbe/apps/main/main.py# coding=utf-8 """Main views.""" from __future__ import absolute_import, print_function, unicode_literals from flask import Blueprint, render_template __all__ = () blueprint = Blueprint("main", __name__, url_prefix="") route = blueprint.route # # Basic navigation # @route("/") def home(): return render_template("index.html") # TODO # @route("/help/") # def help(): # return render_template('help.html') # PK!֚N  *abilian/sbe/apps/notifications/__init__.py# coding=utf-8 """Notifications.""" from __future__ import absolute_import, print_function, unicode_literals from abilian.sbe.extension import sbe # Constants TOKEN_SERIALIZER_NAME = "unsubscribe_sbe" def register_plugin(app): cfg = app.config.setdefault("ABILIAN_SBE", {}) cfg.setdefault("DAILY_SOCIAL_DIGEST_SUBJECT", "Des nouvelles de vos communautés") sbe.init_app(app) # TODO: Slightly confusing. Reorg? from .views import notifications, social # noqa from .tasks.social import DIGEST_TASK_NAME, DEFAULT_DIGEST_SCHEDULE CELERYBEAT_SCHEDULE = app.config.setdefault("CELERYBEAT_SCHEDULE", {}) if DIGEST_TASK_NAME not in CELERYBEAT_SCHEDULE: CELERYBEAT_SCHEDULE[DIGEST_TASK_NAME] = DEFAULT_DIGEST_SCHEDULE app.register_blueprint(notifications) PK!e0abilian/sbe/apps/notifications/tasks/__init__.py# coding=utf-8 PK!. $V"V".abilian/sbe/apps/notifications/tasks/social.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from datetime import datetime, timedelta from abilian.core.models.subjects import User from abilian.core.util import md5 from abilian.i18n import render_template_i18n from abilian.services import get_service from abilian.services.activity import ActivityEntry from abilian.services.auth.views import get_serializer from abilian.web import url_for from celery import shared_task from celery.schedules import crontab from flask import current_app from flask_mail import Message from sqlalchemy import and_, or_ from validate_email import validate_email from abilian.sbe.apps.documents.models import Document from abilian.sbe.apps.documents.repository import repository from abilian.sbe.apps.forum.models import Post, Thread from abilian.sbe.apps.wiki.models import WikiPage from .. import TOKEN_SERIALIZER_NAME DIGEST_TASK_NAME = __name__ + ".send_daily_social_digest_task" DEFAULT_DIGEST_SCHEDULE = { "task": DIGEST_TASK_NAME, "schedule": crontab(hour=10, minute=0), } # expires after 1 day - 10 minutes @shared_task(expires=85800) def send_daily_social_digest_task(): # a request_context is required when rendering templates with current_app.test_request_context("/send_daily_social_updates"): config = current_app.config if not config.get("PRODUCTION") or config.get("DEMO"): return send_daily_social_digest() def send_daily_social_digest(): for user in User.query.filter(User.can_login == True).all(): preferences = get_service("preferences") prefs = preferences.get_preferences(user) if not prefs.get("sbe:notifications:daily", False): continue # Defensive programming. if not validate_email(user.email): continue try: send_daily_social_digest_to(user) except BaseException: current_app.logger.error("Error sending daily social digest", exc_info=True) def send_daily_social_digest_to(user): """Send to a given user a daily digest of activities in its communities. Return 1 if mail sent, 0 otherwise. """ mail = current_app.extensions["mail"] message = make_message(user) if message: mail.send(message) return 1 else: return 0 def make_message(user): config = current_app.config sbe_config = config["ABILIAN_SBE"] sender = config.get("BULK_MAIL_SENDER", config["MAIL_SENDER"]) recipient = user.email subject = sbe_config["DAILY_SOCIAL_DIGEST_SUBJECT"] digests = [] happened_after = datetime.utcnow() - timedelta(days=1) list_id = '"{} daily digest" '.format( config["SITE_NAME"], config.get("SERVER_NAME", "example.com") ) base_extra_headers = { "List-Id": list_id, "List-Post": "NO", "Auto-Submitted": "auto-generated", "X-Auto-Response-Suppress": "All", "Precedence": "bulk", } for membership in user.communautes_membership: community = membership.community if not community: # TODO: should not happen but it does. Fix root cause instead. continue # create an empty digest digest = CommunityDigest(community) AE = ActivityEntry activities = ( AE.query.order_by(AE.happened_at.asc()) .filter( and_( AE.happened_at > happened_after, or_( and_( AE.target_type == community.object_type, AE.target_id == community.id, ), and_( AE.object_type == community.object_type, AE.object_id == community.id, ), ), ) ) .all() ) # fill the internal digest lists with infos # seen_entities, new_members, new_documents, updated_documents ... for activity in activities: digest.update_from_activity(activity, user) # if activities: # import ipdb; ipdb.set_trace() # save the current digest in the master digests list if not digest.is_empty(): digests.append(digest) if not digests: return None token = generate_unsubscribe_token(user) unsubscribe_url = url_for( "notifications.unsubscribe_sbe", token=token, _external=True, _scheme=config["PREFERRED_URL_SCHEME"], ) extra_headers = dict(base_extra_headers) extra_headers["List-Unsubscribe"] = "<{}>".format(unsubscribe_url) msg = Message( subject, sender=sender, recipients=[recipient], extra_headers=extra_headers ) ctx = {"digests": digests, "token": token, "unsubscribe_url": unsubscribe_url} msg.body = render_template_i18n("notifications/daily-social-digest.txt", **ctx) msg.html = render_template_i18n("notifications/daily-social-digest.html", **ctx) return msg def generate_unsubscribe_token(user): """Generates a unique unsubscription token for the specified user. :param user: The user to work with """ data = [str(user.id), md5(user.password)] return get_serializer(TOKEN_SERIALIZER_NAME).dumps(data) class CommunityDigest(object): def __init__(self, community): self.community = community self.seen_entities = set() self.new_members = [] self.new_documents = [] self.updated_documents = [] self.new_conversations = [] self.updated_conversations = {} self.new_wiki_pages = [] self.updated_wiki_pages = {} def is_empty(self): return ( not self.new_members and not self.new_documents and not self.updated_documents and not self.new_conversations and not self.updated_conversations and not self.new_wiki_pages and not self.updated_wiki_pages ) def update_from_activity(self, activity, user): actor = activity.actor obj = activity.object # TODO ? # target = activity.target if activity.verb == "join": self._update_for_join(actor) elif activity.verb == "post": self._update_for_post(actor, obj, user) elif activity.verb == "update": self._update_for_update(actor, obj, user) def _update_for_join(self, actor): self.new_members.append(actor) def _update_for_post(self, actor, obj, user): if obj is None: return if obj.id in self.seen_entities: return self.seen_entities.add(obj.id) if isinstance(obj, Document) and repository.has_access(user, obj): self.new_documents.append(obj) elif isinstance(obj, WikiPage): self.new_wiki_pages.append(obj) elif isinstance(obj, Thread): self.new_conversations.append(obj) elif isinstance(obj, Post): if obj.thread.id not in self.seen_entities: # save actor and oldest/first modified Post in thread # oldest post because Activities are ordered_by # Asc(A.happened_at) self.updated_conversations[obj.thread] = { "actors": [actor], "post": obj, } # Mark this post's Thread as seen to avoid duplicates self.seen_entities.add(obj.thread.id) elif obj.thread not in self.new_conversations: # this post's Thread has already been seen in another Activity # exclude it to avoid duplicates but save the Post's actor self.updated_conversations[obj.thread]["actors"].append(actor) def _update_for_update(self, actor, obj, user): if obj is None: return # special case for Wikipage, we want to know each updater if isinstance(obj, WikiPage): if obj in self.updated_wiki_pages: page = self.updated_wiki_pages[obj] if actor in page: page[actor] += 1 else: page[actor] = 1 else: self.updated_wiki_pages[obj] = {actor: 1} # fast return for all other objects if obj.id in self.seen_entities: return self.seen_entities.add(obj.id) # all objects here need to be accounted only once if isinstance(obj, Document) and repository.has_access(user, obj): self.updated_documents.append(obj) PK!1}Aabilian/sbe/apps/notifications/templates/notifications/_base.html {# TODO #}

{% block title %}Your title goes here{% endblock %}

{% block body %}{% endblock %}
{% block footer %} {% endblock %}
PK!*--Aabilian/sbe/apps/notifications/templates/notifications/_style.css/* /\/\/\/\/\/\/\/\/ CLIENT-SPECIFIC STYLES /\/\/\/\/\/\/\/\/ */ #outlook a { padding: 0; } /* Force Outlook to provide a "view in browser" message */ .ReadMsgBody { width: 100%; } .ExternalClass { width: 100%; } /* Force Hotmail to display emails at full width */ .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } /* Force Hotmail to display normal line spacing */ body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } /* Prevent WebKit and Windows mobile changing default text sizes */ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } /* Remove spacing between tables in Outlook 2007 and up */ img { -ms-interpolation-mode: bicubic; } /* Allow smoother rendering of resized image in Internet Explorer */ /* /\/\/\/\/\/\/\/\/ RESET STYLES /\/\/\/\/\/\/\/\/ */ body { margin: 0; padding: 0; } img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; } table { border-collapse: collapse !important; } body, #bodyTable, #bodyCell { height: 100% !important; margin: 0; padding: 0; width: 100% !important; } /* /\/\/\/\/\/\/\/\/ TEMPLATE STYLES /\/\/\/\/\/\/\/\/ */ /* ========== Page Styles ========== */ #bodyCell { padding: 20px; } #templateContainer { width: 600px; } /** * @tab Page * @section background style * @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding. * @theme page */ body, #bodyTable { background-color: #DEE0E2; } /** * @tab Page * @section background style * @tip Set the background color and top border for your email. You may want to choose colors that match your company's branding. * @theme page */ #bodyCell { border-top: 4px solid #BBBBBB; } /** * @tab Page * @section email border * @tip Set the border for your email. */ #templateContainer { border: 1px solid #BBBBBB; } /** * @tab Page * @section heading 1 * @tip Set the styling for all first-level headings in your emails. These should be the largest of your headings. * @style heading 1 */ h1 { color: #202020 !important; display: block; font-family: Helvetica; font-size: 26px; font-style: normal; font-weight: bold; line-height: 100%; letter-spacing: normal; margin-top: 0; margin-right: 0; margin-bottom: 10px; margin-left: 0; text-align: left; } /** * @tab Page * @section heading 2 * @tip Set the styling for all second-level headings in your emails. * @style heading 2 */ h2 { color: #404040 !important; display: block; font-family: Helvetica; font-size: 20px; font-style: normal; font-weight: bold; line-height: 100%; letter-spacing: normal; margin-top: 0; margin-right: 0; margin-bottom: 10px; margin-left: 0; text-align: left; } /** * @tab Page * @section heading 3 * @tip Set the styling for all third-level headings in your emails. * @style heading 3 */ h3 { color: #606060 !important; display: block; font-family: Helvetica; font-size: 16px; font-style: italic; font-weight: normal; line-height: 100%; letter-spacing: normal; margin-top: 0; margin-right: 0; margin-bottom: 10px; margin-left: 0; text-align: left; } h4 { color: #808080 !important; display: block; font-family: Helvetica; font-size: 14px; font-style: italic; font-weight: normal; line-height: 100%; letter-spacing: normal; margin-top: 0; margin-right: 0; margin-bottom: 10px; margin-left: 0; text-align: left; } /* ========== Header Styles ========== */ /** * @tab Header * @section preheader style * @tip Set the background color and bottom border for your email's preheader area. * @theme header */ #templatePreheader { background-color: #F4F4F4; border-bottom: 1px solid #CCCCCC; } /** * @tab Header * @section preheader text * @tip Set the styling for your email's preheader text. Choose a size and color that is easy to read. */ .preheaderContent { color: #808080; font-family: Helvetica; font-size: 10px; line-height: 125%; text-align: left; } /** * @tab Header * @section preheader link * @tip Set the styling for your email's preheader links. Choose a color that helps them stand out from your text. */ .preheaderContent a:link, .preheaderContent a:visited, /* Yahoo! Mail Override */ .preheaderContent a .yshortcuts /* Yahoo! Mail Override */ { color: #606060; font-weight: normal; text-decoration: underline; } /** * @tab Header * @section header style * @tip Set the background color and borders for your email's header area. * @theme header */ #templateHeader { background-color: #F4F4F4; border-top: 1px solid #FFFFFF; border-bottom: 1px solid #CCCCCC; } /** * @tab Header * @section header text * @tip Set the styling for your email's header text. Choose a size and color that is easy to read. */ .headerContent { color: #505050; font-family: Helvetica; font-size: 20px; font-weight: bold; line-height: 100%; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; vertical-align: middle; } /** * @tab Header * @section header link * @tip Set the styling for your email's header links. Choose a color that helps them stand out from your text. */ .headerContent a:link, .headerContent a:visited, /* Yahoo! Mail Override */ .headerContent a .yshortcuts /* Yahoo! Mail Override */ { color: #EB4102; font-weight: normal; text-decoration: underline; } #headerImage { height: auto; max-width: 600px; } /* ========== Body Styles ========== */ /** * @tab Body * @section body style * @tip Set the background color and borders for your email's body area. */ #templateBody { background-color: #F4F4F4; border-top: 1px solid #FFFFFF; border-bottom: 1px solid #CCCCCC; } /** * @tab Body * @section body text * @tip Set the styling for your email's main content text. Choose a size and color that is easy to read. * @theme main */ .bodyContent { color: #505050; font-family: Helvetica; font-size: 14px; line-height: 150%; padding-top: 20px; padding-right: 20px; padding-bottom: 20px; padding-left: 20px; text-align: left; } /** * @tab Body * @section body link * @tip Set the styling for your email's main content links. Choose a color that helps them stand out from your text. */ .bodyContent a:link, .bodyContent a:visited, /* Yahoo! Mail Override */ .bodyContent a .yshortcuts /* Yahoo! Mail Override */ { color: #EB4102; font-weight: normal; text-decoration: underline; } .bodyContent img { display: inline; height: auto; max-width: 560px; } /* ========== Footer Styles ========== */ /** * @tab Footer * @section footer style * @tip Set the background color and borders for your email's footer area. * @theme footer */ #templateFooter { background-color: #F4F4F4; border-top: 1px solid #FFFFFF; } /** * @tab Footer * @section footer text * @tip Set the styling for your email's footer text. Choose a size and color that is easy to read. * @theme footer */ .footerContent { color: #808080; font-family: Helvetica; font-size: 10px; line-height: 150%; padding-top: 20px; padding-right: 20px; padding-bottom: 20px; padding-left: 20px; text-align: left; } /** * @tab Footer * @section footer link * @tip Set the styling for your email's footer links. Choose a color that helps them stand out from your text. */ .footerContent a:link, .footerContent a:visited, /* Yahoo! Mail Override */ .footerContent a .yshortcuts, .footerContent a span /* Yahoo! Mail Override */ { color: #606060; font-weight: normal; text-decoration: underline; } /* /\/\/\/\/\/\/\/\/ MOBILE STYLES /\/\/\/\/\/\/\/\/ */ @media only screen and (max-width: 480px) { /* /\/\/\/\/\/\/ CLIENT-SPECIFIC MOBILE STYLES /\/\/\/\/\/\/ */ body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: none !important; } /* Prevent Webkit platforms from changing default text sizes */ body { width: 100% !important; min-width: 100% !important; } /* Prevent iOS Mail from adding padding to the body */ /* /\/\/\/\/\/\/ MOBILE RESET STYLES /\/\/\/\/\/\/ */ #bodyCell { padding: 10px !important; } /* /\/\/\/\/\/\/ MOBILE TEMPLATE STYLES /\/\/\/\/\/\/ */ /* ======== Page Styles ======== */ /** * @tab Mobile Styles * @section template width * @tip Make the template fluid for portrait or landscape view adaptability. If a fluid layout doesn't work for you, set the width to 300px instead. */ #templateContainer { max-width: 600px !important; width: 100% !important; } /** * @tab Mobile Styles * @section heading 1 * @tip Make the first-level headings larger in size for better readability on small screens. */ h1 { font-size: 24px !important; line-height: 100% !important; } /** * @tab Mobile Styles * @section heading 2 * @tip Make the second-level headings larger in size for better readability on small screens. */ h2 { font-size: 20px !important; line-height: 100% !important; } /** * @tab Mobile Styles * @section heading 3 * @tip Make the third-level headings larger in size for better readability on small screens. */ h3 { font-size: 18px !important; line-height: 100% !important; } /** * @tab Mobile Styles * @section heading 4 * @tip Make the fourth-level headings larger in size for better readability on small screens. */ h4 { font-size: 16px !important; line-height: 100% !important; } /* ======== Header Styles ======== */ #templatePreheader { display: none !important; } /* Hide the template preheader to save space */ /** * @tab Mobile Styles * @section header image * @tip Make the main header image fluid for portrait or landscape view adaptability, and set the image's original width as the max-width. If a fluid setting doesn't work, set the image width to half its original size instead. */ #headerImage { height: auto !important; max-width: 600px !important; width: 100% !important; } /** * @tab Mobile Styles * @section header text * @tip Make the header content text larger in size for better readability on small screens. We recommend a font size of at least 16px. */ .headerContent { font-size: 20px !important; line-height: 125% !important; } /* ======== Body Styles ======== */ /** * @tab Mobile Styles * @section body text * @tip Make the body content text larger in size for better readability on small screens. We recommend a font size of at least 16px. */ .bodyContent { font-size: 18px !important; line-height: 125% !important; } /* ======== Footer Styles ======== */ /** * @tab Mobile Styles * @section footer text * @tip Make the body content text larger in size for better readability on small screens. */ .footerContent { font-size: 14px !important; line-height: 115% !important; } .footerContent a { display: block !important; } /* Place footer social and utility links on their own lines, for easier access */ } PK!3mffRabilian/sbe/apps/notifications/templates/notifications/confirm-unsubscribe.fr.html

Veuillez confirmer votre demande de désabonnement des notifications

Vous semblez vouloir vous désabonner des notifications quotidiennes des communautés.

Pour confirmer cette décision, merci de bien vouloir cliquer sur le bouton ci-dessous:

PK!.55Oabilian/sbe/apps/notifications/templates/notifications/confirm-unsubscribe.html

Please confirm your request to unsubscribe from communities daily notifications

After clicking the confirm button below, you will not receive another daily notification email.

PK!Rabilian/sbe/apps/notifications/templates/notifications/daily-social-digest.fr.html{% extends "notifications/_base.html" %} {% block title %} Résumé quotidien des activités dans vos communautés {% endblock %} {% block body %} {% for digest in digests %}

Dans la communauté: {{ digest.community.name }}

{%- if digest.new_members %}

Nouveaux membres

{%- for user in digest.new_members %}

{{ user }}

{%- endfor %} {%- endif %} {%- if digest.new_documents %}

Nouveaux documents

{%- for doc in digest.new_documents %}

{{ doc }}
Posté par {{ doc.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_documents %}

Documents mis à jour

{%- for doc in digest.updated_documents %}

{{ doc }}.

{%- endfor %} {%- endif %} {%- if digest.new_conversations %}

Nouvelles conversations

{%- for thread in digest.new_conversations %}

{{ thread }}
Posté par {{ thread.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_conversations %}

Nouveaux commentaires dans les conversations

{%- for thread, info in digest.updated_conversations.items() %}

{{ thread }}
par {{ info['actors']|join(', ') }}

{%- endfor %} {%- endif %} {%- if digest.new_wiki_pages %}

Nouvelles pages de wiki

{%- for page in digest.new_wiki_pages %}

{{ page }}
par {{ page.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_wiki_pages %}

Pages de wiki mises à jour

{%- for page, actors in digest.updated_wiki_pages.items() %}

{{ page }}
par {% for actor, changes in actors.items() %}{{ actor }} {%- if changes > 1 %} (x{{ changes }}){% endif %} {%- if not loop.last %}, {% endif -%} {%- endfor %}.

{%- endfor %} {%- endif %} {% endfor %} {% endblock %} {% block footer %} Vous recevez ce mail car vous êtes abonné aux notifications de vos communautés.
Vous pouvez modifier les notifications que vous recevez en allant sur la page Préférences de l'application.
Vous pouvez également vous désabonner directement en suivant ce lien. {% endblock %} PK! :7 7 Qabilian/sbe/apps/notifications/templates/notifications/daily-social-digest.fr.txtRésumé quotidien des activités dans vos communautés: ---------------------------------------------------- {%- for digest in digests %} {%- if not loop.first %} {%- endif %} Dans la communauté: {{ digest.community.name }} {%- if digest.new_members %} Nouveaux membres: {%- for user in digest.new_members %} * {{ user }} ({{ url_for("social.user", user_id=user.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.new_documents %} Nouveaux documents: {%- for doc in digest.new_documents %} * {{ doc }} - Posté par {{ doc.creator }} ({{ url_for("documents.document_view",community_id=digest.community.slug, doc_id=doc.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.updated_documents %} Documents mis à jour: {%- for doc in digest.updated_documents %} * {{ doc }} ({{ url_for("documents.document_view", community_id=digest.community.slug, doc_id=doc.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.new_conversations %} Nouvelles conversations: {%- for thread in digest.new_conversations %} * {{ thread }} - Posté par {{ thread.creator }}. ({{ url_for("forum.thread", community_id=digest.community.slug, thread_id=thread.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {% endif %} {%- if digest.updated_conversations %} Nouveaux commentaires dans les conversations: {%- for thread, info in digest.updated_conversations.items() %} * {{ thread }} par {{ info['actors']|join(', ')}} ({{ url_for(info['post'], _external=True, _scheme=config['PREFERRED_URL_SCHEME'])}}) {%- endfor %} {%- endif %} {%- if digest.new_wiki_pages %} Nouvelles pages de wiki {%- for page in digest.new_wiki_pages %} * {{ page.title }} par {{ page.creator }}. ({{ url_for('wiki.page', community_id=digest.community.slug, title=page.title, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.updated_wiki_pages %} Pages de wiki mises à jour {%- for page, actors in digest.updated_wiki_pages.items() %} * {{ page.title }} par {% for actor, changes in actors.items() %}{{ actor}}{%if changes > 1%}(x{{changes}}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %} ({{ url_for('wiki.page', community_id=digest.community.slug, title=page.title, _external=True, _scheme=config['PREFERRED_URL_SCHEME'])}}">{{ page }}) {%- endfor %} {%- endif %} {%- endfor %} Vous recevez ce mail car vous êtes abonné aux notifications de vos communautés. Vous pouvez modifier les notifications que vous recevez en allant sur la page "Préférences" ({{ url_for("preferences.index", _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) de l'application. Vous pouvez également vous désabonner directement en suivant ce lien: {{ url_for("notifications.unsubscribe_sbe", token=token, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }} PK!KllOabilian/sbe/apps/notifications/templates/notifications/daily-social-digest.html{% extends "notifications/_base.html" %} {% block title %} Daily summary of activities in your communities {% endblock %} {% block body %} {% for digest in digests %}

In the {{ digest.community.name }} community

{%- if digest.new_members %}

New members

{%- for user in digest.new_members %}

{{ user }}

{%- endfor %} {%- endif %} {%- if digest.new_documents %}

New documents

{%- for doc in digest.new_documents %}

{{ doc }}
Posted by {{ doc.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_documents %}

Updated documents

{%- for doc in digest.updated_documents %}

{{ doc }}.

{%- endfor %} {%- endif %} {%- if digest.new_conversations %}

New conversations

{%- for thread in digest.new_conversations %}

{{ thread }}
Posted by {{ thread.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_conversations %}

New comments in conversations

{%- for thread, info in digest.updated_conversations.items() %}

{{ thread }}
by {{ info['actors']|join(', ') }}

{%- endfor %} {%- endif %} {%- if digest.new_wiki_pages %}

New wiki pages

{%- for page in digest.new_wiki_pages %}

{{ page }}
Posted by {{ page.creator }}.

{%- endfor %} {%- endif %} {%- if digest.updated_wiki_pages %}

Updated wiki pages

{%- for page, actors in digest.updated_wiki_pages.items() %}

{{ page }}
by {% for actor, changes in actors.items() %}{{ actor }} {% if changes > 1 %}(x{{ changes }}){% endif %} {%- if not loop.last %}, {% endif %} {%- endfor %}. {{ actors }}

{%- endfor %} {%- endif %} {% endfor %} {% endblock %} {% block footer %} You receive this email because you are subscribed to communities notifications.
You can modify your notifications by going to the application's Preferences page.
You can unsubscribe by following this link. {% endblock %} PK!#) Nabilian/sbe/apps/notifications/templates/notifications/daily-social-digest.txtDaily summary of activities in your communities: ---------------------------------------------------- {%- for digest in digests %} {%- if not loop.first %} {%- endif %} In the {{ digest.community.name }} community: {%- if digest.new_members %} New members: {%- for user in digest.new_members %} * {{ user }} ({{ url_for("social.user", user_id=user.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.new_documents %} New documents: {%- for doc in digest.new_documents %} * {{ doc }} - Posted by {{ doc.creator }} ({{ url_for("documents.document_view",community_id=digest.community.slug, doc_id=doc.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.updated_documents %} Updated documents: {%- for doc in digest.updated_documents %} * {{ doc }} ({{ url_for("documents.document_view", community_id=digest.community.slug, doc_id=doc.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.new_conversations %} New conversations: {%- for thread in digest.new_conversations %} * {{ thread }} - Posted by{{ thread.creator }}. ({{ url_for("forum.thread", community_id=digest.community.slug, thread_id=thread.id, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {% endif %} {%- if digest.updated_conversations %} New comments in conversations: {%- for thread, info in digest.updated_conversations.items() %} * {{ thread }} by {{ info['actors']|join(', ')}} ({{ url_for(info['post'], _external=True, _scheme=config['PREFERRED_URL_SCHEME'])}}) {%- endfor %} {%- endif %} {%- if digest.new_wiki_pages %} New wiki {%- for page in digest.new_wiki_pages %} * {{ page.title }} Posted by {{ page.creator }}. ({{ url_for('wiki.page', community_id=digest.community.slug, title=page.title, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) {%- endfor %} {%- endif %} {%- if digest.updated_wiki_pages %} Updated wiki {%- for page, actors in digest.updated_wiki_pages.items() %} * {{ page.title }} ({{ url_for('wiki.page', community_id=digest.community.slug, title=page.title, _external=True, _scheme=config['PREFERRED_URL_SCHEME'])}}">{{ page }}) by {% for actor, changes in actors.items() %}{{ actor}}{%if changes > 1%}(x{{changes}}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %} {%- endfor %} {%- endif %} {%- endfor %} You receive this email because you subscribe to communities notifications. You can modify your notifications by going to the application's "Preferences" ({{ url_for("preferences.index", _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }}) page. You can unsubscribe by following this link: {{ url_for("notifications.unsubscribe_sbe", token=token, _external=True, _scheme=config['PREFERRED_URL_SCHEME']) }} PK!{njeeLabilian/sbe/apps/notifications/templates/notifications/invalid-token.fr.html

Jeton d'authentification invalide.

Nous sommes désolés, mais votre jeton d'authentification est invalide ou expirés.

Vous pouvez vous désabonner des notifications en vous connectant puis en allant dans le menu "Préférences" en haut à droite de l'écran.

PK!"d88Iabilian/sbe/apps/notifications/templates/notifications/invalid-token.html

Invalid authentication token.

We're sorry but you authentication token is invalid or expired.

You can unsubscribe from notifications by login in and going to the "Preferences" menu in the top right corner of the screen.

PK!NgKabilian/sbe/apps/notifications/templates/notifications/unsubscribed.fr.html

Vous avez été désabonné(e) des notifications des communautés

Désolés si ces mails vous ont importuné.

PK!Kw/Habilian/sbe/apps/notifications/templates/notifications/unsubscribed.html

You have unsubscribed from communities notifications

Sorry if those mails annoyed you.

PK!{yG'abilian/sbe/apps/notifications/tests.py# coding=utf-8 """""" from __future__ import absolute_import, print_function, unicode_literals from abilian.core.models.subjects import User from abilian.web import url_for from flask import render_template from abilian.sbe.apps.communities.models import WRITER, Community from abilian.sbe.apps.notifications.tasks.social import CommunityDigest, \ generate_unsubscribe_token def test_unsubscribe(app, client, db, app_context): user = User(email="user_1@example.com", password="abc", can_login=True) db.session.add(user) db.session.commit() preferences = app.services["preferences"] preferences.set_preferences(user, **{"sbe:notifications:daily": True}) token = generate_unsubscribe_token(user) url = url_for("notifications.unsubscribe_sbe", token=token) # Not need to login, since we're using the unsubscribe token response = client.get(url) assert response.status_code == 200 prefs = preferences.get_preferences(user) assert prefs["sbe:notifications:daily"] response = client.post(url) assert response.status_code == 200 prefs = preferences.get_preferences(user) assert not prefs["sbe:notifications:daily"] def test_mail_templates(db, app_context): # this actually tests that templates are parsed without errors, not the # rendered content user = User(email="user_1@example.com") db.session.add(user) community = Community(name="My Community") db.session.add(community) community.set_membership(user, WRITER) db.session.commit() digests = [CommunityDigest(community)] token = generate_unsubscribe_token(user) ctx = {"digests": digests, "token": token} render_template("notifications/daily-social-digest.txt", **ctx) render_template("notifications/daily-social-digest.html", **ctx) PK!VA0abilian/sbe/apps/notifications/views/__init__.py# coding=utf-8 from __future__ import absolute_import from flask import Blueprint notifications = Blueprint( "notifications", __name__, url_prefix="/notifications", template_folder="../templates", ) PK!$.abilian/sbe/apps/notifications/views/social.py# coding=utf-8 """First cut at a notification system.""" from __future__ import absolute_import, print_function, unicode_literals from abilian.core.extensions import csrf, db from abilian.core.models.subjects import User from abilian.i18n import render_template_i18n from abilian.services.auth.views import get_token_status from flask import current_app as app from flask import request from flask_login import current_user from werkzeug.exceptions import MethodNotAllowed from abilian.sbe.apps.communities.security import require_admin from abilian.sbe.apps.notifications import TOKEN_SERIALIZER_NAME from . import notifications from ..tasks.social import make_message, send_daily_social_digest_to __all__ = () route = notifications.route @require_admin @route("/debug/social/") def debug_social(): """Send a digest to current user, or user with given email. Also displays the email in the browser as a result. """ email = request.args.get("email") if email: user = User.query.filter(User.email == email).one() else: user = current_user msg = make_message(user) status = send_daily_social_digest_to(user) if status: return msg.html else: return "No message sent." @route("/unsubscribe_sbe//", methods=["GET", "POST"]) @csrf.exempt def unsubscribe_sbe(token): expired, invalid, user = get_token_status(token, TOKEN_SERIALIZER_NAME) if expired or invalid: return render_template_i18n("notifications/invalid-token.html") if request.method == "GET": return render_template_i18n( "notifications/confirm-unsubscribe.html", token=token ) elif request.method == "POST": preferences = app.services["preferences"] preferences.set_preferences(user, **{"sbe:notifications:daily": False}) db.session.commit() return render_template_i18n("notifications/unsubscribed.html", token=token) else: raise MethodNotAllowed() PK!Dcc(abilian/sbe/apps/preferences/__init__.py# coding=utf-8 """""" from __future__ import absolute_import import jinja2 from abilian.services.preferences import preferences from .panels.sbe_notifications import SbeNotificationsPanel def register_plugin(app): app.register_jinja_loaders(jinja2.PackageLoader(__name__, "templates")) preferences.register_panel(SbeNotificationsPanel(), app) PK!e/abilian/sbe/apps/preferences/panels/__init__.py# coding=utf-8 PK!(Jp 8abilian/sbe/apps/preferences/panels/sbe_notifications.py# coding=utf-8 """This panel manages user setting for email reminders related to the SBE (social netowking) app.""" from __future__ import absolute_import, print_function, unicode_literals from abilian.core.extensions import db from abilian.i18n import _, _l from abilian.services.preferences.panel import PreferencePanel from abilian.web import csrf from abilian.web.forms import Form, widgets from flask import current_app as app from flask import flash, redirect, render_template, request, url_for from werkzeug.exceptions import InternalServerError from wtforms import BooleanField class SbeNotificationsForm(Form): daily = BooleanField( label=_("Receive by email a daily digest of activities in your communities"), widget=widgets.BooleanWidget(on_off_mode=True), ) class SbeNotificationsPanel(PreferencePanel): id = "sbe_notifications" label = _l("Community notifications") def is_accessible(self): return True def get(self): # Manual security check, should be done by the framework instead. if not self.is_accessible(): raise InternalServerError() preferences = app.services["preferences"] data = {} prefs = preferences.get_preferences() for k, v in prefs.items(): if k.startswith("sbe:notifications:"): data[k[18:]] = v form = SbeNotificationsForm(formdata=None, prefix=self.id, **data) return render_template("preferences/sbe_notifications.html", form=form) @csrf.protect def post(self): # Manual security check, should be done by the framework instead. if not self.is_accessible(): raise InternalServerError() if request.form["_action"] == "cancel": return redirect(url_for(".sbe_notifications")) form = SbeNotificationsForm(request.form, prefix=self.id) if form.validate(): preferences = app.services["preferences"] for field_name, field in form._fields.items(): if field is form.csrf_token: continue key = "sbe:notifications:{}".format(field_name) value = field.data preferences.set_preferences(**{key: value}) db.session.commit() flash(_("Preferences saved."), "info") return redirect(url_for(".sbe_notifications")) else: return render_template("preferences/sbe_notifications.html", form=form) PK!ͦIabilian/sbe/apps/preferences/templates/preferences/sbe_notifications.html{% extends "preferences/_base.html" %} {% from "macros/box.html" import m_box_content %} {% from "macros/form.html" import m_field %} {% block content %} {% call m_box_content(_("Community email digests")) %}
{{ csrf.field() }} {% for field in form %} {{ m_field(field) }} {% endfor %}
{% endcall %} {% endblock %} PK!Yq%abilian/sbe/apps/preferences/tests.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from flask import url_for def test_sbe_notifications(client, login_user, req_ctx): response = client.get(url_for("preferences.sbe_notifications")) assert response.status_code == 200 PK!h#abilian/sbe/apps/social/__init__.py# coding=utf-8 """Default ("home") page for social apps.""" from __future__ import absolute_import def register_plugin(app): from .views.social import social # noqa from .views import users, groups, sidebars # noqa app.register_blueprint(social) # TODO: better config variable choice? if app.config.get("SOCIAL_REST_API"): from .restapi import restapi app.register_blueprint(restapi) PK! abilian/sbe/apps/social/forms.py# coding=utf-8 from __future__ import absolute_import, print_function, unicode_literals from abilian.web.forms import Form from abilian.web.forms import widgets as abilian_widgets from abilian.web.forms.fields import QuerySelect2Field from abilian.web.forms.filters import strip from abilian.web.forms.validators import optional, required from flask_babel import lazy_gettext as _l from wtforms import StringField, TextAreaField from wtforms_alchemy import model_form_factory from abilian.sbe.apps.communities.models import Community ModelForm = model_form_factory(Form) # type: Form class UserProfileForm(ModelForm): pass class UserProfileViewForm(UserProfileForm): communautes = QuerySelect2Field( "Communautés d'appartenance", get_label="name", view_widget=abilian_widgets.ListWidget(), query_factory=lambda: Community.query.all(), multiple=True, validators=[optional()], ) class GroupForm(Form): name = StringField( _l("Name"), filters=(strip,), validators=[required(message=_l("Name is required."))], ) description = TextAreaField(_l("Description")) PK!6| | !abilian/sbe/apps/social/models.py# coding=utf-8 """Social content items: messages aka status updates, private messages, etc.""" from __future__ import absolute_import, print_function, unicode_literals import re from abilian.core.entities import SEARCHABLE, Entity from abilian.core.extensions import db from abilian.core.models.subjects import Group, User from sqlalchemy.orm import relationship from sqlalchemy.orm.query import Query from sqlalchemy.schema import Column, ForeignKey from sqlalchemy.types import Integer, UnicodeText __all__ = ["Message", "PrivateMessage"] class MessageQuery(Query): def by_creator(self, user): return self.filter(Message.creator_id == user.id) class Message(Entity): """Message aka Status update aka Note. See: http://activitystrea.ms/head/activity-schema.html#note """ __tablename__ = "message" __indexable__ = False __editable__ = ["content"] __exportable__ = __editable__ + [ "id", "created_at", "updated_at", "creator_id", "owner_id", ] #: The content for this message. content = Column(UnicodeText(), info=SEARCHABLE | {"index_to": ("text",)}) #: Nullable: if null, then message is public. group_id = Column(Integer, ForeignKey(Group.id)) #: The group this message has been posted to. group = relationship("Group", primaryjoin=(group_id == Group.id), lazy="joined") query = db.session.query_property(MessageQuery) @property def tags(self): return re.findall(r"(?u)#([^\W]+)", self.content) # TODO: inheriting from Entity is overkill here class TagApplication(Entity): __tablename__ = "tag_application" tag = Column(UnicodeText, index=True) message_id = Column(Integer, ForeignKey("message.id")) class PrivateMessage(Entity): """Private messages are like messages, except they are private.""" __tablename__ = "private_message" __indexable__ = False __editable__ = ["content", "recipient_id"] __exportable__ = __editable__ + [ "id", "created_at", "updated_at", "creator_id", "owner_id", ] content = Column(UnicodeText, info=SEARCHABLE | {"index_to": ("text",)}) recipient_id = Column(Integer, ForeignKey(User.id), nullable=False) class Like(Entity): __tablename__ = "like" __indexable__ = False __editable__ = ["content", "message_id"] __exportable__ = __editable__ + [ "id", "created_at", "updated_at", "creator_id", "owner_id", ] content = Column(UnicodeText, info=SEARCHABLE | {"index_to": ("text",)}) message_id = Column(Integer, ForeignKey(Message.id), nullable=False) PK!R7Yrr"abilian/sbe/apps/social/restapi.py# coding=utf-8 """ NOTE: this code is a legacy from the early days of the application, and currently not used. """ from __future__ import absolute_import, print_function, unicode_literals import json from abilian.core.extensions import db from abilian.core.models.subjects import Group, User from abilian.core.util import get_params from flask import Blueprint, make_response, request from flask_login import current_user, login_required from .models import Message __all__ = ["restapi"] restapi = Blueprint("restapi", __name__, url_prefix="/api") # Util def make_json_response(obj, response_code=200): if isinstance(obj, list): obj = [x.to_dict() if hasattr(x, "to_dict") else x for x in obj] if hasattr(obj, "to_json"): response = make_response(obj.to_json(), response_code) elif hasattr(obj, "to_dict"): response = make_response(json.dumps(obj.to_dict()), response_code) else: response = make_response(json.dumps(obj), response_code) response.mimetype = "application/json" return response # # Users # # [POST] /api/users/USER_ID Create User Profile @restapi.route("/users", methods=["POST"]) @login_required def create_user(): d = get_params(User.__editable__) user = User(**d) db.session.add(user) db.session.commit() return make_json_response(user, 201) # [GET] /api/users List Users in Your Organization @restapi.route("/users") @login_required def list_users(): # l = [ u.to_dict() for u in User.query.all() ] users = list(User.query.all()) return make_json_response(users) # [GET] /api/users/USER_ID View User Profile @restapi.route("/users/") @login_required def get_user(user_id): user = User.query.get(user_id) return make_json_response(user) # [GET] /api/users/USER_ID/messages View Stream of Messages by User @restapi.route("/users//messages") @login_required def user_stream(user_id): user = User.query.get(user_id) messages = Message.query.by_creator(user).all() # messages = list(user.messages) return make_json_response(messages) # [PUT] /api/users/USER_ID Update User Profile @restapi.route("/users/", methods=["PUT"]) @login_required def update_user(user_id): user = User.query.get(user_id) d = get_params(User.__editable__) user.update(d) db.session.commit() return make_json_response(user) # [DELETE] /api/users/USER_ID Deactivate a User @restapi.route("/users/", methods=["DELETE"]) @login_required def delete_user(user_id): user = User.query.get(user_id) db.session.delete(user) db.session.commit() return make_response("", 204) # # Social graph: following # # [GET] /api/users/USER_ID/followers View Followers of User @restapi.route("/users//followers") @login_required def get_followers(user_id): user = User.query.get(user_id) followers = list(user.followers) return make_json_response(followers) # [GET] /api/users/USER_ID/followees View List of Users Being Followed @restapi.route("/users//followees") @login_required def get_followees(user_id): user = User.query.get(user_id) followees = list(user.followees) return make_json_response(followees) # [POST] /api/users/USER_ID/followers Follow a User @restapi.route("/users//followers", methods=["POST"]) @login_required def follow(user_id): user = User.query.get(user_id) current_user.follow(user) db.session.commit() return make_json_response("", 204) # [DELETE] /api/users/USER_ID/followers/CONTACT_USER_ID Unfollow a User @restapi.route( "/users//followers/", methods=["DELETE"] ) @login_required def unfollow(user_id, contact_user_id): user = User.query.get(user_id) current_user.unfollow(user) db.session.commit() return make_json_response("", 204) # # Social graph: groups # # [GET] /api/groups Listing All Groups @restapi.route("/groups") @login_required def list_groups(): groups = list(Group.query.all()) return make_json_response(groups) # [GET] /api/groups/GROUP_ID Show a Single Group @restapi.route("/groups/") @login_required def get_group(group_id): group = Group.query.get(group_id) return make_json_response(group) # [GET] /api/groups/GROUP_ID/members Listing Members of a Group @restapi.route("/groups//members") @login_required def get_group_members(group_id): group = Group.query.get(group_id) return make_json_response(group.members) # [GET] /api/group_memberships Listing Group Memberships # [POST] /api/groups Create a Group @restapi.route("/groups", methods=["POST"]) @login_required def create_group(): d = get_params(Group.__editable__) group = Group(**d) db.session.add(group) db.session.commit() return make_json_response(group, 201) # [PUT] /api/groups/GROUP_ID Updating Existing Group # [DELETE] /api/groups/GROUP_ID/archive Archiving a Group # [DELETE] /api/groups/GROUP_ID Destroy an Archived Message # # Messages # # [POST] /api/messages Creating New Messages @restapi.route("/messages", methods=["POST"]) @login_required def create_message(): d = get_params(Message.__editable__) message = Message(creator_id=current_user.id, **d) db.session.add(message) db.session.commit() return make_json_response(message, 201) # [GET] /api/messages Reading Stream Messages @restapi.route("/messages") @login_required def get_messages(): messages = list(Message.query.all()) return make_json_response(messages) # [GET] /api/messages/MESSAGE_ID Read a Single Stream Message @restapi.route("/messages/") @login_required def get_message(message_id): message = Message.query.get(message_id) return make_json_response(message) # [PUT] /api/messages/MESSAGE_ID Updating Existing Messages @restapi.route("/messages/", methods=["PUT"]) @login_required def update_message(message_id): message = Message.query.get(message_id) d = get_params(["content"]) message.update(d) db.session.commit() return make_json_response(message) # [DELETE] /api/messages/MESSAGE_ID Destroy an existing message @restapi.route("/messages/", methods=["DELETE"]) @login_required def delete_message(message_id): message = Message.query.get(message_id) db.session.delete(message) db.session.commit() return make_response("", 204) # # Likes # # TODO: use an "objects" namespace instead to make it generic? # [POST] /api/messages/MESSAGE_ID/likes Liking a Message @restapi.route("/messages//likes", methods=["POST"]) @login_required def like_message(message_id): pass # [POST] /api/comments/COMMENT_ID/likes/LIKES_ID Liking a Comment @restapi.route("/comments//likes", methods=["POST"]) @login_required def like_comment(comment_id): pass # [DELETE] /api/messages/MESSAGE_ID/likes/LIKES_ID Un-liking a Message @restapi.route("/messages//likes/", methods=["DELETE"]) @login_required def unlike_message(message_id, like_id): pass # [DELETE] /api/comments/COMMENT_ID/likes/LIKES_ID @restapi.route("/comments//likes/", methods=["DELETE"]) @login_required def unlike_comment(comment_id, like_id): pass # # Search # # [GET] /api/messages/search Searching Messages @restapi.route("/search/messages") @login_required def search_messages(): q = request.args.get("q") if not q: return make_json_response([]) messages = list(Message.search_query(q).all()) return make_json_response(messages) # [GET] /api/users/search Search Users in Your Company @restapi.route("/search/users") @login_required def search_users(): q = request.args.get("q") if not q: return make_json_response([]) users = list(User.search_query(q).all()) return make_json_response(users) # # Activity Streams # @restapi.route("/feed") @login_required def get_feed(): pass PK!8R  6abilian/sbe/apps/social/templates/social/_sidebar.html{% from "macros/box.html" import m_box, m_box_menu with context %} {% call m_box_menu() %}