From 1b111b1d2963aaa6967fd1ee3bf4bbf1d3a703dd Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 12 Aug 2013 20:00:46 +0000 Subject: [PATCH 01/59] add ability to import course (into CMS / edge) without static content, and without rewriting static links. changes xml_importer.py and import.py --- .../management/commands/import.py | 16 ++++-- .../xmodule/modulestore/xml_importer.py | 51 ++++++++++++++++--- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 46f439b055..520e36f4d2 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -2,7 +2,7 @@ Script for importing courseware from XML format """ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand, CommandError, make_option from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore @@ -14,18 +14,26 @@ class Command(BaseCommand): """ help = 'Import the specified data directory into the default ModuleStore' + option_list = BaseCommand.option_list + ( + make_option('--nostatic', + action='store_true', + help='Skip import of static content'), + ) + def handle(self, *args, **options): "Execute the command" if len(args) == 0: - raise CommandError("import requires at least one argument: [...]") + raise CommandError("import requires at least one argument: [--nostatic] [...]") data_dir = args[0] + do_import_static = not (options.get('nostatic', False)) if len(args) > 1: course_dirs = args[1:] else: course_dirs = None print("Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, - courses=course_dirs)) + courses=course_dirs, + dis=do_import_static)) import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, - static_content_store=contentstore(), verbose=True) + static_content_store=contentstore(), verbose=True, do_import_static=do_import_static) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 0b30a884be..0073863883 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -51,7 +51,10 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ content.thumbnail_location = thumbnail_location #then commit the content - static_content_store.save(content) + try: + static_content_store.save(content) + except Exception as err: + log.exception('Error importing {0}'.format(fullname_with_subpath)) #store the remapping information which will be needed to subsitute in the module data remap_dict[fullname_with_subpath] = content_loc.name @@ -64,7 +67,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor', load_error_modules=True, static_content_store=None, target_location_namespace=None, - verbose=False, draft_store=None): + verbose=False, draft_store=None, + do_import_static=True): """ Import the specified xml data_dir into the "store" modulestore, using org and course as the location org and course. @@ -76,6 +80,10 @@ def import_from_xml(store, data_dir, course_dirs=None, after import off disk. We do this remapping as a post-processing step because there's logic in the importing which expects a 'url_name' as an identifier to where things are on disk e.g. ../policies//policy.json as well as metadata keys in the policy.json. so we need to keep the original url_name during import + + do_import_static: if False, then static files are not imported into the static content store. This can be employed for courses which + have substantial unchanging static content, which is to inefficient to import every time the course is loaded. + Static content for some courses may also be served directly by nginx, instead of going through django. """ @@ -116,8 +124,17 @@ def import_from_xml(store, data_dir, course_dirs=None, course_data_path = path(data_dir) / module.data_dir course_location = module.location + log.debug('======> IMPORTING course to location {0}'.format(course_location)) + module = remap_namespace(module, target_location_namespace) + if not do_import_static: + module.lms.static_asset_path = module.data_dir # for old-style xblock where this was actually linked to kvs + module._model_data['static_asset_path'] = module.data_dir + log.debug('course static_asset_path={0}'.format(module.lms.static_asset_path)) + + log.debug('course data_dir={0}'.format(module.data_dir)) + # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - @@ -129,18 +146,36 @@ def import_from_xml(store, data_dir, course_dirs=None, {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge import_module(module, store, course_data_path, static_content_store, course_location, - target_location_namespace or course_location) + target_location_namespace or course_location, do_import_static=do_import_static) course_items.append(module) # then import all the static content - if static_content_store is not None: + if static_content_store is not None and do_import_static: _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location # first pass to find everything in /static/ import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, _namespace_rename, subpath='static', verbose=verbose) + elif verbose and not do_import_static: + log.debug('Skipping import of static content, since do_import_static={0}'.format(do_import_static)) + + # no matter what do_import_static is, import "static_import" directory + # + # This is needed because the "about" pages (eg "overview") are loaded via load_extra_content, and + # do not inherit the lms metadata from the course module, and thus do not get "static_content_store" + # properly defined. Static content referenced in those extra pages thus need to come through the + # c4x:// contentstore, unfortunately. Tell users to copy that content into the "static_import" subdir. + + simport = 'static_import' + if os.path.exists(course_data_path / simport): + _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location + + import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, + _namespace_rename, subpath=simport, verbose=verbose) + + # finally loop through all the modules for module in xml_module_store.modules[course_id].itervalues(): if module.category == 'course': @@ -156,7 +191,8 @@ def import_from_xml(store, data_dir, course_dirs=None, log.debug('importing module location {0}'.format(module.location)) import_module(module, store, course_data_path, static_content_store, course_location, - target_location_namespace if target_location_namespace else course_location) + target_location_namespace if target_location_namespace else course_location, + do_import_static=do_import_static) # now import any 'draft' items if draft_store is not None: @@ -176,7 +212,8 @@ def import_from_xml(store, data_dir, course_dirs=None, def import_module(module, store, course_data_path, static_content_store, - source_course_location, dest_course_location, allow_not_found=False): + source_course_location, dest_course_location, allow_not_found=False, + do_import_static=True): logging.debug('processing import of module {0}...'.format(module.location.url())) @@ -196,7 +233,7 @@ def import_module(module, store, course_data_path, static_content_store, else: module_data = content - if isinstance(module_data, basestring): + if isinstance(module_data, basestring) and do_import_static: # we want to convert all 'non-portable' links in the module_data (if it is a string) to # portable strings (e.g. /static/) module_data = rewrite_nonportable_content_links( From a5bb971c9b46269beb7accb9cae9a6f3c45cf791 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 12 Aug 2013 20:26:20 +0000 Subject: [PATCH 02/59] add static_asset_path metadata to course, and honor its use in link rewriting --- common/djangoapps/static_replace/__init__.py | 9 +++++---- common/djangoapps/xmodule_modifiers.py | 4 ++-- common/lib/xmodule/xmodule/modulestore/inheritance.py | 3 ++- lms/djangoapps/courseware/courses.py | 4 ++-- lms/djangoapps/courseware/module_render.py | 4 +++- lms/xmodule_namespace.py | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index 9e50d73b26..a34db1a5e0 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -90,7 +90,7 @@ def replace_course_urls(text, course_id): return re.sub(_url_replace_regex('/course/'), replace_course_url, text) -def replace_static_urls(text, data_directory, course_namespace=None): +def replace_static_urls(text, data_directory, course_namespace=None, static_asset_path=''): """ Replace /static/$stuff urls either with their correct url as generated by collectstatic, (/static/$md5_hashed_stuff) or by the course-specific content static url @@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): text: The source text to do the substitution in data_directory: The directory in which course data is stored course_namespace: The course identifier used to distinguish static content for this course in studio + static_asset_path: Path for static assets, which overrides data_directory and course_namespace, if nonempty """ def replace_static_url(match): @@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): if settings.DEBUG and finders.find(rest, True): return original # if we're running with a MongoBacked store course_namespace is not None, then use studio style urls - elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): + elif (not static_asset_path) and course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): # first look in the static file pipeline and see if we are trying to reference # a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule) if staticfiles_storage.exists(rest): @@ -127,7 +128,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): url = StaticContent.convert_legacy_static_url(rest, course_namespace) # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: - course_path = "/".join((data_directory, rest)) + course_path = "/".join((static_asset_path or data_directory, rest)) try: if staticfiles_storage.exists(rest): @@ -143,7 +144,7 @@ def replace_static_urls(text, data_directory, course_namespace=None): return "".join([quote, url, quote]) return re.sub( - _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)), + _url_replace_regex('/static/(?!{data_dir})'.format(data_dir=static_asset_path or data_directory)), replace_static_url, text ) diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index dd40b5139d..8c1a5ab0e6 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id): return _get_html -def replace_static_urls(get_html, data_dir, course_namespace=None): +def replace_static_urls(get_html, data_dir, course_namespace=None, static_asset_path=''): """ Updates the supplied module with a new get_html function that wraps the old get_html function and substitutes urls of the form /static/... @@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_namespace=None): @wraps(get_html) def _get_html(): - return static_replace.replace_static_urls(get_html(), data_dir, course_namespace) + return static_replace.replace_static_urls(get_html(), data_dir, course_namespace, static_asset_path=static_asset_path) return _get_html diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 1314c72094..2ad08b8350 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -9,7 +9,8 @@ INHERITABLE_METADATA = ( # intended to be set per-course, but can be overridden in for specific # elements. Can be a float. 'days_early_for_beta', - 'giturl' # for git edit link + 'giturl', # for git edit link + 'static_asset_path', # for static assets placed outside xcontent contentstore ) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 086f92a123..c06a2f39c4 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -82,8 +82,8 @@ def get_opt_course_with_access(user, course_id, action): def course_image_url(course): """Try to look up the image url for the course. If it's not found, log an error and return the dead link""" - if isinstance(modulestore(), XMLModuleStore): - return '/static/' + course.data_dir + "/images/course_image.jpg" + if course.lms.static_asset_path or isinstance(modulestore(), XMLModuleStore): + return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg" else: loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg') path = StaticContent.get_url_path_from_location(loc) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0a48c56f87..3a43473f00 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -348,6 +348,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_namespace=descriptor.location._replace(category=None, name=None), + static_asset_path=descriptor.lms.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, @@ -405,7 +406,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours module.get_html = replace_static_urls( _get_html, getattr(descriptor, 'data_dir', None), - course_namespace=module.location._replace(category=None, name=None) + course_namespace=module.location._replace(category=None, name=None), + static_asset_path=descriptor.lms.static_asset_path ) # Allow URLs of the form '/course/' refer to the root of multicourse directory diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index d57ad9ce52..ad3f634977 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -56,3 +56,4 @@ class LmsNamespace(Namespace): default=None, scope=Scope.settings ) + static_asset_path = String(help="Path to use for static assets - overrides Studio c4x://", scope=Scope.settings, default='') From 39b646558ac1e77e106427b0bad7932790030eaf Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:15:26 -0400 Subject: [PATCH 03/59] pep8 in inheritance.py --- common/lib/xmodule/xmodule/modulestore/inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 2ad08b8350..aeec53cc29 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -10,7 +10,7 @@ INHERITABLE_METADATA = ( # elements. Can be a float. 'days_early_for_beta', 'giturl', # for git edit link - 'static_asset_path', # for static assets placed outside xcontent contentstore + 'static_asset_path', # for static assets placed outside xcontent contentstore ) From 13bb3bf0a99cd62ab0cd270e6c3a79ac9ab930a5 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:18:04 -0400 Subject: [PATCH 04/59] pylint --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 4d9b332012..0f672b86d2 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -80,7 +80,7 @@ def import_from_xml(store, data_dir, course_dirs=None, after import off disk. We do this remapping as a post-processing step because there's logic in the importing which expects a 'url_name' as an identifier to where things are on disk e.g. ../policies//policy.json as well as metadata keys in the policy.json. so we need to keep the original url_name during import - + do_import_static: if False, then static files are not imported into the static content store. This can be employed for courses which have substantial unchanging static content, which is to inefficient to import every time the course is loaded. Static content for some courses may also be served directly by nginx, instead of going through django. @@ -162,7 +162,7 @@ def import_from_xml(store, data_dir, course_dirs=None, log.debug('Skipping import of static content, since do_import_static={0}'.format(do_import_static)) # no matter what do_import_static is, import "static_import" directory - # + # This is needed because the "about" pages (eg "overview") are loaded via load_extra_content, and # do not inherit the lms metadata from the course module, and thus do not get "static_content_store" # properly defined. Static content referenced in those extra pages thus need to come through the From 2249692f9a7168e2b60be9fe65dfb1ca3d1058d8 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:19:14 -0400 Subject: [PATCH 05/59] pylint --- cms/djangoapps/contentstore/management/commands/import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 520e36f4d2..1d77e9cb54 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" From e9ef45076d900d98bcdb0f613ca6a2df01350789 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 19 Aug 2013 10:21:10 -0400 Subject: [PATCH 06/59] pylint --- common/lib/xmodule/xmodule/modulestore/xml_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 0f672b86d2..1706de7ed4 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -54,7 +54,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ try: static_content_store.save(content) except Exception as err: - log.exception('Error importing {0}'.format(fullname_with_subpath)) + log.exception('Error importing {0}, error={1}'.format(fullname_with_subpath, err)) #store the remapping information which will be needed to subsitute in the module data remap_dict[fullname_with_subpath] = content_loc.name From 210fa112f325a96d8a207b3538d994c1375e4c63 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 08:56:03 -0400 Subject: [PATCH 07/59] modify handling of info/handouts and module_render to honor static_asset_path --- lms/djangoapps/courseware/courses.py | 5 ++++- lms/djangoapps/courseware/module_render.py | 26 +++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index eb7d690116..b63828eba5 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -196,6 +196,8 @@ def get_course_info_section(request, course, section_key): loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) + log.debug("in get_course_info, static_asset_path=%s" % course.lms.static_asset_path) + # Use an empty cache model_data_cache = ModelDataCache([], course.id, request.user) info_module = get_module( @@ -204,7 +206,8 @@ def get_course_info_section(request, course, section_key): loc, model_data_cache, course.id, - wrap_xmodule_display=False + wrap_xmodule_display=False, + static_asset_path=course.lms.static_asset_path ) html = '' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ba4654ce9c..981ccde441 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -124,7 +124,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_ def get_module(user, request, location, model_data_cache, course_id, position=None, not_found_ok=False, wrap_xmodule_display=True, - grade_bucket_type=None, depth=0): + grade_bucket_type=None, depth=0, + static_asset_path=''): """ Get an instance of the xmodule class identified by location, setting the state based on an existing StudentModule, or creating one if none @@ -141,6 +142,10 @@ def get_module(user, request, location, model_data_cache, course_id, position within module - depth : number of levels of descendents to cache when loading this module. None means cache all descendents + - static_asset_path : static asset path to use (overrides descriptor's value); needed + by get_course_info_section, because info section modules + do not have a course as the parent module, and thus do not + inherit this lms key value. Returns: xmodule instance, or None if the user does not have access to the module. If there's an error, will try to return an instance of ErrorModule @@ -152,7 +157,8 @@ def get_module(user, request, location, model_data_cache, course_id, return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, position=position, wrap_xmodule_display=wrap_xmodule_display, - grade_bucket_type=grade_bucket_type) + grade_bucket_type=grade_bucket_type, + static_asset_path=static_asset_path) except ItemNotFoundError: if not not_found_ok: log.exception("Error in get_module") @@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request): def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, - position=None, wrap_xmodule_display=True, grade_bucket_type=None): + position=None, wrap_xmodule_display=True, grade_bucket_type=None, + static_asset_path=''): """ Implements get_module, extracting out the request-specific functionality. @@ -194,12 +201,14 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, - position, wrap_xmodule_display, grade_bucket_type) + position, wrap_xmodule_display, grade_bucket_type, + static_asset_path) def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, xqueue_callback_url_prefix, - position=None, wrap_xmodule_display=True, grade_bucket_type=None): + position=None, wrap_xmodule_display=True, grade_bucket_type=None, + static_asset_path=''): """ Actually implement get_module, without requiring a request. @@ -282,7 +291,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours # inner_get_module, not the parent's callback. Add it as an argument.... return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id, track_function, make_xqueue_callback, - position, wrap_xmodule_display, grade_bucket_type) + position, wrap_xmodule_display, grade_bucket_type, + static_asset_path) def xblock_model_data(descriptor): return DbModel( @@ -349,7 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours static_replace.replace_static_urls, data_directory=getattr(descriptor, 'data_dir', None), course_id=course_id, - static_asset_path=descriptor.lms.static_asset_path, + static_asset_path=static_asset_path or descriptor.lms.static_asset_path, ), replace_course_urls=partial( static_replace.replace_course_urls, @@ -409,7 +419,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours _get_html, getattr(descriptor, 'data_dir', None), course_id=course_id, - static_asset_path=descriptor.lms.static_asset_path + static_asset_path=static_asset_path or descriptor.lms.static_asset_path ) # Allow URLs of the form '/course/' refer to the root of multicourse directory From 2fe4895e43a24e23995968fe2e673665893568ed Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 17:03:44 -0400 Subject: [PATCH 08/59] more static_asset_path handling in courses.py --- lms/djangoapps/courseware/courses.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index b63828eba5..23890ba8a7 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -156,7 +156,8 @@ def get_course_about_section(course, section_key): model_data_cache, course.id, not_found_ok=True, - wrap_xmodule_display=False + wrap_xmodule_display=False, + static_asset_path=course.lms.static_asset_path ) html = '' @@ -245,7 +246,8 @@ def get_course_syllabus_section(course, section_key): return replace_static_urls( htmlFile.read().decode('utf-8'), getattr(course, 'data_dir', None), - course_id=course.location.course_id + course_id=course.location.course_id, + static_asset_path=course.lms.static_asset_path, ) except ResourceNotFoundError: log.exception("Missing syllabus section {key} in course {url}".format( From fedfa7cab7a37049c7057ab504da7ae6a7c4f580 Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 17:57:46 -0400 Subject: [PATCH 09/59] fix tabs.py to properly handle static_asset_path --- lms/djangoapps/courseware/tabs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 38f81a476c..d7e1158e99 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -380,7 +380,8 @@ def get_static_tab_contents(request, course, tab): loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug']) model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id, request.user, modulestore().get_instance(course.id, loc), depth=0) - tab_module = get_module(request.user, request, loc, model_data_cache, course.id) + tab_module = get_module(request.user, request, loc, model_data_cache, course.id, + static_asset_path=course.lms.static_asset_path) logging.debug('course_module = {0}'.format(tab_module)) From 91bf6ad86dd4de9f9263f9bb2b2fde585a66ba2d Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 20 Aug 2013 18:11:40 -0400 Subject: [PATCH 10/59] remove extra debugging line from courses.py --- lms/djangoapps/courseware/courses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 23890ba8a7..676fdf4941 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -197,8 +197,6 @@ def get_course_info_section(request, course, section_key): loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) - log.debug("in get_course_info, static_asset_path=%s" % course.lms.static_asset_path) - # Use an empty cache model_data_cache = ModelDataCache([], course.id, request.user) info_module = get_module( From 5a7bcd7bb3f5b3ad5269b173397b1c56692575fb Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 22:43:53 -0400 Subject: [PATCH 11/59] always serialize out HTML content to a separate .html file --- .../contentstore/tests/test_contentstore.py | 25 +++++++++++++++++++ common/lib/xmodule/xmodule/html_module.py | 7 +----- common/test/data/toy/course/2012_Fall.xml | 1 + 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..282753fcf9 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1057,6 +1057,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # It should now contain empty data self.assertEquals(imported_word_cloud.data, '') + def test_html_export_roundtrip(self): + """ + Test that a course which has HTML that has style formatting is preserved in export/import + """ + module_store = modulestore('direct') + content_store = contentstore() + + import_from_xml(module_store, 'common/test/data/', ['toy']) + + location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') + + # Export the course + root_dir = path(mkdtemp_clean()) + export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip') + + # Reimport and get the video back + import_from_xml(module_store, root_dir) + + # get the sample HTML with styling information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'with_styling']) + ) + self.assertIn('

', html_module.data) + def test_course_handouts_rewrites(self): module_store = modulestore('direct') diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 567f5c7eef..726cac77f8 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -164,14 +164,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # TODO (vshnayder): make export put things in the right places. def definition_to_xml(self, resource_fs): - '''If the contents are valid xml, write them to filename.xml. Otherwise, - write just to filename.xml, and the html + ''' Write to filename.xml, and the html string to filename.html. ''' - try: - return etree.fromstring(self.data) - except etree.XMLSyntaxError: - pass # Not proper format. Write html to file, return an empty tag pathname = name_to_pathname(self.url_name) diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index ec75ef0b9d..ebbc2bb75e 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -8,6 +8,7 @@ +

Red text here

\ No newline at end of file diff --git a/common/test/data/toy/html/with_styling.xml b/common/test/data/toy/html/with_styling.xml new file mode 100644 index 0000000000..1ee6ca5c24 --- /dev/null +++ b/common/test/data/toy/html/with_styling.xml @@ -0,0 +1 @@ + \ No newline at end of file From b8a8b202a46c20a68b7a556557729fdf737fa8ef Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 22:53:03 -0400 Subject: [PATCH 13/59] update comments and some code violation drive by fixes --- common/lib/xmodule/xmodule/html_module.py | 31 +++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 726cac77f8..7a68c42ac9 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -33,11 +33,13 @@ class HtmlFields(object): class HtmlModule(HtmlFields, XModule): - js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), - resource_string(__name__, 'js/src/collapsible.coffee'), - resource_string(__name__, 'js/src/html/display.coffee') - ] - } + js = { + 'coffee': [ + resource_string(__name__, 'js/src/javascript_loader.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee'), + resource_string(__name__, 'js/src/html/display.coffee') + ] + } js_module_name = "HTMLModule" css = {'scss': [resource_string(__name__, 'css/html/display.scss')]} @@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # from .html # 'filename' in html pointers is a relative path # (not same as 'html/blah.html' when the pointer is in a directory itself) - pointer_path = "{category}/{url_path}".format(category='html', - url_path=name_to_pathname(location.name)) + pointer_path = "{category}/{url_path}".format( + category='html', + url_path=name_to_pathname(location.name) + ) base = path(pointer_path).dirname() # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) filepath = "{base}/{name}.html".format(base=base, name=filename) @@ -168,10 +172,12 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): string to filename.html. ''' - # Not proper format. Write html to file, return an empty tag + # Write html to file, return an empty tag pathname = name_to_pathname(self.url_name) - filepath = u'{category}/{pathname}.html'.format(category=self.category, - pathname=pathname) + filepath = u'{category}/{pathname}.html'.format( + category=self.category, + pathname=pathname + ) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) with resource_fs.open(filepath, 'w') as filestream: @@ -185,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): elt.set("filename", relname) return elt + class AboutFields(object): display_name = String( help="Display name for this module", @@ -197,12 +204,14 @@ class AboutFields(object): scope=Scope.content ) + class AboutModule(AboutFields, HtmlModule): """ Overriding defaults but otherwise treated as HtmlModule. """ pass + class AboutDescriptor(AboutFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located @@ -211,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor): template_dir_name = "about" module_class = AboutModule + class StaticTabFields(object): """ The overrides for Static Tabs @@ -236,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule): """ pass + class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located From d523167bddd852a69f28891d8a7ef3ba75473356 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 20 Aug 2013 23:16:39 -0400 Subject: [PATCH 14/59] add another piece of test data. just an img tag. --- cms/djangoapps/contentstore/tests/test_contentstore.py | 7 +++++++ common/test/data/toy/course/2012_Fall.xml | 1 + common/test/data/toy/html/just_img.html | 1 + common/test/data/toy/html/just_img.xml | 1 + 4 files changed, 10 insertions(+) create mode 100644 common/test/data/toy/html/just_img.html create mode 100644 common/test/data/toy/html/just_img.xml diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 282753fcf9..93f353015d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1082,6 +1082,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ) self.assertIn('

', html_module.data) + # get the sample HTML with just a simple tag information + html_module = module_store.get_instance( + 'edX/toy/2012_Fall', + Location(['i4x', 'edX', 'toy', 'html', 'just_img']) + ) + self.assertIn('', html_module.data) + def test_course_handouts_rewrites(self): module_store = modulestore('direct') diff --git a/common/test/data/toy/course/2012_Fall.xml b/common/test/data/toy/course/2012_Fall.xml index ebbc2bb75e..ce8a2399b5 100644 --- a/common/test/data/toy/course/2012_Fall.xml +++ b/common/test/data/toy/course/2012_Fall.xml @@ -9,6 +9,7 @@ +

gG6rIoyo6k4l>d(sMD_3bQ@Pzii|Majd&5f7&rK=o1zs;^8men{` z#!V8@AB?L_?z086?&W>swObgnjIkwev%LNO({j+8${9<2{7PloMx8WJBh7$_6r>S&jh|oo ztCrCvt?_Hi<@4Y9d)Z*|!T0`(El_n?vLjj&beWkXq-z0f?5@Ur6gAV=mdY$UNcQmW zbjF&W@rGL{jYd!{41H@kYAYq4kF}aLxPGcIlB*vHv?xAG*xH|4l+`A5uw7z z&J~1xeQmP5iV56f_F&ooTK@FmY_Ebryhz zr65k}Bm@;K4s8C}?1PH<4esGMcnsAZux7e}Vqtf0x9mW82m4M+5nIu6KY4~&4s6fB z)dBj_l~>_dBPb#$7o1hi>8^Itej^Zl=$c&vP!2&NQ*+aAvuj{5g;%E%w**JSx7diAOl^i1;xNho_*p6a7-1rd= z>|FlQ0;#``m&60sxSeN1-1|BOz$DQ;k1+DMys`vwPT)MC+G&r?uSb-9%z$`w@T6Sr zzs4zeoG^bnU!Jn2ytnzZtY%3EPD}eu=-hsi$up%qWUxEqaJ;{G>lbC?GR_FtSG`NV z2iT5hxh%^=^u?!M4P##VNbs+I`PGn@Qq?p>b6IMXeVS(s7#W>|0H*g(?5r5t@Uqd- z%kYfeQq)19rb|5f&Hix4-UHp%-ndbI{WrgfbGz-b^Y(k?DeWgsuudxov#fU!wUP>V zIwu!##xMo^*6;jwnK~RPfBt{`VcFkie5b`Q2tUtfcw3w)y=RZg2k*XD&KSv8-@H@i)>q5v(?=kp zTizzi?dw-Fr}$*^pgh~9F&4+*7Ko0nxgws)z2vOUEh8d#8JSmXb$M63%vLl8xJ@>1 zyEX1$`)PT`#sqg3dB~I^t@}gea{4@S9_qldyZx-(zyBc0G99VQHMeme`T~ys=+Ra* z+iu*qOb7g3=46DYqq}HPIcAeN`#5t#*PIWLtQRoz)3N#y&dOZ_ouGXtf#;!GSPy2t zEV1>7A5%gk2nvfN;C48cz98ZQMPaO}w&NL5g&A>ahdsW-NZAQR#esX6xX$+xw;8-Y zdHAqAe)b$GV708URO4Xsd~=JtQw|3bm%kpCd0en69XxpO0O^2oKiR^g$Q%RAKV)ewTaVo#)0=@cq*V9c4{R@4R&n zmr8&2Bc^fB9x}-71m`T2(ixY8(Fnm;3)GPwi3M`DdsyE2{*ORp6eDmG3ltI7=n@ux z5|(iTD-n42{_+(HGiJR)e7s&DLO#9xejeYaPXSD3HkJR}z&>{RtnA-^Sa42J?tk>S zeC9X4RTfrp%gNE49C$Ik~3A&hy}DS^F$wtEi4kRFJAdO}w- zz|3K)sHCprwi(jcyCaCN<8WR5IE_H0YLnyV8ICsQT%UnODZsfjj2$0L_+moLH9rT} zJvizvguS)LcL+v~tcdIq*cx2$&KpnK)>Sqt}4=`z9yj7w@B`W#~D0xDeL_-CZ^d&`T!!3dT?!T{?4Tu1D6 z6iIAtPg3mtOAj}#@BGPMmOJ16VwqxXEH=4_ejX6D03x1Eu`GdB^uR%h`QF`+nB#(| zYnV4KU?36uUL;+WF^=?>EBipBQ64GJP`vEMi=)djb)Gtd@xwuHygWy&ajr6t_d$fy z@)XY;vxs_n(|227-;)P8Mx~GcsfnpY=H2~r^>YjYh=?x`wU61fKSA4_plOtd53rTq zW_|;mg7EsUVo-qSj`5|XMv}3GJK{3jUr|^?ifI&Tik`R_YgM^IbKqQnMA9m zTW#^-8Pf=)5S3j=a7wwV68>H zIC(-*e?sjyBK$TmPN-{^`9uV|!M-0{_R9c4hQbGN&cn{RBm`?3h(z=hXkrcHCtyz* zY)?52!^;Vh9C46U@H3x=eqv?g`qYZEi?hX?fVW4-8=F*$u4fv?4qcCoWA42D#4*-B^7WKi^wXy8@Fw#8pe&rR_@1EwGY>VN@v3 zrnMzo?dsifdYv~yea}e6oXocc#L)iPE}|d}&btSKojR}l=##Sc>P_@Oa31DTXw{-T zS!CYV19==#rzW;p@|h`jKiMu|`(9 zyi>mQ-OrUD{ormH6s~!kKSIizB;8e#S}L7bfWz(}s6cO`}wipPk-2PZ1c*xiKIcZoKF{)x|khG^fzA=@eQ z&W$H${Oy+a-@cCsOuI0rwY!jRAzbI~Q)OitU&d&b*HEC1Db6eMG6=n36Tm~<=Pt12 zZf+JL03m0HzqY(wcFALTWeo-4Q>F+9Xws`W=nj6y9E3bZb=a3u1`A%; zG^04q|7`iBQ8R|{VGexMN`a9h-;-e-9a^Dnj1HVP2{j-I2Y|p$FYtU$fg1!r*2@ry z{yYugBuMl`N$L(@O;a1~;&bXD5KEEF+js78S2<4_PlP416?X)W#TUq}-{L-VJbmhp zf{dq7xe6MWb*51?u~{!$^5j-)^Qe|aXUh-X-YUz>m&*;b(Ml3kJOVD5smOVP;eZ5p z#s-8QoJ1mAoLMQ~`FH*~1IARj{D?W-^b^G14HQbx*s!o&Ub%IHbyEnAN}P~RuNIy@ zc=UqPpUYs+>(>AZQkY}027;QV`v9aS@^>PIkw+1O?t%Ag^)cQ5kV z|^A!ixEcp*ZSE1=mNFC7XRj2tR)Q>(|DX^x#;^R=^fd=7X+3LLt$0mf#T8U)Ia@18SZO?4 z4aJ8`4RS;5=u01NJEh_SYnx*bSf^>|hv@rp9JgVJzJ9fAF#iJQVQpjOwX4(2p~lMh zfAT^3r~mM4WgY|mGnQ^*5d=cXd7XE?=~QFPEq9{;X`Uws`RP9`0jR zDj^Dk(4G_Y_`ws>F;{avn7lMJIDvcZ;^2Mzg?7O#?X1Yx;B0Wf132gb3aN1{sWi9L zvxdHt7W5mnw$A(;<<9PAxjK*72`AIy$ho-^fq1PI)e-qS5LnkmltnKP_cqWAd(1abiY?{VWq}2G6iYn8*a!v^P?E2mjosQnYZ;j-!YRM=#U(BXof%j*;{Z^Y?@K5tiUDqC{vCG;RJ>q2*AU=w7XS&@4;=A2u>-Z zc|&y&<)YNaG6)=>umia4F_X*L3Cu2!Bwp5%0~1XvODB6vm&(JfC*{=z1{RKlJmtW! zsi{@wh6}WpJ7`2Wqz&ervv9l>6gnE-@320+ht<{r$UDg#YV(lO=@1n^`s8_eaW-8Z zp&)z0Ne(VCDUmn|9HJ3dOkF{#=Vl5CS1Y9*oX~G#OtM2|cAt+#gTKN-XP)9fC8!P( z0fXftoOXVGqx|p}_o-vQeC75^`8n&bU;ErOd|L062t)C=3U|J#I6VmhUtU=*Km7SeATbEX^VG^UmO-fFk%k9{jyT)TAdo$jR(mLvUN8vgZfy$fHjEX; znsw`EE~wVKngOECrhF@@r-E z`kgYee6yU)Lg*7XyqjJv`^@E4Zs~$Y*+r3gb8DA%G z3XV{A^%(3HH0q~)$0&yeOR~o!=E}OgRpaSkA&1xY_{tTQ-5~g5M9pLBeDBF2b6)0Z zh|6F8hySG9`PT20pZ@&q^58K(j*%KZe`TUvTbVBpS^D|jFSb~(W$EAo%3be%gdYL^-qb6b|hT3k*keNzC?+^-6oLES_5PZ>IzlIqxQvAjvC&%x_fRm zKprf?;8Tq^h-^{3glc3Xc3~EpFs-r@;!ofW|vhtJ>1MZeRKj>(+Z0CM<{}@iqcr&3g@v-vhB_TtTh32spQa2@rtMXX=SBI zr(#Qm$|qY$8BbZl>iu<@JKBK3Ar3FEluIbIc3y0AQXvcV*dD+C!M*a6kGING#Q$kD zk=N%K>=0$uLZ7Nlr~Qv(6HFUm6O3SAo?^4DCOI3{!6>4Z9h}Htq3v`i?_A?JR#CJu zioHH_{Ax!HvY{wbVKzs751qTCJ@3!6g!A;Ey#L5k^PZO9|NZ~5tY3STHR|_S!`>>N z!&}9xuU_Mz^2xIO_6EiUo)Evl^abwD01-^-@F6G>4B;s_&j+=v^#h3CaKr@ll1N3f z*4x25D7bOGB;_aC>cDPk+Z{Nj5AI1^yaMo)DbOj0l~1g##w+E>lg%=&lZB3Ul6&x! zCo>`3*WtNr%^@H2%`}Dyf;C)G)7%IlPD6g9=Q1pic{4={=wnTj)Lo$m33y2Q!1U+S zu6*o1406l6h+8k?G!p7y`e6XFrfZNKAH3A4e(-p=+}*pLRi}Hy2xMlS&BJ$!Tc3XB^<~#M2#4hv3vbzzy5mp;l~^0CqI3+ zymII34N{@x+>sdpGXk|}v z7jtL6l=TJhP+ zD%`lifm<9g`WTUWZ}bvMAO^PHv$EBjF6#^cw1NRh#=-Aau<>qgG1A%9~++~Bx2}haGkb~$oz3|UL7#3Qpl^||| zZ=NYtO~$X=qw9+zLa@GjA4C~?piu2<#O-?vtto4B8s3P=(k4#Q6Dz%I46l~j{E0V| zNBt%N*En{N#XpBNoaVgT4z!ybA@coqH_GR}zF3y8q0ZmIqaSl3t&V2kNbYH!L=1ic zR}%4iEW^w*kKE(PkU8eP<7^9aeHZ2vEwLl}wM!%Aw^)<>@q16p!$13$k$) zC3a=k7)4yWT)zJIzf>Onug@TIcKgH!GMrluOSyAakfIpH^+2t9QXnS`!abpL#8XUI z75#{y6_!N#@T2?GlR<{VveeLfOl2Q}mMgkZVG#w_%^T}cQmK%7cz+u$KEeSaeGh-i zlh|Y5yfTkL%L1ni;D?%}ttFfmjB@sp?YPC@=kXzA9DJ~Bl(ynh@Iox}75wP7CmQDh zI`O?CkDD5hAnv>#Vmi9}L5N)3szB(wiew}Z>iN~+2|JODmyCGRP93YMZ~x=JU*7oa z?egKH{qm0D2m@CJ0u$GSx!0vu(+kU+4*{5RXSs)<5N(S?dQE&1AU-)BQKvkC;7C`?(aKf7wN$N|mEe<}bbZcE z{qa4F8@_eD%rd%bJ!jVm_F7)U^gQw~2(tVG% z+EcVywig0xVdjL57?;o%uS8P#@X>C0gr?f1B>h5Ny{6?E4)=NvsvLc=bX9 z2`_68N=&NJHBnT?YhMjnEZk2o~`~VmqxBfcU;!xld63xZ~!kmOMD- z*4g+T8p$1;yKQ5h!ro^-sJSFY@gRrD>>N|0444zf11zJ!LFVDC zUtJq3i~X7M94nwc3a@$QaTE)~0y$~tEdujNiG?vI9Bhq~0FTisOK>ozD8+29afa}D zt&0$AX&?v{F_64n0xOdMWdIuAdtN^Kh3k}cwrs)4pz74YB`FOtynDKygd+?O4V$Aa z)|cJ9-}$_fgP@tVURpot$UQEAh&x936a$sc^lsg{g3n^~2@v@qT=2mLbAGudgGirs zZPZtk35_^w;p13o^?Q3*LZKYIcNbQCo3+X5^7U`tDnCU5_Tk;99oE%;78yd2K5(7m zi1>W|-UznF>LSw5BMdU5OG}j7a~5np6Zzek4Oe~kN?`CSuS2M9zGKw&^RHhkYYgl= ztSWdI^y+1F2^zN0;bB(6Rm(0o;hh5!i~Tzf1FO3VS^oUdY}JgB57LU_Y$HkU!;K-V z5Qe$&KcGAKX6v7?#%XDJts#EM)g@@J!}pF@Thyw7K93Oig9?yO_iT)mGp%5i`Nuco zZ|se6cI{cYhDFXa8`0gxa)2^scjGyP3&Efv-9!1b%iM7i)5cMjOXkp~F3^e37bdBOi6~WgaY`CG~aOPNWE_=&5j={}! zz^ws+b3;g_EaPo(5BBWl0*~exVY%oGPIJm!yN_j4#AXP`wNe#nF0;(CdEhyO|NMwi zA0k?W(@URo3(F9l=9dtyBQu1L7(xCJ2;z2ONo!?kmQ53@#)0j6>tYF#z z59pNOok(m0^PX>Zmhb5`?>wbq1S7hSF`qrV`ypkfjSM%waRQ(|5C68bhE)_aO~pAk zqAV@1Wk2*|2Bu%Uf4^+J_Yr$xnKOf5dbJ!AAHbV>b(ytH2lwu)py`Xmq7LHF9P{-~ zdrWXwQKw>F`YkfD?-?Pw?^zTGfq{I)$;G#D#xAXd<7EXKZ3!!(9dYuthICoC*TLr& z-&6!f5b~T#MP!4R+%~5pw-Xwz&!{gak^8Fr*W7?2SWPHm~;))||xqi`X93@i`Lo8ySz>NjTPznXlNK`(cK5I}g_gRnf(6YrVg^kEqOb$0j35GH6KU^`GpGOT zoy+A<-hPyWw)XLOs9$F{M5wURu30gCOfJE|Gf6wl&_)yED9g~o=g=_3XS*O{0}_LS z!x)3lENj!tOY>!Y9j!X+va=X=>%L{I;6~xZXTSV8HUM^J zgXO)#PKb|w1-?|FT#SStclK|#F}PJbf0(Kzo4oCS#im#r4s3FEB!K^*#iKp1_vW>5;WCt(-TE- z%^InZ%O#%W>ReU>3H!PS(Yx8fxYH1j8_xH(*(xSy#t)Ng%%gBEcgbj{tmE<8`!*w) zL(mi!KUFUqh7-9cx7#kbZ}0`3?r zy@y_(Evaa;EUcyPa}YUNFu9HGF80)ayyTs_1;;E$3$yo(+g(TpRF$SAP7ur_#-O!r z8~zgCXG8fGT*~_eNXaUZ_;L6!;V9bm5B~Ts%g3xPplUB?&sd@gffDIC>sm+(mN|(B zg{pZ1f#8mqQzjyGS$0JPkNOyxunEH~YZ;AE|$_k_1%;#bn(H=WvIF-9d z)WA9)mD3*XYPb#;0wF|3mtLl93WlF$B@0|1@P${5@kXt28}|)ZiwOvk{b0*cOkTz%kiOz;bn9 zdW4HDtt6s|uR(zpXKI?=YA5HN7-t}J5TTr#tkG^`_&@G+1ZBVG59weX)%!Jwdk^0OXya>?|=2g+exb+?6pJXL{>l zK27Wpk{DC>t!YiMqj9O1;sCEhOY~+ zgU>Z~N@w2Azb;&M%6V9;SYY|XpN-J{A9P;A_|YJ6D+E` zW+_JiDUUSrr~LYDl8fL21`vqD0EE{=P}MiEM0^UY9Mec8wYua0g$x4Dolg+W0SN){ z1H5rYjh?tEcWP z6G46b>EaLIbXf#eSL$;RmbWq=6`x+#-K&CO3n-g5x;ZU@gTmAY z-&^C$Fh(Delya{7wYYX^7(G?Ly-v)F0B?PAwkkl55Q=H<>n5$R}5_U%aXGjdwjJ`8$!xOQbudZN#p-&}E z2t9UEZXBf#0OfFFgxQ|(B9IFr1B+f^G5xKd;l1!^0aZW5#kWNi651uZN!@`iheacz zA7J9BOH)l5br>*a$7)xP-B;xwFE zF}!dIiVbrCIYN*tEHEk2HU9lgLa)-!w~B$0ek4#GJ9#=4fO&|{t0e%?mxwK z4iu{<`OQ~m$`@X{Uf%jQ-!EUeig{z|&%lJm*wtHC5e1P~p$? zzK2tCuVm*NFNs!hBQFjt1m_U53|>aa|9n3G5o%2zY%)PR($k> zSIQdFy%=6URy^*06EyfSBVklHGA*%6sq6U$YxqI>WKv5sT(J6Fo1+16iN&iCOlvN~ zDuMdo+t3A|Hqs0}4b`-jVUT_(XX73)S94?dB1)GJ zFtY#J8>_gtVJT=A?-ke_%Z=T;tCX_4y&0qWeRjH7R-01T*4sbVkmDs$&WeV$M;RqL zHOZP^%C#0j_SZtf8IVv8(IX;$I+Q{+) ziXqo$&Hw89rE-gwyKH4YUT6JF_{X%Pvs!ysN_5b%pU(9m6!E2NKzoMs5;CH_^2*Pz zipJ?3a;Zof-1U)sM6RSafBq*%jV5ArDTHiR_G=ynune#Go+p1QEpd0SH?INBQ@3_C z`Z;FW5KbFnF=GqSQnc79hMAlD`S<#s5)ER${E!qDysJv9+spBMZ}8hU%TdcUG-6u7 zPr1y&EAVS8CBzX5Txb4|M+&y0DQYNoX>HVF^qz$KxTD3*+VdP6A_vlq>KTR^?mJb} zdy3e=jyyWRaxf1@tac9MatEvu#TJ%R5W<%}bESOmuOF7rvQR&c8RWtKCJ2Ww4C3u= zJYz=*{uOUY}5i>vItQ9P>}dB@wDO z!k}^aMtSn*Kf-j8J*sfuWeo4-d^SKIMO2@u09u_Gj<_xwjGzq2xi&h8ID*1xw%kN3 zd4vS;FOSs zL1>SCzflC9c`V8*O0wtWKl*on3k4nusI3R(H^2BQcHf(kge2NYmeZCmtzy~4G8lIj zeOVS#5(0}O##M|Gr)41(b%%e&2CEv3psZp@!yOyOXa6An1OtygeHjOZVZ6*SjYmIZ zuwX?ZR|UaiCA1DyJ|$-Y1_OqJ1^Kozna0asFw3UORpU4;wHU)`L_~175r_5Y($&;| z!|$h6tZSPKDbC{w72x&%g*Y|cr{_9IZ@Gvvcprk0w(s7%T#}FRZ3vY`hO11HYm9;n zGuX5qVQtgLLEQw-v}fm*AueJw-_-5s6vT?21i}DOU2}Aa0)Qfjh#F%K-gFoj5tK?tH{1{KRpp@R%! zjs)w8@288{<}yHG6w;@Twvp%VUdH&rT}SWYtdLqJ`R3u*qXONFXx|VNmBFv)bFO_uZxi+aw7|W#N5y&7ZK2P!YM@BIh0ND zxx>EG5C$@@ZaQ`2DN)#rS)YU>M|@?TR)I6hfsk@+mwZIz9K8Xu@4nb9-+E)Y{KdPQ z zb*x+3>@}v~T7fC*9WmdQ5YIH7#HH#YW|{x-Z`>}sPac)`|Kxk+H$Jz_2Vo7kbA==@{!EaJhC=C6 z20BfwM<|onmn+eErrkqtDFa_Ah!(LG7B{QfB}uyaQ!~nMS9lBMb%A(!Z%tKUG)NZ< znS_I%3ftG#FqLEy!cY5N3A4vq$R#(G%F9Fau2K`vYc}d(N5x?XN~sT zuU{&E{r->2A3eEIzWntsv+aXqjxEV613u{iOw1DTC~LPUgU}liamCgB@t~c9uJqK!G0Of z?F02L66~qY5CC9UmpggaI&=k0>1Zu0QZuoTS~;0r&6Ia5Y!p9ofy2C?hZ#2!Qd*wO zgpj1`zNG+N`Y0|2MQVY9u+vCq8h%>-dF`Slbf_#Bzxl>xT;jMcJI2z*mL3oDtmg#G z>n>epIbyMlJ$hL7S#mhV1aKVVdw0kj>tGFcDRa|o8-vS^p%|ez5MkvgG$myWv5|=e z1jktGcm}OyU4UTS{W67>({F$EdU=NVPs-2mQh4p!db!08mjw`UYWbi{Vaj)m zk%x}X6v=h6rr32z$J@N_$5c{CJ6ZzPnI~q@2(xj0lfAYdfAA5D_4mtb?6o~)NpK1U zl_&IB2Psz5NXw)#!;uzY3#EB^8NK&0A%Uv z>vvYzB6m>k-g{c^%`UR7ZndnfETUjq3`f;!%%!b3jw4UodWt#xF$0myY}+W?wl^`B z|L8uOGTue0^sHQ2JT2e4z5o%Pl^0u&Gv7U?jAKqkY+KK}r2(cevL7F3DuLc>ZwG%- zSh7uwEr+w(!Qx^FRYLbcd`!nXePEDk+>lT&+d>=^H#Jtdwl-h>v)}#gvQIz!{(t?) z?DVQzGi&Vj>nP_{^UI?hl6>+8)O@Wk`L!(hpIBs<7}1Axu^OWrpfoYhkn)V$zL_p` zQ~~%Ush#i%xDu^FM&u@)hsbI?<8(=k#MOoSXguR5sD*QH1X}0hegNYpR!iO=|=Z03HRgZem-!#Yt;dK!Cr21CC>$|Fv*(w&whC0`4O6D4OcHTm{w&9QL=r zd=+mLW91VzDE#GLJOI(A*?EJDROV^3xPqBt&OCyv#Sz4GMP)fathAgvLwFvexeu%( zkoo{a`)zzUPaJKQ8`JoUS{lJ-8XwANeOE7EhKbIHYeqv(TPwyYsyTPoP~wdJoTDmn z##oPD;Otj>fPq3zt|r=bmBV65l$gU#h{={=ov~Nul+_9^NE`q5a?Z}$d3D#_VoE=zC0LB<9-)(tt*bAxz$7goChd ztnmFJ;nq0LN+8;y%a~nBD#7`!hDn_Caw1EnX|zF7-bk0^&YifDo`Ae{KSB9KG_`@i z^h$%a=Ni-sVuN4IuZu^tl!CM^M3FZzJJ5c4xOr0EX5;#;FM>2ZJR;uz5M{w`S-E<( ztlqp)i4pU^6EwcZbhMkjYhwY)fsJ?9XI&|afr1j6h{xQ^WuX!6j9IG`S^BKo$~9tV z3|H@+?lM1HF5i6PYWWhdw@^?#XQ#@Od(X-y%Nd?NaKNF-q^#n$e7-DMfST=rt!AJ$5s+H z2Koi=?`C=Dqz2_8Z@v0|)HF$MseAxiZd0p$7`+J9&gT?-p-5)&QZ`SZEM#o%}i7U$2eid!|^ zd1#UwJ^0(gsT1$@PuPCvhsW`ZLI#+UfZX{FS(&-Fd1^nEz<5=qzT63(%SlvL(0^I&-&&J5roy=X7_FBe@}R*5wVsGw+aF!I|B(Z3LHrs!1RO z0C&vthaxHzKc)#G7%BqVol9^X&%F6OVQNBMbK8di<0_T1>l0YT4y(0T3E?q?{t2!l=f%v+&J* z!IF>pHssIet}=rhc`&>+SbY|)&kK<%`=PsPg?(YXgB#f}rk5KyIHV%^9frdfVAOmX zr1oCFwNM#RYp|LvB_J#vz!&q;hWD?Tmu(+ksgAeHDuxd%#o#Y^1}@?Nt+`yzxN&|3 zft(?>*0!)lmfg;;rB&Qw)|C091m>|DsSlskQhPS7#I5$$d9h`z8etVbkRu#UgLO>| zT~D3EAVOctv5sPyL>pNZQw4ARa&woKJXTV>V%?}p1hcH!&La+wW60pi2S+N_AUX{k z%vZSX0nXGS+*Shx_6tLl6Qk_cMXc^KPZWoAZ%US_om}zx#wMOP9>$W3uQ9`vpoQ1N z1oY&H@-kjIw?D;tOY5@J90=wbv}?$2K0ryQ(>KayTa0oxQd=I<*82dlbss|uiBl=X zmes1szB8@6r8e2;>=7XAH(sNRBjtTG+p$>>a#VaUoycOkI%a@m*Tm18H6>p76nTT!~8czzx6dCftLB@2-92IWE z*>+Ef1=wY4xOnJ$Ho;qPt$@Z5d9pSKJmVU1Ks-FhxmpLrhH_MwlL$L0Qo@(h@lN=# z2+|7HwHIk=(h8*O0D;#l`4}-*GObVbLlZ2i_A(o> z*`_wuiHv6;NFN2t2?*Rnw4HH|1`=zuet;dXz@phrUczDqNQEs$L`5D{>P&$3Cm^9- z2E%cUL@@~f06+jqL_t(&OB*F+Aq+t>@#1<#L?xsR7G%X78p8`%)F@)Du#+)}djL1g zH{u_|nT`=B-BT-4YhC1EGr`)kMCwTZdX31ArG(?u8fie$(>ghNjF^_AEqZV|@h2PH zDX0D3Bfxd!Cvd38c(B}lE~i40X8A_%4rkM7gCWXnX=GC6juSZm(@>nysa(`HV?8z_P6}kEKlwFhGY&Jo&uFj4rH2Yh4v8;oj z5$#$3<#B+c4J9%F@YU^wA=vIihn}f>npp?}A>Y9}57oHE(zMH`+MkJ@;PbB?kST&*Z~5+r|<^R^UB_@dmDqpCOjazHzfG z+`7*36Z>WBhd*a2QA2QZ>>^uf{!;AoQYVi_Y+bIoRYEttSJ%L#Gw+L+zzno;E$lGx zWLcvpT}_;>;xQ?Qqjz(hnwur9u&u_U7ya_DetN%r_IKFS4uY!z6@iYKbGc#tWCw3~ zgio-hIRzIRf#cvFKbcsUhRi2|Ao8;vQvDOU(RH zg-bRQSZ4^xeY|e9JJ#I|h#Ybzn^W>v5hVBRIWI(PcFpq$g4dswM^x(TNlUJ2T)D(1 znK|<@1{QM=zH?i(+S8b2j<6A4t#Mx>>O}t@+x48Ms}*-A5dbo1>pfD$Hr@S)7=SQ^ z(B;l7>&R|qn8kadjglp0+Pla4uhv`+S}uQ$GKf#QG~-@g%6@`&eN3n6l+~j~J@6~i z4|Sbp{;Vcfw?8y6#Hy7RT$=ID`S3BD2K3eCCWDU|K;L^n8#w@xwsyM=#nRL_pL20{ z5D>5OO{8ld+uhHJU-6_RZMazRv|*5u`7#8ihWzTUxDtpRqT5=P-I5)H)G-o~jtNFa z*zUt+zM&r@^4CP-MpKp`1nhSrw?Sw8>RqBWrUNSvKGPmpppBIe-uh)Z z+uSH8kDhX38JYwVMnp)0I(j5`)BlZ^ku7q)%%e6J56CP2lPHpTqSdgPg+;FgY#kt! zj9f%8PEC?ndCzx$xB|&|4X$?zP+>C%fI#m)FXey#%a6-{@$Y{P?#7N2khBL!>xH|3 zOs)~a#dg?*qe~i3Kc8N?!njdAP{>t9O0Ue%5W78a+0vPh+<07y=86>+jay4CM}jCV z4}X3`vV_16K}6sxqVr5eVcR(-n)E-^lBJkQ&WTyMd>t{GGjZ`= z`SHhS)Je04k-spVubxq7H!dy28PF?4&wa=-Vwc}5GZO=8tfDml` z;Ky8veU4BSM*<^)C~*R)i+!Gk5vU7uFn;+8rgecl*CO#=``jW#py7i9nGwVV5!RGZ z7*uE}n;6N<3)-r4Ji=8t zHX>anFfZZr(%w@Eg9$gSS1rR(K?n$88pjAUq;X7}G|Iw$;IG0kt!0KYus-=e|C@*9 z-~P(g^6k%FOXbyS&MvVhm4Bzj}_{ZH6R$F7}GG~GMhUSM0)fyh2MgQ9y1P&L zgtCuQPQ~vjOe|H9Ik;#5;&N2PY5R+C=iDLqw4+*P=78?*p?`NdKINSUc!5BCR%O+2 zgKWA!;lXhGC{Oi6i>KrwqWdm33@!<#B3J-bF0D0}Yqr9p9)rmBp+acC!{3KvTShR3 z!~xHzl#KjaEc4BG^N_O+zSJ}=tzU9=1BU5rGPbhN@P6?-fdZ~C)}_8B$R7$*j~E<) z%cnF*2fqg7xBeu&!l~eZ(KTQy(S6S|DZHn8l%?g8)Yim{yJKE!0>{TRLRUd+kobvf zMBezHgRgc+%762J{<3`L)@6>u^f->{A4*S&wu>T}rg$uTvJ(Y&Ge~hFXIdB-x@U3f zoTBjK99AwWS&L{AqGeW5WNAZ8ih{&FL1a|Zs%V(`E(olP7U#p3QEubbH;pzVsEEU8 zxjkswV=&GjSj#3%#q>pXB`xqbiwR+ES8J`-T5bh`bC1(>=C-aMAG6G*U9b6ACsEHi zqG=^8jUM`#44&Z$(f+}bu-v(GsocB&VGKP^h|}+5M55FV@w)x)6p{E8(Kp9`F!z;! zt(&&Y_QHs50|85{!8Y1KrI`e-3md&+$VtW-xME-dA-PsOr9UZ*ab14;ym_qF9#Lu+6L zf`BHJf{82snJ^GCSQzG;ml4k6Imm?b`bCWz2@{uSzNNi{SSw-WYXm?0OiT0Eu3EI3 zQ4Once)ZWx0;x+_8`X?!eBmYsek;rb7*;=vltY_WBdjQ-n8Rs1s|Sly#Z1I#r5TrVG9p7bYK{%pr;{8<(K+HU zOCugfDMysMy2O)Cqik-0b5plvcNs-47ni9F#jY1?5HD9fMWJ;@UYaxtJ7>#^@QCuG z7(`UAOW8<20+AfTjom~cmwTQ0?O)8kO&rQcQ}!6K+?cNovc@J8%vU{YcZ_8(vdg+M zpC}i-pTP(?)C~<9Wmh2ukrHtnzfh{CC!BhKn2>(j&a}FvaYs}Cl(Ua8g8A{gPYR9= zskfqG#e;OOeJT^NtNA&n;nwl#ii#{gj2D|9ucJq#oIthJ6n4C9DV zV}P&iq5)|$($Hi)8OR(y*Txyxsk1=AVP3CSIJGn0Z$c02`+oX27kzDSGR$9&qvARAi(JGP?iysIt z?{G?57Lp~1XGG(1+C=AabL`RejGj$g&gN8kl3C*;)@MhhQeZjFw;tw~lk9*y&VZsA zXL~q*b(!oK;+?_wm1rt~sgUZAs6U~Cv~6|yLfQs+{{7>?Z#sWzA~YGbrY8Mi(AwY^(N-|Ho!>31pGLxUK}`)mrwgBaRjIu-6vHaV@1 zjx||4?%U1OJEP{e}1GBPg6G{T zD%N8#+20Enoy9azQ$e@i>9B1QSE&vXhn$8tI?AD8m=(G!$vRjU^N8{ZIBK7(k_Te3 z4b2s)!YfGR_+tB69uU|82o-ol^+To$^RqZ*-KoYAwgGdBXuU_+xS;n(iE~63IsT&! z>WTqCS?01mLyW?voook>z3juC`=pf~561>0y12!-g>!lmC!j8rR8>np!4!!BKIftJajfx3nX!1E|6$pDfMHeVNM5MF^* zP$_neq9~CPcb!0Is%;dx&X|k#S^J9?lg9R(SjABHb7~Ev_%&3tj^noM$q$eth>v%% zu(@-2s@z)DTF9$5jL(P60e8`+zhK`dXu{m9x{Hx$MTg4a;EZxH{=&f!KRr=EPGI;U zOCK)bG%lJoNjYh_ZRLz{DFd+{p>%SSforyM+!NsfXA*a%daTg1p>s&Nt;!t<#fo@y znfq1@(e9xAaU;K9NNR7@85rnA;5MW zSd<|6Gam<>5UYJedP`Ed2;adtxv?5*xw?I!q~YLXfz8wHfcAC<5c)M`=DTMsogflZ z*z(~gFUo)S|2!yuA-*UH4}ub0#97v+q$&SeGy^`4t|q1P)C7vCIAa#7wX zwBVmxJSxBTTEG0-*9u3J;@$@1k%OInd5r5G_&-;L=aX%94$+YFtL2lB#2fnNdm>O* z>NkyX&#T}1YT3nI>@jAV4*r%)1*QX*aWkLwA^)qGUds|dDGNlq{Ch*$oIifrQ%zUD zRJgTjL*eaF5hl+xSeh{}?P~=wJX#I1Yw=QWDyx4bv?4u3LeEM$193yRe3x^?L=aJH z3?dm}S!4uc@pAlb4K1i#T|pek4%jyGQ#|Y6Ugi8*?GGS2mq3oOj@rdN%>Mt&*n2fw zmSp#RdA%uf%e%JIJsx8)07(I$1b`43dQb!;0(ubT+g)c>Z5Qx5%U;yEv`ZukJe^(ad%b7Zqg%aQn;pPZ3Xk{qtg|cWvj-0`C0vKT2qk#Z-Xx#&7%~|Cal*9wQ)>C2 z@)2OZmQhYLPp2~u1tZ{=pX9>r9fRQx%UAENwNE}TZzpdA7+TEa+t{5&D~YWQyi4GE+KG5ph}a5m42ZXhI1~P9@r8-YuRlT52Z9N6 zoOoUkOB-6lEU?Jwsq}}~rrK+GX7K?gckXC*$YgN@R0S2PxbhWZ6G)hGTyy=1yIjBf zf4tlN$3ME!{`6}rTyN<*COscVvFu_c^~3j$+h@$D{--w;l8aY#A1?P;} zG$3sL0U~)GBG4*?)Dp|=(Havdd1P4UunKG);%1{kLIlD?!O!uR5d?TTEcSxyIhLs4 zTKj(Dv2Blr>KW(kF0&C~g-gCn=WT$}-(kA=IcMHu?U~F;LND#`9!5E0NbmN=QgIbC5erI$Lyc|MI;xj=W*03d(61QlY{D5_jvK&_h|3;W2?* zM)NDC443(-Ph9w6>|xqDB3vTxYp;E=E!_EAG;%bMch))E>LJq)j6#w)O${JMkSqw_ z4CynL2=veW6hBjsBWD`^d@!myq?Z-UNby8fnup~{Dam}2cY};+1ArIk!Q@7yZ1E~! zL;yc^!P!7STAEPIfT|F~O&ch_sQH7Tr(~KSGDaq;l936jm96{?8M5LT0W+R|Fxd>t z8t2$b=XH^nI6!QH1GRTHSWhL7Q-t^)mQ6=PUJ8ehhFk}zRxCb%11SG=ZnS2v>&|rh zpa0co?QcGK-u}}!IlX~t@+R}j51&o9KY#bAUA}hO{s+ql6M{_2X>XEsU4+bK6i=LB zR2ha;31x3n2{U*1F@*VZ|KgEg+_SRSpRHNSn0v!EH_xEI$gY!{&)O=Y7*2(YwJjDVqV^68fX z>G;YzkturvXK!to<+tIrA)ZCrRkqDhpR&p#bMnu&X4}8M&vMr(=Ui!lBEOka9`)`> z%6uV!A^a;BhWAS$ub>3@S_qO#MEwt9V8Jw~>d_I#E_3;u6>TpoZ%zWKEbxJL7=TEL zIAQT84+l1$#m{4`Dk6B|6&A{m;r^MJ{{ZP+X?)kTYk6ykV@zLlB!SiV6W>}^;M4E z`~sST%P3~N^mvOQ;K%nLw>5g(=L;O-Z0-9uSsr2FV^-!}EnG54F*VIQo;uh5^25XS z{SQ~U#F?HJmF))i!k$cB;D$UlVs9R_JKX3duU3y)!@LYPsbo;e&{ENKZfZH~Ma|Dm z@$!*^5=|it;}Vd9?MUGOVR8r5MXZ~a*bAEZA{A@-?1kJXtH8R)mF}4w=+XG+ywP>r z>zunZMI*M&ZF~1QknIttO?Z#bC9dRfXidg64B$sEy3ZP{FlP>-yMBwiZaJFg7+hn%In5d_7l=57a~Uj51+BBuK6EJ}J0@`-uuD+#g3|^S zJvJf?NJz(qd!{^|uVv5gklqo6hX6d^@X$LAF-T`whdS$AA_sOcmA3vRQnK!`^y=>w z+xia=4v!Pog;%k!fAqlzjMUhN%;NL<-Mj7SQ)p$wGVpiOnRamIJ~icPg5V6TeCpju zJ>PLykK5yn4sZ+1${}Zh@trT=PTBE6cQxjEj-ifL>UFc#r8OjsB>kA zLtL-1_y}#t>tDo(p%PS&ALOg>uDVu|rzV zj*3x-v4|@J`?r90RRx=Cv~#Qldt9qRGa4F_m&|{`TM>wNj-F!|A#hupmQ!FuTb)#hmQ~K69*XYfnv97KA^w;lgl) zQDt_BY-3|-ZKSOH8J-k1d|)r(vXyJK2P|oMjF0a+8!me~%)KzASY>I5v}B`jPu7*A zo9^_V3@iUI8*rqw=_|gqFTULgcb7cgeU6UcZ=SWcKmBpLNFCh&?4bSZ>1^9z_-oS6 zHRCR>Ly<3qH^7u=`8Ee}=&rwy%Ml;qR>;`jejf^s^3@!3ej+?I8T90$8kR&KbG-`W5VV(V*Uco+X$>pZrPmB7#tgz`T)9DAvQN z^%D-gvUl{#jjZ*tF6+*Zy3q}@JHC-@hUCoAP*_++f)S_W0Ib4J_yxY_Qx)0~tFI(A zh5za154bD#mmfZFfBI#5Gyr!&G@cDyDVJ=W5nPA6w2lsmHSvp5CJopCmGY=+H9x^g{z#Tis zj$B4;O73A=8Jyz!m!G1iU*!_eKPs99i8~u+dXGlP+P%+K!@zxL&|AOc zHKZQ_5rhQ`Vpfs^K?K3M-Nog!;E%Z2=-nsJ8`q<^H#pJX`(5@Js%@}8aoXh`wV1fx zbN5CjSph%(z&9%iWOkAySr5Oa7bk~E`n$3H-@pH;{r5X}+Ibi)Mgia^gNm!Yw89z} zNpgr>1{s#Yctci1p-kZDTIjWFS2-s1a?;lEFn}Zd3~Qe=tR;KnfkNy2bis?f0CGb9 zE~8pE+1pFL3ay3$^Yok!W9JZ1g?@^3TL|v6$E(1@8p+*7Gzy2VsUma^ulMLFcd<>j z6HP_J zde5;`rBK#b!*%jO_eofdNMzla7;LPw%6TFaCu~Qv0r7XB=W$KL2dAI!?x&%&WdCoq5;+ zB`@g{ei^XS6IfN&eBhB3d4?Ji21o7IOF_aXh$k#Se5<$uAL8;B`_F+KNkfq0u!$n$ z7ytfO?df};I`+W_%!bT_HFC*kfGM(p?ROO_<qO9NNax21=^_qg2Z z<44==o42}VkyyZyX$xMUBV_beEYH|>xF+g+&UvXM*6)}FeV2kRU%A|_-MA9;%DxnM z=0$JnQKa@j?)-6RJj!^vAvcy+}zG7`qyr9>mI{dvrI2aUNgXRi0c_ni_4B8imqOK zee4b?8xTBgfV6K8eaW-;q~wx@M$<({CpH3(EQI24_z9}xhifz-oakXgXnMo);J4SQ zVV`%FOA=P>dVeuig^hjrCXE~#VI&|p2S&k52_qlH7p4uL=5vHud<`okd@-!2>F?ub z52*~vpFCBJ(dPl8LaQv|VUE;|Q27y^QbdVp;#Oer)C2E~1d_r`(;f$eEuP*Xt3fz- zux!5jxUF+Z@eCDW4A&eB^bjdA<5Q>%i0UH&#kmW@ENilt-ng0Jk|*HrK6=8TXwOlW zV1jn(8Ydnuwhy?x_lPAQwURmu>z^?C6Q~9xJQ;AWt5tZzkXDYC$#c@9!(tUwEK!)3^Ff`xuFZCinW1bt@89i(a2`D6kR8D~qV*6T zX%hTHnkvgY4V1^vAs}=8S&)Pl5QcFb}2sYInj#?{q+xDg<(|3k_vy*4C#i9;}<;6W*8X3ViFZ$1h!z3bCtogm1R&jNf*Wh zr1!Aig1NEB>G4Y<>LalEW^9lsVr|SJLOd(30P^V%;dvB7`bQ~3NFVQej;{)gAe}-~ z5~57F4$EhE1vA7GuHI`KZR^AL+dP*_eTe<**4uC$Li5bga>SKIyH+~v$8 z3@eD

={;$SNp35G_5{ z?Ty^@Zc_7}VLm#~J+LPzvt(oi+MY3rCwj6N{u4LrvLq6(2r2>_O)JVEH`UirN*D+V zFkB{bomAY+Q^8dT+1G9qvNbhcQ(|NoIalzImKrzUZ3rHq#LI0Xrj51Fq&*z|oEa++I9+fZrVD*OJSj zzW8Y!=8#)u)ih~L8OHew`oJmZwm$?F=fSz@eFedQ}TE>nbnQGw60McKS20RE&`~hEM8=>Rr zS80!ZtCV?5?V6HIlBiMw&30|lp+rc{`K!xZz{jg?33*$4O9_NScKN9&(P<4isUKg1)CX)KK37LHH>Hrg1O zE9H^l#K0{130@>sc%z;tnmxp?9vpD8BU{#3Udei+jDX9WHB12vTSL56=%~Cor90op z##ZA2mv%he;S!1=uY+`MY^DwqJ&?_%6PHCil@M-*xxorxC&QA9imiR#GY_Pk8uq6m z#S?3>DKx}8+bFo0D&*iag_lFY_gSYp1~y0$(Lb$jd{9yw8(`U^JMZ*Hd$q^v31q+; zIh2xV5BhJhtfYd=M#3J}dUHf!&1F;poy;PFOk?F;y7t;LDD=D=N$Qd%iKq|fw!m<% zD~_K&MGxRVfaQ;9I^WBUUic&vojAV48!$NVvr0x$jSB{*$v>ggd&M3iCULkx?*V?# z#5nz3C_PQ0j(#RmfUS#8X=K{;je1 z&fWXm2lJ#&vEOtWs~{0|;GPz~BgA*d$?!vt9_@wp+b|i|Vd{lfu0fRjw(;Nzb7N1V zNQ5BFTbS_v>QQ_D(Qf_ zpX8%RU$|EME|j@OgzHQ;m@A8K;#ekJdPHE911O=^X)@5$u=4inbIu?mm?F3g^>2L$ zyFL?|nE4sNS6Rd%Z5Q-ccnf0Iv|#{8QFcQ5dQIxW^=obJ5;rY!``|tY@0HL702vN| z%|zkKvmT>z^nztr9O4<7|HkXzWM`2&g2IW9@Dl=J6tJ?G$du5~GG11K!!3t_`h%YW zqT8eO#@c9_;aXbmjw3-;)Bz^`zQtK_Aw)0>Q4q&AclD_K{r~3MoaKA2{l!23Uc38q zmZ^jiV)VoqA<%IA=)Gqsqi%tVM1{KGWKS85GK?;hI=pphpeo8M+4+*N2l)OoF;R0rmoEYdFb8m-bshBy^Q!|R$ zLZA-m4Ly=)mxjS*Cw`o6$N0nRJfnmYVe+y~5${ECP?IgaYW$mR!zCe~^aP4ZFI}L4fRm=-S(F!mITQx&9t#pzIrJQdI1KEzF>3wt1*xEc3# z?%DZMwoAPVVHP;9<$t^X{kHjhT)|1g&lCm$Kl*5+{lmX?tzATGZNFZF8JH9T>!VX} z?J3+`f5PyXU1%s=VA34A22&1Yvyv6kD3lOF*Isi91kA0_y6!2?DtJ^dIj^<#^>#TG zKgaZV_K4{%=_N^xA6OA*9xXI0Vi^{@4R8-VzyU&+lNW;qy!az;el-j z(A&HmVtWeYDZ}pVgX#8g{kYxZfHk-4U06JAx317Ipa|5M!=6Enwili$j4G4VG&=-# zKFh!ZnbFvv`l8&93x4 zgp&{j4hxEC@7Yq2j=;;F69^G3I0+NYV%{MGpcNph?PUNy?-!gpVx$8=$Z2xnh+m!( z06|Hv6hBFbImD|uA|G&v1Qt8z5e`J{?XO*0Y_~4Vx9b?)|NfV6x4(RCp406$(@WI` zs`)BBo}K)wdzHgna(N)co<6`&$Yjc*R|CXT&Y zHWr}q-Q+5a96E{ zTxZT{6~sYaL6BZjtu1w{w%>WP>%1zscAvPl5F{y<%S38(AFc1VyH9u8>ehCabS^S` z*dof45E@fQ_W4db$K{`gnLFHF;Oa(&RjsiC%l|yx?W8rPJF{q)!d_3dPTRYfUruq! znDsJ9(@;-5d=tP6&ie1(R%;hP{d||T_c11E?9b(+onGY9HNsBZJdrr^f`5~41aagU zSc&G3;V-7a2l_eU`Puu=xueKl*24fLwv7Xv@wo!%NAQHR9uOFk&@i*)_S)CqZdYgs zu3@q}HGiISvL3d_?|nqYaA8~lHa?k5;3R4ao#AH!h*Zpv?^9;FJ55fRL)z<{*yFLt>$W_{78>w- zZN~|Mes^QP{rWyyXyRaY%|;3oRSdhAIej8DAdbT=@;}Bfknw!;D{j4cx{@O>L|@ zYufVgy!i^#DFkYRZGGTaZuHVUM$gW7diY+fWc)Et^vL?|acli=K4dzA;91v00%8xi z4lt@SAiY}JPexWVtFUV6Bn47AbTt_g^UQfrDBL{6O<~>HXE^$Fv;E}mPP@A; z6Q8v62+b`nANtyjbM5Nl47c1N2pq767=m*tgJe+oC3pMar&ii|uQtYq_B!Ax(|DAO z!pY#6&YAftqk>~6qTJ21$tH|FEQH>FOz+Ro`5Hocj>hIZz30j$%p2L5?{PQ;kvBav zf(SUY`q)Y%f&Rm8KGo<5CMkBAj2mv&Z_uJy-#xGb%GQs7>Tx`hr(*?IrP<%wq^oe#4L9 z70=}Lem1=FF;e9@QHDv_Aet#0n_spV3jg}Af7EWgcB9>=D63D`+8RCW6g;Aig!WCx z?-=Lq!&6UwgOzW@tN(dFTcC<-tVt*QmxuINMcn>1ewq?DS+IC?X!We%A?^^p3v&0( z)9Cz3vJxA4h+FlZ9JKpS589V*W32>p@20m)P?uE59y!cZtSncw~2zkc4ne&-Iu zMYoT`pmuw_#l($>fT{DK@Hfo)sRBNQB1gC(Cy}&Da_w^uMUuzMIGo!;3ADPp&01({ zH_xHCqFMcn(-fY=^jj!6b~xhGVbdRc6BEc21dR0FP3utC*5iQQS|f>h%oXP}r(_NL z82f3j{j?Q#T5t#gduUeo=;`*4?Ws|qxFZPvM~~UF3Y<&ypo`?Qgfi=#8}gY#EYjV3 z{OLSrJ6Wb`^EIsC)h~KbhsWAatDLjfcMGE@>#NPShc&%cVJ?Y1p!4_?3%VI^sGq23 zh1FV`boufp^;luOp5tkUBMoq4So&EX;IduA@df9PR6f% z1!moL*VA-;ia~&ks}c@^jHgo`8}kLWRyvGdd$xv>^m5zddePla?&ko2@D2Q^x+{PL zHU>C+3<&hh9d0CkFZ9_IBzn=o(f}=5cId|GC6gewP9RV4=Ls@d%okzz?+&Yfli!dP zBrH!KothL{WsNx5D?NO^)&7bLn_hkWc6++E-5zk)4db~edom{{uE{(0Ds6u70jD1P z&T0GN72y(Qhr&~(P`0cgptU@sF#)|9OzN^n6ihI)LtK|`_BB+XM>=GejmG@jjElF^ znoe;E_09GgilQB^#PGJb_qmesLzF9P2>3SNUt$U7wHxQ#$L~L6K8{)B&6V6n=h?;@ z<2!Wrvf@Le{($XwTrQBgvL=hiNZAQO>M<^B&$vNQ!xM++>%0Tsok4T#?OkAWu!Py- z#p~V`gt3VA4{IG`9+?B^D1%BXK+?NHGs^D}*mYNL?o(l=ZL+6fNT`f%h6Yfr_Al>p zoh7!v%pgWiI(L36s>>VtT4&ZocjXr!>mU_WewA*BHwbgm5>LHKK`FlL(E9Ui-W{&F z7u4d{p_LlAqVt9=_)_dcl-=1-}+S!O5hK%K{(BbKVXdAF zil~$~U?9v0Uia0b+*zUO;T@$OtH<>@AjYE?OdpZ&-xXU}6;j{*SHh0dcHgtljz{^9 z>$NJ|!k%d~%*MPxz+Uu(&ENm-dq3uqIu!^9?a8A@@TH14Vgan+fkzOg5rPTU&;8m@ zr|i3PX?fweUAe*eRcL8<*lPFo!9sgNz8}&HKVWa{6L#=S?a#M+_cypKdyhHx8N!Ql|#;1T4d|tGAB3&YD3Tij$hq@nR1c^Ws#w*R|PW`Pc&TokI=?c5g~RC zTWgV+yJDsFo2RTfPhnURVoSucby?o22q1T7-T7bMFf5;WcT$sx)^SgeCvhjho2;BK zh7dh@_ki*n0~x=`6XUNWec@*uF7N3&RP)mCL=C!AqiO(pUW6KvsoJR)aWvS^-~XT; zdzKXR4i7_H8B(jUMD8}mV-RLnt_!P~RN=b6M3Q_ja|5J2j8TL$W&~*>Rp`ZcJ;xXn zflTr891QuKBWlIAQU_Rg$Wl6e+;TSs1Ld%6?}&Y{+5XKx`ymV2Xj&h`NJbr14j(}* zDFa;1?{iNKJaj7;!@_hGhV#d;*MFMmY3%QDFWNXe*IPCW!e@8=@QBKv-k4&;6ZiJeTOYss8QNY9EP%Pj zl^#>vl;8zjNA$A$Xv24yhfgsVou6m9jQlh=MX$yASRVPp{Fh#IiHob}SzGnQ#ASNc zrEBc4Vu)@pwGA`bzZ{|Xa)ZBj@7kO0u*EIL7?dlgQn-tV@B`q>Xf|op%Q@w+B49ci zhmW4^w?`Zvudz&#&6uBeRz**jC)+cf}n({9K+hOs{ zDu6J@!Z>6YumY~IqDf^YGJU908S6157#c_rPLQL(?GDqFwc3%Gb}Yt(?A>o zW0$$H_w&g7!I%8K>sE!|BFeQR8ZnhT9x~??>Im9pS|l?$!K z8DGrrV{1&j?mFMp_fQ#7-QlBrBRm_O1vWM;P`_KxwnFQnb}Jd+x|X*0#_AhJ;s%Vf z`Sjd-t^(kFV1^NF(Fv;lWEku=7a=-@8)X*GG|+~gj?+%=1Hh0kn6#KKhpi-4BH!gu_EQTR(#{~IpA?w^Rm zcO@pI;2LC3x*LMa>Brn!1+jijEdV-XA-UWzQ)_4JVw_JIfGy4q=SJJ-9oMU1pB!+{cTy ztY}Rsoo^zdnTroQT|hZCi=aB<;PgBiF?%zVNISsRqDZ_c zQ&ppsDrM6&y|47tVu}LdP5shWY@|6Oa__yayBkjiJ0AD_ipv$@2m$>+DnUfly25cGt(kGSW%^MHstY6v4Ni^mt zzxzjD;X>by_Q8LA039I&-XYNMeu%}R zDl^0+L-W)bHFcS<`WfXPq{hHfP*QXls72cmMggwP6{DrC3J^DXuiag3|LhVz{_Xaj#<)^qe)coO6Vrd1kO@|JqB&2@?U5mz5Ql zJhU{jw^v}T1cmn;LVup2st2h#TviBFCb=Kio?Gpr`)xS^4NYRrm8X~w9x;+|=)H*Y z!reHB2izG;uX~K39daa;OF9xg^g+;F{_rRrFMnP@ST%V(e}QG1K{4e~tlRESX794S z_7;X8+glun4s7v>B^SMgG|Uc@4xF z@`VqHJOJ%}^<4l`dfQ9AU zOQU@uAJ8+3i#?GIK+a|Iu337%gA*_6;^w77I9-P1B1dMr>xng)n=_z zgZ~)>+~MGyb5oRDt`+Yg*qHXF*K;F6H4pf~VcGTv<4?KwmY(&98wxTKfcd>wXdjrz z;IZq_oA2ByY~7hwictP+0+2Rokg6PW;NOk&BZQO<36Ap0lGsWWy( z-|;tL{4XBh6`}!-dYx{+_&4uy(KfB7i^L>LN-HKbA4AY9L+f}=<~5E}K?C3TVYC2D z$dLa_(n^Rkz0ix_mHu^4pRS`Be?Wya9%Mj`R+6ay?fIB4CA?VPj}o8_wxT$zz{O zppTe8dh((dJiCmdzx0XF$sqKEHPjS$HYq45gnQ9dt;NE82=Ns4G0VIUI|i1OP*9oF z`4j-Xkq$jD3MB@o?5sIpbAxunj&cvEdpFAO(c^nEqe?puOJ88=Cuk3q;E>3U}b1$B#8`T8zXgV;BpnE551*IE4?@4Uzt& zOs~KB0a||I#+W5z7+i;Fya@c*W5i3NxRwj}$=~Nh+%a5BJmCg`)i=p4aCZS! z@ZvqZ_Tb_pJSk8urB>RWm(dF-{`J4w2LhNiRhTXK0mp^zvjh;PAh1}}U)(^DQ^nlM zPi6*4L@M7pAVrWLGD*5oK}4g7XUr%R@QONnh*R+m)fvbi05d1z)x*CMB77r28MH53)t*=~yvDH36C>$;;gu4i#*K}BhG$i0O%k<#@ zTj2wC56v;3Jz zh9lvjA!YJP&ENuV=P%qu2}i>dN{AB)@}oFb1(bk|-+PQap`&fQH0{I5^iHW9se9Jx zo!9|4g!8UH#mLhb^6+nT-ueQ~%eCnp=Y%SI_z7*?C#yw~_NG4VMeL2`e&ah#&GtMC zg>Yr52YSRbkf+=F6k4s1T=Vrx62(J7Es^vIIACV!Nq@?;&(W+7q4~^Gjq_kFm$HGK zvJnMm=+KT*hV1FZ%cmOwY+1Cko(DGlD)4JBZmtp~-2wb!Lvx;yoR@}Krho-*JqJEW zF62gebz^}Ue`Sz9jEuob;bCV=RUgUQp>R1u#lLcv@hFgr2x(Pl?9_ zQO~04SC8}gkd7E|gkc2WCEtnPQ-)RcES{R6Kuo$KVEHH%TTl1eyZaxqF`nHhG9^I; z&liSmq`!EZ!_IGBX%9cX%Xv*TY^{o`n8I|#arMSR`v-saO1pabdVBPAxBcShPun{` z{W#|a%kVlz}i!hz#;V!b7FS@Sum?Y#&Yzb)lqKU(VOvW*&q^z*9DRF%(6N7Nm#0m*d;5002M$Nklt^~D6kba#XT{%S#up>v^<^kNaWZkez3jIHG#Qha*n@_+ z;tKJsD_-d^4gWmwic<~CDF>J*)-%cgB88=|XVZmuse8zgGJ7@5*qx1Qo|PQPXHYQS z*rVj(#WIO-f@Ni~xB565@f-106&hsZ|t$`28!sP)Xp%YF*Wafp?J-vAE(0gibG zZ@&vM=c31Gc16^-@r0`>ihLEP2$)1kGv!8Scb&k%fjWTdD7Ys1(NErQtDNMoOxbHD z-ta3Vb8xBU$|cTjW?A8_OH1urZ@s~my^Z!y+jjfG4;~H7Rro$$$IS0f52yo{3{OJ? zVh%|{)5Na$e$UcafBW}e#V-7?y^8|3;0}@UFyFy-)Wjt~dsu(=cz*lWaV>xI(}#6h z+xOJvfH)O6f?*U3ULKKj873+CwWbZ@|CoRY{M=8{;@h9&2!&V;55~ToQpu~LBp+Kn z@vFu)p+x2Bq?)xv**|Vb)05g$?LFVc)|iW;jUdrRfqavUMl-GEO-u*RBecuY+=`Cr z;Z-b?{^)nlw@*I)tg+c5#g?(3F>Kyo20K=h6|Jf$j>fjRE}ta>!B&cq)kg*{g>30CG528|Vk21y<&l zx71_h(z0iYD{-?#Ka#U25Gh$^hc;qoI zbomx@7_S4Kj4eLpAL2}a9H$t+Rn##YVMl8`_DTUQYlt)gYM#Q8K$X8tHp|(=m$|9$ z;ss9sr?c_yovYl9bL*Am_Ss`Mip!^58M06B&6OcvVySHZfcwgc>j@AiCum(|W*FV| zSeZJs5+52m^U$EfURc7LwL{McBtunuLw_8crW|68fJL-O%i2-s6Z63cDhebFJC3pE z)f&lpv^KuIP)S01t1SvHg3|N)P5h8aBKpUdZLv!_T5T}UK#EyBH3##pz(vT#pp+3kpL`eRAeBM>iP)Va;IaOD` z1!NRByhyj9jH956A84bbepT6vCe$0Q_|!=LRsJ29@Dp@>=O>?{d}PghP?Vak7bW0_ zkAjs@e@0m6WB8e5&2f&CF<`4n;3gPkI4@l`Ekp-)N^3c{;tmq5Wcbsn$ZI;DKW8r) z!fU}QzJdV2o&g~#z(oEjbUY=7e-*=Y!qC-YfJ4tD^CsE|uIY3Yzg~wp`IrbJs;3r? zqM9_x>6v`tzb6np!whhiyKIi<7)k@mjQ~q4x7(M#eB6Hh&b#cy;gC3c8U8-xyxtRz zR#kE2aC64EXp*&*l94rzaQ|w}*~lPh`U8EGYs#uIzDKBP!6f}1T84B)cOBOe09mCc zWj2|fIOjYAKl54z#dNN(Iuu2yEQdh+m-C~{k%l!aY&!BtPe?+0dTDOM;jIl9R=8@D zrK?4hM$3#cra3le`P_xrMZ0X{us#214YIuu7=XkMnf<^Hc;JI6jBPv@$}-tIFQ4Pg zD74hBI0Fr2IB3Ae3A|I4lEgnBly&Unh4Ghfex8YvuK~adft`&;OArKw0Bg>9FM!fBj&-pfAPC*{r>&-)9=0C{_sz}-2V30zuNwn|Mf4phVw7%_oksthdI>h|wy{GMOe)FXL{x`1EL#(vz>7BNwT`hv;g*q~~e=3XK z{cO8!?q6y1(5L>{gVu)dDS8=rRMKIr;vsZaq(gA6fmG&H@0a;C0^rlVp9<3~XF+8j zD14*I;wi(_IJiDB*E_M^{PzR3!y>dOV}dA z@+EpuZ^>J^a*?fg=eg#xth0`>iS=5NB{nWNU-dLbb<(a6+jt#$|-bqQ|RU)G8Vnz_$*acKo@JC<8q;4RH1zjV>{XaDfK*Jb?pF@F6jRO1wZGXbv) zi4|b1N@vmWu* zbYGu?kS0#`xJFBi9u1aUd$iI1#lQS%`;^@>Z@uvfrh?n-HC=7LdFL~hN7%2bQbxf= zz*pS^%M#f9yhAGlgl14-b9gxeu38u^Vt}FadNM^7xdh2weH!(rEZE`Nt2#$IRr5+j z=e2R+Y;Xs@npa@>oRme3H!sT}>c=0nN8G3Ltv~n{ zYlH8$Cm%k5{sNK71VB0w7$H1{s5e%$2_F+f-?L6Ib{7Xv(GUY#xE-7}xgBNwd!>{w zA!(-1!aLg_5G4ShxKD)Q6ou02y}kC=|Ki8(N9TSKqy2g2Xd92nKXW$X^51P`&$jv7 zIc-x?&lX*6iy}I+lV@~LEH;yuEy z!|Bd(_?zpsa~Nr`m`jx$u!{*D!3~a6`3%h-&OEIL>sq>mOZ>=pDk$-WW-RY#eo~1; zd>#Vp^LEQ><%Yz<#6_HWt-9~<>sfw@HvIJP?pHAUDg`QaP93MS8=_X8J^t{N7nAfw zcojbG#R&&b^%-#;=76uwFRuGeyau>GkPiTboVTjYw1>a^r2VUhY{F(pG($!0tZi|j z)YW#0;~t-U^w~fn;hJ~l)VD?jtp3a`OP(NRUMP47S~02s0Wk`X-vGcQC7=~YA@)0< zhT>F6hw&26GZCDl(6g}JYvvNCEwah{h@(`GxWi{x0-Em#guN->cy41@@F~aDeEB-_ zSbI4I7Jr3O`SR&)@rWODN-v)}!2o}kr5ThuL{N9(jqF;0&LSu-8_h8%wk)QN7MaY1 zHAT-hO~arv$a$k{pY!vcm5ktu9wa=+sgm~irgbQK$niJ|tx6^7=Qg|r?40ef=dj#b zTC%rg?Unqq{tA4BRd~XQmYOdc39YkExZK;G-HrRZY}{Ytv_@|p5T7X6sDN71vB9DD zriXQH*15g{x64+>|MH*yn|8SFa!L_>OlBFr z2-6FpfWfD6f;M-Z@QFI%>Z5S#0rgbCCS8QMdys*7jB%)dj>qs-j|t-1>ub<)%t2cA zn&M)bp04LCy7A*h;r-2LyX`;!I=9;)2rlEmd0{$fp_!pV;~JGqBp%@HQj3D?oOS{D zDur@X4MSzEq!3%S%bkZVuFZlbxOj4+jP3?}EsLmx2$jR}S&STdnbhp6h?17mG$I~l znvDktfqBoN8J|Ib6;uWL6w^r0ri#$8&}+{)x;#sukl9|)-AlVPP;N)mfMXYP&CMN- z$l<6}zg;4dKG9A)dP0fg5Pq3nRyxKEl5(Cf{CA5>*9z%<)F1`=9R*- zm&m*bCQX@*h!b_Ox5D+><>*B<%L<}aOBD{IbKJ}l0%p(V@HD$)$ZwkYqzywD0>+cp z8uI5bHHM$2w`WXwK+JQQ7a98cZNssMpk@e+@kaO~1OyTP6ITW{FRiphR4uWZY?VtM zEwfFJzU{U@aP=gM=WEzna|BKAJ#2)ej|w#bEVGU9(h7O9{ZBmgWf;+Z{dkWpuq;pI zoMe}j=Gv1d=n7tcvwiKq{d;Zs%7ymq-H%j5%M^nv$_GZM*!`vFK-bD49x-r{R?v3w z)$8rTYqziv)6fGtQ@^z&NFi~Pko}r;RreKd(mzGi<4jP;j;F7N@l3t)Q?M$Id6sMv ziOQI{e3`N$+|YOgo4SZ83)LAx zFgj_1uE_J9?0D+(?K2VLHU1d_3q3!|C8|GTua4FTUixCz9e&#@!8q<-+TvD+9hQ64 z9y=W0@vt}=9Ic;pp`kd*U^tZh+$Zd?oMUWIbh#Y0=-Msw;W=pIhJ#s#+TMI{nR|#| zxw+FGAFj2>@BXIkesquR+2{hMP}NHnVAWAvLxm-W@sBi12w9uE_2)?+vz&Kzjtj}Z z`g>o2HVf?+Kl}w=jK(k$VV8%lcDK} z4zI%#%;k^`hl(i+X&jO+U^NvpKZNfbTME67(jJSwHKbi?o7{-5CDjs&A*I_Mvo;nW ztfW^c6>xXrxYXosAr(w6?$4pcHV#}`xX_BYeB_~OUejSZ%XUD|Y1d;pyt&>X;}Y{> zY2dXTyM!yO=2ctu)DZIro*Nv?T8bxxJ>{)N&m|peMD4%=je|XG%s3H{Mdr~<%jen} zs}Fk&hfhz>F^H}qwAZ10`HjnM`#0~m2Y>#M4I&Ut(-M#ge@b@i65Ph_%m>~~CY(3Y zYkU6JJXUSD-n!nt`o%X`0eBUf?zOwW;xIdw3|Fquh(L2M7JH7#;-QRl2Ha$3lL6&l zvLShUn9mNwIJ)Aa{#AbC-=vVIEFpEY^T9{xE6{zU-teVSd+YCfy?e@$6>j!Q7 zV~(PMnS>gZqM3Ig6>Mcxnf7cP`ix!0u+@(@pJ&5+Ou}u@`X|qV(tZ3YAAj`?E5dg+Uq7)9tPRh zEkMMtzxjI1_coc6O?e0!|LTCVGL+CKb6Tu(kB>R|(8LhMIc(;iFnV^CAaI`Qu*H%R z^2)FnYhH$j^Zduk`u}rQvB&XvZ0qtYWndnihK3&75yBklfB(%p?R!7_sO>>-=~I;U z(JO=ZGmiYM7JnFuPhDoYirrS1R_5C4cb3|l+|%?HJanED?3NfMeDjr+w$Ae1YZvy~ zH{M|L!pw#Cy&pYkfAJ2q;S>;gXPlF~J^mVJYlhTv_@Q??L1Ktf4NXt!b279d4L%Aj z5zKd*HE&48pMJn6>GBQimw_j29CMs&y^Ry}a_T>( z$*8RdbV2dgUhSF|ZHrRjhwae{muij{HT*DL<{XniZLGiWl z=6rh!%r`EdZ-4lQ-)xsyKVCTfUi%3rSw1*+&J8Yuo8*P=S5IZbx2ofM>M?vzyiUfN zgeU4Llfxd~rGGoe2*Cutvw}Ke+Ma;`UvM4w zQQQ~9DP0MAhy6l{UKSs0EYtx?ny1`ByNK

yf7jNf^~a4i%Wfb{H>KYWE*KLz2;# zFD6OdpYg2^^C;ZMfquKJVT1Vk)r;+|+gI8S#_w~q@*i+h!FOJnZ5Qo>8QwXh6t={+ z=Mr8NJv=kqMewj|3R6z!l>e$c)5v5S+Z^yj+BxWOkptb73=G-y&=O!P3Sc{c2C<&M0@aZd8+T!hN?aI!h_LWQY zsCUk{xB0$&aU~|UhYW#tFsocX=4Qmh=WGOFm`}sDwt2z#rRXc5;v>udNGeup4W;7#Mb@;k3H`e3`~&)$bN$^h}eQC=IMjovVQ zl@awWv@_&Q#vL%&&^r_!yw?ly$F2*}*vDw&tjyMyrTduY%iMql`LT>d<^Jeje7}9; zcfQyzV?y_Qmt~kUFh37LsAma0#4*1i^HCo8?ih7gLYVaPeT=J1DHqVjzQT1G-*}a^ zT$T|I9Qx6_-s8~oC+rNlw32};$&?a=j0|Pw^t3|rn?HlS|3;>EmU2vQ>+sYyQYobp zXnhmG#kmCIcD-)wpDQ7pzOfmyL2)`DcIQ?uL=&#k z$-KOkVBdxZl7{cZpEA#~l%=+Ji#uxf89L7+%x|x>_Qs`4?JmOmt0#cr3Id6Rh14#0U-RMRr-$NSWawGH$GVr|`fjIv3y zPx;3P`JKq2zcM%bHj58E{KYl{@qP=|KFj?Mk$?5`1wndd`l-xfz8|>`Qwx@dxWN8FW>G2$GoU*me zoAoyL=yAb`KAlF(A@@;xogwopOhI;z*kMK5AAhpe?yhozCi85eGJ#;Y2#q(NvlDBo z-MEU8&k8-NN8~UqkPnI4rZDi>=VCLD@_B$g!H<7*h|M@R#UI#U7Oz{BDoOnUKD6># zWf=SGBOP^}cShf-=>ZZCdAaygKMEG6G9~rk13o$WjI+&rCoFN3ykO`plya2k@7y?K z9>5M25`Kmu{)b!hDU2)9B4PWiGaoB{<<^ZX(m(n1o>&J|dX5gWq8l`c8w=+|pw-uS zVOqwTK+2)d*vTkzodBmWb9?3^t`jGeP2nu|KyYINb0oZcm`uSF-Y9K>GWO_g{2~Am zld1I$k_mnli?8dL3C?ZaWsBa;c5jzqB+8Wqns9rdeHy)c&tUd1Qx?)nJGZA*H zj?zFp!b&_19Z}A2|Dd&Hvlgw&MeYysRs?Sg^!OV$TDTwAMun@B+9ua`s>wc8c?ZsP z(ox2paESRXcl&5Ex<;>Sg)TE1A&bEJ%H`?y_O;vGCitxV_}&hCnXC4vJd>G`%Ixe# z(ztnD3ol18(g~#aDJ2^{@4`h!NawhgWSySdwQ1>DCAMIe&HO&^x~I<(15h((3eRrtqK3p}<&Z|{` z(WubFyD?ywhT%RpBplNrOksz8Ox)x{VsX*3i+kqL3m}9TmfYK7$?Sy91`MCMUJSEY zZUkib{Kt%)zkcUZ`{~`S_AV!utZsn!3b!zvEVEvI1s0?1G-g_4iI$C;BmG1Cx9HWk zrmizY=Z>5y_C`xc%Nv5GSSbfTzs04XBRW+i6H6OftPW^0^#D6wR&>3X0{jL()z7pE zt~gl@z)KoU#+&`UKl>AWVTP^M7=tfg+v4W$N9}JuQUTC2uv{eLZ(m~o{)!v|H0!Z5 zP8ZyfFJPuvi<{=8#~OBxLIfOPwQ_YmZHLqIO+LwLjFY*S6Pt-S3ErlAk$@fckf`E} z@&tx72a-bI+Ug2>N^f1iMrEJ2Pd@sjeTlj3?VC5-uQs?_hsa*GtWxHPBT5{uefH$I zJ>d?WH#lQTp%5dFHkG3_lF+g!N4$WaJ&*WF4@4$IIU9^@H0Mf5uG~RUyqqLj`ySTj%VRH3#rvx{7chtmGdpGxJDUCUpWX zoj!g?U`Mopp1dbVUx=H$c{RO5P!W{|^H^2b_(*@x%5u3U2e{Ltp|WdFP_|4H$K@i+ z?U6LcA_jDoLkBci9zt+|BYZfZwmoN9?1A3vM|15q#u_WEWgc?A_C;>>`v+fLX}`D2 zErx85{1vwFzXXSy*RHqQSKv`j(P&f5Rl&_h!#Vmo+VT63@3#*>{D|pHDF-c!|8%_m z|8~V+)gVZWTHYG?F0jVHI{W~aRuk?_82&HEfea>+%`V*f4h3VZe?&pi6ux!kLHqrC z`|W4HB|b0UhOMbsN-(W<5*}KJfT8GNEjDTLq3ntKgkuE;!5Gunu$KeR>%F7^x78AvBv9=yenncib39vKCBwEz%q0Qj%1u zgWY^8)CeiWEyWnL<6AnER$RqWC>&yS`lK9zuea11C@9T%wXU4QYfv$Vu;jdE8Y-=& z+^CSUaXZ4})oHHkJZ@Zd%Z@O5;Iz~3eiCLuUZ-paoN#e@g)26>A^sWm&C+Cxp81pA z*>-zjw_TrOXw2H>G9!m;*f#%{Z(nKOV5iad?`^jq|NMh?ab~lfym6h|Uzi zTM6%z<~??5J!k9XA$Halsq>d~8h^>x(gm;2xp68r?^iCf-+F74olcBEso#S0Vj_uT zP=0tRz@c5ezL+52(!Y49aP1h*PIEegZf2I<9&>O96@BZKSKFnF2W@SCD@92r18@P& zvcz%YtsCtbljr^CZe;G@0&1eL`jh%h>C3OujKfBZVLqcF1jxb>GKoUypfB7XcxOvn zP)XZS8R3i9L|{P@E18?#fiRZH+6~Tmx{cD~5|{cO&0lMKwDKFAw)o)T6Dnl0eFFx# zbA73O$f*_wRE{lcgaAR{jiW=W<^Bmb#sA%J-Nv{81cE~i5%bQs_uGoT_y5=f*Ab)k z3RDm~O=nxI5G|_}mfmAW1@NI6o(pTGARhp#>5 zZ0ccz5k9K&pXJftCa@ApSHt;pR~FlU_mBP-I3Bfs`hWiWcK`nKjFaRXd&rs|%z_1|Z7dYemQaaDB_O(mxGB*UA!&dbOrr4ZX;-K%D zwz0O}o<4il)>fag{cx+jedijps+qP8qsXfB^ej1GoZj#<$FVFRAgd^f-v4y1t)kGo zhH}rcEwcS>XJUXPPN#8I7g~w7fQZHZawz5>EL;7ic{i2GbD4k>19mYIC5!d6OnC zi0~eKh-YI%8sX$JYs%uc8tVmyxd&*c)viB9nC^4w7FY5CyTF~ID04m?#Ttps*Ih&$ zr$>3W(Iar?HSu#$9*wtiWOo8R+(XfHyvWs`D4!_kHn!W|tz;ek-nHfS;pSBP^{=0| zkMC`@ORqd>=dN9Bdo1;+oxgZ}rEP9-;s!gjm{Ei_V}TVj3-&Une-ERq5PIY3di&{b z-f7?Z_LthNS8ufk_g7O-UD!e@Qdyt6Y-aZv9~*`aFToQ(6@MZ-6ALH~@vDT+V^mgb zZfvwq?mcMF)~4E(l@;tncVG<1F;*bI2+=L^ua1JSv}jo5v6^ zA_{)On1eB2;aJqyuAXZb+;@uMx@SN=r}uia@2vnX`ZK@hkT&;y?l9~*z%s}U?$^$* zFfX!aV59+~DjaHcmzPkEF#C6&c$o{Wes%v*dyi`-uYCPFxNB3)FqW_g0#9*LknE9M z7CCU93sXlCg91nTD!`VX|1tERr7>|nyrgv#nq>uj0l_^+aX8H}C#NvF<&#_)DkFdC z!D;VOb$r{4S?`@DsT&1Oe4v+U6;!prqq@W|g!cJ@OM3jokrXAg^tR5cb&_;B+c48nd*jth*|D-?FH0{>jfEx-RcH9iLOI@xYy|K&xF5f_+P?YLdFG)E z1JMG@jkRVdUJe|DQ;g#78?~e5_X{3HM;&-Rd^8Qok z!h!IdD|NKTol)F}ME{x|_ZdUyee%3bW4DCf*-Ehi*m$zrUcbUI^0%hj{q@84WR*=0 z(DpIAjpjK{XL0@#jm$$7b4Xn;Rthxn!RyPM<<)heJcW-o=(Xc7{REL_umg~`zFV^T zEh^(@-4^KynRGo3!Mp}`j|aVGvvvog9v`f=?WZ5NPd|Cq?yq8`%#dt-V;wrEq-Ss-nf0SJ??$G;l6dDW${ul}{gbkj_BL%VK9b1dcu#h3phwt1C9u7?^ z2_a@P&?lrZ*#P$Bcsq;pfAIPx)}rUzDvSE7z&T)<;|Kcg*7$7QV;L@=rL;_z$69-Q7X>XNyBltaa z&y#oxG|YVt?e6c~SZ;s&%dfYGPdD59&tZ~<xr1gxSnV6Z{cN9YstA-@FHID6{>ZEoTi?5ShOtp zpnP)8-?Nvu(UR}cfILTex8vy=i~=YPuow;pw(#vBJ8fKDW<3>6@g9QgoYw0twEdy0_eJ5^r&u9+w9Akg3epHcBdn=|}MQP3rp6 z$!>e~_#D^0TyDQa&>yU#NTp&UbVU-<;v|WPz0^Su%bsw5dgRW}eve&32VR#qtJUwo zc}h@S_o}Zm--$8)X|tbv{PWD+-uc!2_HTdmsJ-(5D;&4@L)z;0ks+;c+ zP=?J*o<)X{qd?KsikFr#a!_l8 zo5$+O-d2ILx7AtE-je4D1}E+SAe9Eztt)AZbxt96E;;Fq4< zKx3Fce+l5T=NG&p)QMPeOkqC8$2q*OP(DDznlQblgWK4QJCD`k$@!^U`JOPPSzNwE zSuH!{r*Vsd5g=hF3@Oy&yPN5R@_L$OdU<+YwbHT4VN0Rw=X)DZ+w28yPG83Mn{9T} z440+RAwBL6h8WLX7e|wRec5e|SSqt5w1nyDCcCf}7)jk=H~DRpir>0(qy7Bhx%N{G zVjgfQxR;KpOba6qcafhS*(C{$*$>>B#<1UB_=I7yLVXj()$L0hvP}_=Fu&XKR0oFF^9buK z1G%YrH<^1H16EAg+IG+$(6gUIQFMzXfdv``ZHD835dl(IDN~k>c-qq_d@{1jJPNIf zs+5Z|Nh^gyQE`KN4jq@I#L?d#Gsnds*uuBB)umB@_PP5982qn4X-9ti4$i#!PMjXE z8V^#`f5RjK&XndECgInSgmZHhnXVx_bvS~LJf8X^6i|=%=Gbv_fgLvXo-AF#Un&4k z5j1;U3WAj-h9LB&Hi{|(FV7-mj3m$=v~|{0caCU+l0A4aR2J8HdfbdsNOulFKhGT1 z^Pe}l*nA(@R7rZ{C5$pOH{E4ww8S~qbJWSy=96}tA@!Tw?KHo1v8}mL0>$EpFNd;% z9FB}T4+n`VyY%y|(=q53k_qYli6y=8I2(s(ldb~s8NH0&<^S$xo@`$D?xSZ84F|7@ zR<31A&~;9yu#-5%NMiR8%K(JjC7+oYhE}tjm(Nm9&PHZB!GM_!)_C6j_3`Vgj|9y!6o%LXzh zBho;Plu^*T_u!*664SJWRlKWOir%SjUiC{_e_Q5A9-0%3Eoka!w7i|+L_tJxgb5VOSQwM29kz^==fWI@8Ez=x-Hj~l zB1GpP%3uv4<5lkeBJzzWUltwr1JeZN+fGhCzW{Tt1)qxy(O9_>^m$^tO+!IC=$UM=Twn z@IbH@XyIp=CoRzud$E>t#IL`4v7N`(mux5o%)l^BUH<~>uQSYP-TCIQ&A;^K^7p& z+~mOdMb4%2wgZh^G&XTlKPD{gDs$dfubyY8&;=S9c3i<5>Js9lhh^qtz`&NP!s6$< z_0(hZAiukth@WVaad{sHliTz;cp z{rNkkN1Ea+VGqbojSwMF#5r#jZ``^JOogU)jE!-}F6sS&tIngwf9v(vax2^Z7UpR# zoj`0`?D=ySv2bF8G{a!nS@lF)WBB`gbu;G>U1hR#fg$b*hj6={&S9z*MIXxESo#M# zfw%8mM)8A!gNk#IyN3a_AWGkYW0v{rJUc~VafHyH$c2P_he{t4tEp93X`8}` zF1196i34j#@e>?ntw^IHiVF^e_+fn^7~&LIAYvX7XTpsVe62e^)Kn`xX^c=bseIaG zgyELJHMS}~#|-uWrP5`#%f((=ULcLuP%dSu+&enP&;%F`k53O&A|VfyL!n*t9!<)> z4;`N|L`J^TkR%d6(q^8rEJN3&rStT>2%7cCp9nf-Sb_(HKeNBVu`R3$a4+#bjouPx zX{}tKMrV#tn9)G1kfbi{k*}<9&CB_V46*B!kId1@DX)BTR-DDKzP%v3_I! zp*>8w#xaL*A0xS_(A3K~V%}!vFI@g^&p#EGbYumRJ+t2?oW*cwA$Ih78c&*=;?zOL zqH;js$_Cx}HbTsoGf49xzyWB*rNSMW!C2>)mfM@Jze-QW{E61xjovxZWuALbxj@TH zqZkC!+s-3I&tnOrK%S#jzre8R-0>#I*s#W?ki+dZEXQb%RZ2W%LblBE#n*1JM$7j_ z$gx1Lw#`z+)I3J)uCH<(=HZd^EA3+$%EBz-s64Kd-UHBXa|F$Tix|wIy{JqEkl9HD zcMc5{<*uQx!{rJRjs;g}R3jOlB?7)Bn1XF&KkEYed7JhU0&_7r*gBT+x+4Cpa^ z<^3h`X6Dad`mX*~P{k)=5aT;eATLRYpD@p4;+JVFAU2=e9fGzHKjw~UWs;HU24{A@ z=BND9whISdASpSKzw!JdioM|y0`>MAZ!o81O^+QMM;y0#I=93(!!%@K20@x(j&wpL zauEVOK3lwivq0;sMac{dvdp@k@hrUaLWj`8_H>1ZoRsh?7gBv2>zYMi*jsAexCN9G z^30t!^y)`yFJ1bfHMci8pw`{Za^Ov+R500#I-K1$4Ij=ept_ywr3vZ=| z^eRq!ygd*RZff$7p*6@QAFR`knU^{o*CcdhhB>M_D)Kp`&eqsc=h3Ht2p@QQc^Ud^mF5;nIdWie-UKCA`d7Gh{@2HFUE=VOSiQR)7Ir|+Qj7v*y>yr8RF?Ji+g%4*@kWIpk-fCN)+r|{PP2LZbaN|0VecqV3nhVd!QaGD z7~l)j^+FGrWoRh#IM0*iuOUpYz4bOd=nA_%m%MCkw`-G|(+_F+Pv{vB+$0WYJKqS1A4Qbl zBQ2a;+k0jsfz=KUGPlCzeAcjC6e!sc0OM&O@BGm-np8?<*AM9wO5vq`psH1X*H9Cc zzYWG{h>YOJi4y5d4`uSS(NWQ5;oUkXF8k!==JhA5|4&)x*4xOD9pDmgB1KXpMO{Xk znH@W0XV*61tbuIoV1Wb(ke2{KHV;9NhXDC0`_BU8KP1R{FYwM}JTtnCB;F)ayyW{% zbxT^iM;tc0tGcVsy-uAvMII<9|M(-;w!%!#A@2}2s)m*DWnLqRT)1y^U4yzV$RBf0 z;aRQTfM=I6>X*=L<-Op+X|LG9f#Hfl^E)^K8=AxRi}qC50DKDvB9SvhKJBM53+fj) zV=SC3aKD@Fr02LNx~u{MSkyz~ZnYVM^E_~V*sY|){x(VgPcj0z$vF48cE>V6R`alm z>#+L~@zYEZy+4I{rL%%vflsE@2UvPf;NH{seF;titWhb>Ju7kJw}<3bK;OF}Ag=o# zoSRL#qua>q-i$R;CYDT6Tr zA!mf@BznpC@Xvp8oT|NNX;dYLExyJ3C}o>J&9n5g-nF`tKM+G zof-}oH&MPIvg(xmZm`^#f?BfI&LXs}y|#^}kBJNt5O=|k0UwcWO3 zR)KNsOT=`~X4@#K?2uccNu%A)chgq!)}ZgMXRhImo;#MMJz6`ayQvTfR|A|kD=eZ` z*QY>#EW*bN{fZ!rr%cmz(Bv}&+>|l528Wi=Ych}QG+E_Fw#ETDtjEkktch9Z;k;@y zL(r-+7r%Xw_K0jcJwH$H`N+J&Ab=!Y31{FUyq$C2&_1M9^et%IjzqcQyWmB?N;;$} z_{i|}JReahPBD)Mzb-EdP~In8{^Ss;Hl#crPV=?L_j02tNugLA5DaVmxRn3%1pqfc zhjRXlF;rPDdqg*kH~S1)z9jbdAHLX2fAQ(_bh~qiiP}u#i_>&7zQ*xHLNexLZG(;n zO-gg-8Zzi+VPTG{d2U#(#CxHYAQo4Lv`BqwS0y@Lk7rmyCr(up31a>7*?=6qS))1m zzkq4Sm0i};5(EH|x@oeVaK4w#^9W?m!4U%A1Z^$K&)H>pO}^uO#!X_@OnZ%pzyhJP zN5)ih>wu~evul7t)K?Sm4nf6BqCHL<#rr~-$zZ$(V%22U@uVf98S2=U)9gl|etv|w z5()A5fMf6T5}A{~IQ%ZnWpNbaX)E#pLzFK}Z2Jz)$b*{2ug+%a1M6Dd&-UQtDcz<2 zz*7NZu!ZZ0g}>GB&}K^(21Fm}kHIV(0$(b?v`e~E+z@!Bu}YwmHP5-i3mMSU1pojR zS4l)cRE3VFT~y|?1AH9M8q%sSL8cAZfJo<%W!^R3Xcu>E%^zN?h>o(Tf55gH*Du(Nc*aA6QzLq_^oMo}aH?^qF#LrU5&< zT@rrwKR1K)ehQ&Zi2@-sZA1D}5ol@?+XAk583ck;gJ^RI7|DsJ*NJ|mI+U5;Lt(wV zGK|a^vCI(Hig3GNVD6jA&t&A9Q4(zn6RFYIg)n6br*9ZT(?2(#5?wb?`VJ<3#SU;D zOh_~iBLspimg!`$O2{6x5RpR&f&q;+AVZI`8bS}XjiZUt50iUKWUlB@>e_zIV4XSE z%Z!aP1u4FO81%}@Oors?#g&L0W6OYE^Phk5jsP`J;JGDp#@LtwdAf&21fx2R z3dR=DfqP2i-R3bRfycYi8cPBp1N?`F2jB#!8=YaW!}}eZGklPNu?Sq5WIkmD1&_nr zn#GL&bHo_g3Q4ECMTSNDUZ-rGzShXHxD=;$4NENJm-Yc$haHA{ zuF=ACqNujQ;Yaz|3a5~#g1DqfI8pA49bqS{pnpgUCJbZR#pvWp6AG#$c`w+A6 zKp#PedHL4%0@J&Z+Lawl=cn91U>WCmXkRxEk6~QzAbt9`-=uGTdW=N7PXF1uOW*$2 zw+!EOv+=UWhaLFQkxK>21d@I9KaS9}UdtHu`-eyQz=s!QDN!Qt;rb72#_zIvanqPx z4^1wQ_y*BRP|c%Hy6g0-&v( zxBxC^^iUQ4<-!1!n#WOKBuJJ>sxjoAsF>AZEU#idQI4<29^zi1(B zu|^^r2BvWXjZ0k z=5bjne~YAy-17PZ_xaNMCl&839~gIXfhd!u0~N})S)e$)2l>YQ+Q;ox`sdI4>0iF; zrO$}j)to&YZPK@B*Y8$)LHH~y!rUa)B!SJWF^Nq6WkAH?!9hA0 zKEW0vNgC#tW4tMjs}YW|)$EEmCXV#kG z46Sa-yrAR5sf#<1t2wp8JT~?>sGh#bU@nb*U|M%!H-$_H z4PmND7fhH5Hl|kSoz5GzrZAN0Wh~>QQL-?PTE(o5<}S|Wy;Br2mCO}*De(bwXC}=} z4aXN{cw8YABwU3+)QonOxbo#O-lYvPtIwxJF@J<0|YX6|^&P(07>P_Gcmt!DSm9I_`DqPhBV- zD0uWC%;t`7Nn=ZTnE^PIx0eVJmjm`l4(G@Bp@T7*(agg^B=;Nr@%!}ZzkieZ1a18F zS6`$ze|VoR-jm(JHWf$a4}O$5DzD%FrbKk1tb?zkto*}oX(*)YedR|bWWZqDhikw+ zUsTnX=Uj=?C}xFFK~x3lR4|9pL32rVy>9yZPj}N^n+=y)P=C()$2BIDi>BYK$i2i| z=mHa`x}M^Kck4u^dB~Pw?p&W0jtQiB10X`@{0I)Tvsz9KB|+}>F{NN+rL7WHtk!FyK+5s+LEn7&*Sr%We9ukbt>EvI?m z9ZU_aG3R=OR=y+s39;#TkMU?@8PqW)>-8IcWA5=hrMz0qHru`ocAlrEz|5-ySA!M= zs2bQj)#ix`hGX?nWYdH()B;`;)4{ep$f1#k!bCz;pjgiFf8JYG?U5NaYkG-1moEj1 zaLSL7VY7y(N(c@a)^YUl6Bhn6CclPxj0vvtKIf(tA`xb3TOc{*n?8X$0V?4<#0eLX zRlK1YyAIfI!#;Tdf10e0QRl$MX|olZQrpD^WvV!mCV0*}z)>@Gf+miQ$%K*rXsi8Q zQ2z!v&zRF)V^(jNYhRMgP&0l+-mfKVtA_+ywBZPGA$^7#=Bo}v&zT;2_u-bHj1-9! zYCCu7{2YfFc)=b_PtnFL1nW7^nc97FupVCWC&LMeIz& zj2ZY(Cwfg;krOFhC1lwVWa?=|irf5|-al9NK*f%q;HWTb1)S!YPL#*36#*7AEYe)i z?ujGPeU{%bhNE{9zF=7ptGZ@70hS3z1}qatBV(TGTxB4Xd09D1Kgfh zNbaVHjyOmHFEGFL(1dc?;p> z8PBvmGQS^9Rhcu!ix8-NmPoDR;n;W;;f47bGOSGEGZZSkfwDN?(Ck+T9|IM-1km(| z-#zE9?!8irH+0S+qyV>xJm?oS;|Iw7JOz$|Rr*=s2|r~$?D4RkPFVW+mIe3DHdI(Wv|qb~u331` zSojLSjQc zh+PuBU@VkZ_oxN^5Jdx!MUI5(M<1BRFZrv#nJ>JtvB-78;{+P$t#ayFkEM|9{>2(?ral!@=RJh<4mcm1CF$23`bRmZ!NjL>IYDVM;M2Mi_wgepGL>aP@Ia$;k zZ{xt}Bq`3_mt7+b#8Hl+_nu-?V7vXg>vyeO4$MJhP(1jFwqwz$`J^*x86|W!Vz= zoS+Y~UQr8kd8+#nQ@&Rt3n#S4vjai+R`f6*q4lS@r)|%M`QkRyMbAlcKg3k7I@JXI zx+P1%NO1TD{v;c}<7dFnAO7}!{=l?(%BR3JcO}!}cvLVdl-GasO^xDh0^zfEG%V{@f{{A@^80_M#aSGWyDwFQI14Cf2YVLZfJn`?z0CUmO$FGl4mf(1-#gF>|hFJ=k=aBi0}i$&Q(jn6v@PqOXk5ArcXw_tuv= zf4X4W2fr8XMKCdfY8FfN+)&vRqegIfDW-I2gMC&Kdqn=IAT)nvOCU5Azk(Uj4BhYSl-_yo+RYZyfQPA!14hq9^pH39SIW%L2z!&MGEUkXg?-5 z^DZe|dPE=1Fn4b$uke&X&a)i)xUy)w^~+u z#WkDS0 zjB(I4%iPZ9Fe+&=$4#=?%~-b8AQuoJhwhK+(ZTF;3r2SBV zNmy;7Zu)HcBoWS8rJ*Fd1z-ytRHp3=2Y_yUOz0No`8I(g_t~L=Y)c3G=Lt)g0L> zh%ullhXbiGMw@7or~&SN~FGa{NUn3Hu( z_=1S9Enq)_al#1}?~>sZI`qR%p~+B<2skMNk2xl7jdG4$o!+T|-`4yx^;nYEA#||C z?(k}2Gf(t;%=c2K!PMUlK^i7Rx1T5+1U~Cy(X?i4+}iHYK^$_7Rc)_=ugz550EgnB zhruQ10cdu!@}C|d_*{Hr7Mar;3}idn8P)< zSeGG>I~yDx9;K_Rt2Da)fOf@s1+(keTJnrF775^>iR82*y!9Dz07DWMI>a${UYvzb z#CC`iK<0mf4>08m{HWGDy#`Is19%%7`8GrgO&)2nGKy@u{wpeuvS;oOBjiGkzlxF6-M0( zGZ5xy5dkEJ5P?x)+!$X%EzaG>+%fM8{7_d$ESbue#=Is_cB)5n(7L&=N_+?-#=C0x z!i=?kd}s|1Ed^Z0BHD*KkO}40zG~QvYZ*2RIBtRa=#Te!h^FrxL3phr5KZ&5mxp*v zcSD%z7<8(|=h+MYIpp9nu#i|8Q*+168!kq7fq?de(_U&jP2)QL4Yi;js=!!{bhytx zNpH_$HO?GiE&lbCUxT<2n^QY0pp~K)+itd*6`1l~ZJ~q{uosI4`hB9D`efl`Pg+-b z>Buv#*r}sUs~;8>16J@Q^=1_NP09nzc7rY0Nf6CD*E$MknHtv?;yGvKP```$%T^_C z-d@GfO<_hA2WP>&O_+Cy)paNHid!Jz(nR+>diMMf!9r{^v==F!Y(oLGU_Dxt-vmUt z0L%g<#l>ht{AYvn2^BFxt_dzi#A2Z>3uz-gd49Aie?O$Wk! z<6LJ(j1|LtV+@33H0$CT=Fka|1qtIQ&Z}reTpEl>yh{6R2SAb$T<6+s?~xBmGcz-g zOrjK4@5Lcdg{Ff_fy+mea|?uSF(si=aU2E@MS^ zgqr!{2tmrU6e0xcMDq?u5#?Nh=Y5IWalabLCfZJ)afbuvj(o*Odu_A}W+qG;Rs(&u zzALNjgF4Wx84V)n4iBCad@)Y1UcG_=#xT)4d?q((aD%6s7nyjjfq*1@CWa<=2#(eD zDcrf6s@+4p$oyt%(KI%i%ynp42N@WpjyoMNW~cxApr-|s>*Q)<6|f?kJm4ezA!I-? z)Qa*_jD0~huoux4_@1Y?t05_I+5~h!ebDC}K^pdLuz!;Fj|S=N{dM~7Z(pYm7x05f z<_L`&C!B*TNsONwaT!qF^9V%c(;!e8HzS`8bN={{P`oGNk66+2%by*jpFeBizDHt` ztol2=a&N|jgrWU9bTUjgpMhCX0oaQfZhAVA$4@Yu2x0NMt6C6ZXjse{S}x(c#t}#a z0%S6S@7+4m{7OnUwSdl~1w`x^CB}P{*F?(^FOZ@pGSc3y6h)uFjdGm1pj)9~dt{L4 zH`ik|3(pWrQH5)ukFrcTa6#la?r@iO3%Fd62H^3ujntBe0eHX1R0FG79P4TgxUY>) zuuFp0TMZZ0TR*=yoKDnHo91^aSdiGj*=rH7&kg?t;{ms9;s!$KC%C1#wmNH79?c=X zcx=X5^c`Wq^%(PmA9NwOpq5SHBW%3q+@%2$Q?%@7n06oSchdJb>n_=*b1uAfI8ja} z)Tw4#K-*x-nLCL zs?%y)wiWogWY0q%2w@(PHpupiyS(clo?`a(QCpe|tPayI@#JpEY@xHs_pfi6w_l(@ znq*AeJt8P(lp2e12ok|4Ai|2`$Ap|?YlQkiSQ^yOfI4+{_=xVyVM!QGuCz~JsdLLj)iyW8M1gF|o+Zi56(kO2k_zWHr! zZS9|XUsu~kdOfYn|}}B^%Q{r!N=O^3jhg#3IG7a z{}r79aAj@WtbhQU{(mJ%uP{J303Gdr>|c)l-y5_y|6yUEqhnyIu14tDIOjvIWYk-`Two>|9yGw2jF8M(I9<8 zLBaKt@7F zMMXh-^Dm*I{P%AxQ1DR!v;=6pbno8C>hOK`2qi>MDUs8qCu+9x{1KMQ&oEcoB2SEQ zu`?imD0{DG?e)F&!y=>LKOD$NC`hRPp&}#y``14JNchMow7dkU7qUA4dPynyLC5zF zt$A(-AxEf7ZzQ8n1AX3@4FaDq>vt52-c(jv1nl|Ib+Z4IS0%F!P)?j@yK53}bXo;H z)_KEne_Hpj(H1&D=OnVw5_`Q7E3Jtp1@ zKD3X_jcPe+}wj&p%zav$B5E2D}#Q*dm4iX}y2D5S7EuQrHR zeN9~6sQu{b#UnHFZEVSnT0$sCJpP{NkKu!-a+ral_CPuQ;C2GVT!BG6QUNYU$*(v~ zk{Vh#R?JvbC*Zfa3Y(U8ynYrspk~TyKiQ+ywhk$VQDw>0gHWV$fTDOlxZ6VkgRN~S zb(rlQxEHP6WQ;xjlh9Paa#BOc$B;(3xMjKfzn|P+MRUxJkM`P1mq}`K?RLKP`T=Uy z1YQB{^f(=4r^vtuy~>RuZ)s+dS?*& z!q@$f&IAd?Ncwn}olJ9e3(a{%nrh)NnMMa{y3%h8umt3pT|D)a^0B!wws;n$w>#lC zl#=|`{%4ht%@A3`nFbw;BByi<1;H>CI-xxZDCZSNUu6P)A1g-${!DW2*Fh!Hy6J-= zgzx2<;opa6`GObYz|m6^-tHN%QNUKk_wxo+)%AQ@ih=s~vx?RRZ;XHW^jK8EtFDBr zyxf;3aEu)o-+81;PIle0GAwFLDKuDD`@~h&BZ_Ig3p9?I2e&6E6;1c+>H=1hdS2#s zy{Uu~6S#>TSO5TPaAa&PET(O3#`WPfv8U7boD02K4 zZVwUDuzQiBZVhbQ@Y3Ky7OO3lQ%Vd))~7Kd{1on6dBIrZ>Qx;IkMCetqJQ(Ux1~JMj~TGn)-&GYn49 zfA3U0I6Nin!%>QkicV{aIcfuhO8r)>Qj%}CZC2Og;FHG!DWmNiegrUM3yUG{s@^Ou zRs9dC;l-Ny9*3O?$cvv%_^Q7$Whm}6Qa*kQ(PP+f6Bdu-?0g8Gg!AYBoE2` zo{zM1%~Gq@%UZAyL~FW?qV8+!TI~9 zxi(Uoa)8HUv7TaU0Tv7(dL6~7wAzy9Jj(SwHi0{}JO9AP1Xi_4YAvZhNpVG_Ula66 z>*IG4Q|s;(g_p*sdz4$2!y}a>R1kw{1hc~=SN{SH5lQYFfHN@MSL`$craiN~zIDz`2XTvs;F@}7L6nf#EWXikRgW5 zx5R+T$hg$U$OHw`kq2~Cul$cipFE=Abzs3nQF7S$;Fe%ckv=<)uTx)pQNloyRdKjk zfq>u>;(3NO(J*MS0_5aB7wDY&GBV$K&R>@*{gB0uMwYN>Ac{+zVvB*IJ&_-T7=Ma~ z(RCu01uvA3t?G9z4MN_j%Qz&-Mv)tca>l;N_Hh9e3{u8Qnk(4Ictheij@)n0Y!=Ep z)yHMY9Tn#5iz6BSO+3-}gTCpzS&jE2Fu$6dZJ(VM3ecgnJ6UrIE_mLD>=0LiF!_<1@(Eox>DbZdjA!fo zq2|rW=P$6wpKd=LysnUqI-J^4jSpm+TWUG*K>`)5jqlAJY0t%hD{+$N;+dW@|=!$z{By~gibf*+gH%~@3cB$(9w_$EC{hUmd{RfWLl zR8w{1kbm#8MzNFr0)9^Ry#6ML5D~z#wk7DT?w^U1H#gB0B?dR7n-Amq4B1x>EE{uL%W zh63q~Rx1Ap%3-|g51ft~yjWH$H3!$bc)A2{=xIui6|JSQF_g6hN!^EwoxikO<*+zf z6v#t|f<8D(oqAsNW&fEQR#QQ#(t5Y|T}{>pQkimHT4lSDy~w%&QVi6zT2vyI> zw-xEC;intnmP83PUJp^>XqvC=473jxZu5VB3?60za}z&KOHZh;+Um zlqf~wmWzie;BAp~hABYho&<@0KIxA#M|`>5snxkYKiR!SoZrZ-d2~!p+E@Am1MMr@ z!+By=SjZ$ly}^7iZm6}>6j@3&1Nl$g!_VLk6Q<01$>M7ERh4wUI*UfM!^%WIIpqgB zKI<-Xvg1?hOC9X=Mfrtx%jd3JDj7DAoD3S9tolg?^@{Sc z2p5^+!+F+nVoT);kpa#1Ki-`Zjw^>n3%fyk;%4tkDb>jKYaL{7KWAj}R+Abe(aaN> z{>9uCnAD){r`4Hc4~*V>^3cevmBbuS=F`+SEJv$(5yAVF-n_cPYReJiSZuVZ1s^zF zJ@uItZYy_VE0uEoA^Vl|Akj~Mrbu}wcht}2>^Z4fE%*zn;e|{SY}Be%4Q-UI-mV&X zl$^uSIFiE0fvMc;A;Jx+2I+a;rY1Ua^27YY)wedOV8rdaBY3=7lh9pZAMbhZVlXi> z9cAUEYZqlZE7cU{38jW`((TXuH0#a`67|_mXN*T% zY(1Gkbev$2%HBmKU9)Ccaw@J4&n}=ugp^(x>I-^4INbDW(!3-xBd_)i40cmKKdXrO z!onb6f0OG_S#szUqwJ>5aWh+WCpGP3T^iWALkpgPP@W}&ZfHJZVb|^m4uez z6(pItqt-8VI9#*reMMybA%Bc ziAn(PI70?|njV z#!&q7t~O;+A8g{tN5~*tgdY!f^gF*K%Zv-eKlf% z{AW}Dpv|7pPi4W1Uk@%r?h%(4SC`+JIyo4vnj;rdO7|?$Sq2xYTa{zs-lNJ1GJiuT z?9*+3t#0LkY|!Kf6IkO8Lmv$ZT^e&(7HStCd<3O8(ed|dynm$pN#e}kU)(^O{NWQq zDFQA)`I`9qtEKW7)1C?Sq!xVW%Sx!vANa2Ndcz^?j^vzktF`}#?~QhU3y+wIX-R0M zf5zCi-ZydRAufKNu*x0pQ|f^AvQ4ftDiIsIE`T{J--o&~eO1I?JA;J^oy5~GxV`~S zRepo=tR{j93>Skrh3JIulKb(zDn;ByB9y=5eb)B3BF@f8l0aZNf5b*$elPx>vKOt7 z#n%5_HP-!d7@ORO&GWjDBwVmz4gha@H+r!13ixK7ljr3k#KEXSYQrPp{a^0(Lc4Vm z6n`&PsNaogp>PX~CjOMzXoNq2dR zDjlcdg25;7On8IkgOwg-YLtUVZ{cR_hMi-9E2ml%0aecgF{&V$=q4%Kf76&pfBoHm zR5Z3hG->eV2T}h!e%l&Ji!11mQZhsfFlpU3+vrY=L}JBP5|7?LMzio#NOlpOC5r(0 z?=cAiU94XLw$IZ+-d8w1$Rx{vuo)cW?}!m6;P?bl@GGESg*Jw;4bjTaB4?(|MpP>> zSIfTlJKlFvt0y!y$^w697Z0pZw`>;s*u^!7uAsG@T1;< zTQFer&B@Z~2kQ4N%S{;|;Q-&F?QvJ}_g|xG=!xt(QWIj}l(zh%JF8fOe;hB0?2$*}yS|)?U4;f<3|l{bH==@Hw_k6N zaAclD&ke2If{@`$t~IfZ_k$*%(~6Lh%JsmsrE;aTfUA5K<7d5^4{TGTH@F~Ed-NKI z+T4uH#P;-`OQ#EN)Y5-`Cb5O2LK1?SkN$boF5Ne0A)0GK`^aPcvCX1Q^kh5?we}~! z^X_GBWK|cNsfY$|L}wF^zj3MT4CN!-OYE>W}QsLzs`B>NWg> zAvG+W=unj6KTm$X*#`KdUWkNlbgR)Gd;;l8tZQ@ud2r%rj?G?Zo zAECgW(qOU!Z4g^tmph*&TwPwG;9dWB{imS3?v#U@h_+v_3F9XAW!Q<~B>T57HzLI? zMkzQHQQ*r_aGA!8Eov_y1u%6zH^XMHIeF7PuJA{fxQ0ou4^)xSG^Fb|tksw|8>iNz z*bi>+q!)!DL&>ZObI3#U;RPSQ?;M2sDx~DeE5|Lf9mMDu5Ygb-W7O;=2D?m5#=QcV zw?Dq=;SRB1Vil&Y(S(1~e0Dz*=#D#LHUR7vKNs1Gitu(F7zNPREIA}%t&RmYA30!F zk>)Rhi)qq!_Cg=8*94UD->DSeC}uAHLb;cxWZrEX)?+L)!2Q_beTQaKCA+>e4-ivq ziEi`k9vWerrZz3c3)aAU*|3JF`=4A1zXD*t_zgw*2KhexV34lHWObkwD-(+>nNU9X zrGU%MNs!3e6XXnm|03d8*zQ{BP&z+(o}YARHw&B$bjoR)Uk+w#eAkYRZ=8BsE2--L z=pFCHcYXHw2b#11Jx%;6Z>rknvi&%E^i(8tMlkIm5yzn1Fw$U6s2g-SSXZjNVmC80 z#52s|?R0B(%?j~x-6+6%yB)WS()tdow}lipt*DjAo>YG{CzOWmm$s(DAH|^b@4FuA zY(2;@u>r0UhqM}i&#|J4g_9+4u_HF#`WUxS-b65m)Aa6WXu&k9t)*pSOBwD%s?8Do zU6fKjnamKdGMcAn7qPju$`~whbISrhO8n;cbu+uWN4{cZ!z+_Ee`MvfZ(3UEfiA&bF{Bn*DfXu7;qn;%aCL8>T zGxh`D$nWgnzkvp?31R{Jf zjoqrnthk{oltJLffSaE>QuF-b^Rv_;71-WR@+Ydid*T^!{9vF9_Ai;Ku~dhniSl+M zT$_Y~^mwoyrRu4NGx$xpqduP3Jjk8KeEU!OV>-1z*jrl5^B#& z)2r*AOCguteT*TxfS1g8^G-gKOMuNsaArNU48}|)>SEqE9x;qiAkD3nog7{I$W29r7<}l4RI;0ZKZ>3gDRuP$+ePMG=(~c5 zFJPfAFJ=-gxuPD6xr(Xsx>}?p2Rr@1$X$?=e$Dl zhy$&-y*}5?VbG6R@b!CyX;Y>XK5D9pAG=xgvCo_A2aj4wIVdHqgZhoNl-R*zoY4ry zPijh``bXLz&EG*b0@jM+M#WJgQGz4BTA zK#%c*BsjuXwdFu{du8~z_tY!kR7dJ7 zl^Lb%xn%YVV2WLt-Ml>H zJRNo8ABAit(5+I>kr`Ga+r}I}Nu6?ElJ+hby#mCZEQ_rxIMo6Z0)t_F9vz!d;@n-~ zHTbfbKj(o=%NQ-+pS0^Og()u8wS9-*tyrLXIJhqqXIS?vq{GE-!4;S?;egu8vXp$Q zy@K1|bGwwccmCHG!9UW(XLh}3*Ac8}sXIovSW-$f&zkw=cZ3~iCuD8-ay$0lA#rcL zMI;Ps|M}8v2~%oP0(O};y=4g$AG=mX4JF~rcm?p1t9-I&l@kqRR^_@h1cS1CU&I1? zrK^k(t7(RvUv(N2Unq+jf17};BHcfCszvK3R~UY198i#t71TLpu0&$64QeWPc9DV6 z3zvSMX%}Fc=CyBSmB84EJ~r(bHA~5N#1OSwk_C_I{-@}sp)?-)g1Y18F)8O0lVO9Y ztYf-B`Vdh9Eg#2T{c6Sfl3UUEa>#O?%y?qj#-nJKp3aprwl4}?CTwoZu!kx(A%d5; zyA1aB4&Y2!^+m~#8Rup5X0B3HaDl6_hN;x4)2Z0EJJy37RBWBqZ>x``4qzYNCm`AK zD;Dd1J7HK!5TO4&PRi-OxTE}IVQI?W>K9|?TW%R!6>IgZ2(3PR{j8tptnc?f1z*2R zeMHXmik%v&_x%;oc+5{07#mC7hgzL1bZpU<6mlU&+tuN}Jiq!M)rVLX+C(K^KAsPR zUrQKs!VonKdf4j2xx{CZJzaqUQWJ2g>vd=#*KTlZ;E5Uv$2QBA8SIAnNWg>MXJ4)a zXcHWuOj!M6r%jaaMz~J1wZ^db!EBox+*{&+u|aIoP|_?+oYLOb0c82l0ic^2BWlvE zmBCxtBbS{Fm0dm!3`HjA(RL=o%I&Vt-6i-lD?))a08B+47+|6M>c z&pnRow3M#;(ZsktA5tTvZ0ZVYcdz1gV|dSe8m7WCBe$ex>Eq6akSYJ#u3dO5jWL8a?lXB_aVLJ~Jqt}xBy4*e9= za3Bj=*4jU&*pPyI)zmfW2F|=av1?aTGhTN%B(uQBBiPU2JVLHQx+5I@Lgby=HltE9 zwUhtN5hn_~Iu16i5w!mmDUqN+Aovlxn|Jb{PRtNTHyt6uN;vJ-WTfS=8hdDZ5M3>r zm!q-+7ddhFS+LID$mCeHad`6JsOm2Fj4wtiI%G~{ki@sbACJ?m6A>sToTA3pMdJZ$ zWlD6%x#?xS6@hZ1Dyobf#AhH^U8M#~dDmPv;4Y9!2Kqh=qVXnFY}Rv5g$|NfJ4&*# zb+buBr6n#z;9_@jxI@Ffz#6%#vH3n9_dmo%$@MyRN!WV2vJnXh<9TM`M-fLl0{R3# zMw*?!wG1S)F`=Bqqfa~o;YXAF-vVgL6Hz67n6ypg(Gu%2{1-Sasr-MCyui&MH6prQ zcZo0OcpHe-`dfFjDo3JfS^48hBMJ}c_l{j$AB~QE`dAr7oRoXjI@wn4}FpM*6GBU=WjN`&QYp=Cy@$5?qD z+8#3v?47Uz{{}%ze#)N1!;@(-SUTkP+xz*3fiuhPjlK3QPU0!#Q}xtg@g^JgR{(7V z(M#ndsaS6vu5s-{QPn&pCt+KbxgYZ>XO4B1Xn4WT6Z8p)gKb%Nc=C^&T*Ue5+C@EmN#9Y zlETm3reh`6+nbh^6(A(S3&4MwQB%8HVo=QWs8_(XG~(_Rfd2kvGb}@A!)0EZt#U}e zXn6XH+V-O>S*M2IT?j)+S_Z{GCi{bU;SXwSefjzD2Yv||$;1NHvPJ1dh(bFxH#YD` z>>AtF?D}mG`t~IzNMutBar6?k^0LK1)w_#ICD2kr_2Z>@1;>K(?_{Xfr7F;9%Sc$d zYW5bsak1fwtR^mOe9&XB%4k9?U*iMG{ZHa2=HFSX>9FNmpCzZW*<2HE=fv%f&t#PqP& zz~rFQZSQg7mWD#I$iDL?1Pu1^_&xSOto2Z>qaEN!Pq! zB>F^ZheJ29NTIG+Nso+(49QZb!ovh)bG*_xYpwu+!=PWrANRTzJkA11pzK-1DVCIN z<-{&?)`5h+muOBlZ^;Hm)%d~s$MZuVA+CTy{t62L442@84L=C0L|(B86SRkRz)U2j z)CYg!7i{p4N;0C%ZV;n^1G19(3Rpt7Q*C$JQiXMOb>6uic7{zHX#V(A$2loxdZN(P z_727ZxFL!3xv)gn9)dU-924;x+@WT5iYK|k+#2p!U2Fr5)#oNlK1DW1+8WxW3j9{6 zJqFH{cJmlt33vGYFxWuvdC1sEb|L4S&NuXOHESbJPYbEiOziqkjwOb1!v!|4Sptf= z@x+#dlq;c57P${FhHW+wkzUV#`jXiJlpivpm`bo9>-_q6wffB6jA^p-Ywn*U*Iu9n zGn1Kvz!6pvg`pTL{FnXV9yI2%`&(O*{IsRWkvcu9no#0e!h#bR#BwoA7klxRESV2w za4iw&XL~Y$gK^xCfBUH+LIvb20Tj0y;`<}r;Be}&PxBW6@>m|~)%Us-{d`eO7OMJd zUWUq*gq%~EFH9acd(9vMIa5vs<=(NEYD0qtazpQW#BYpGz%gSJPBOvoC~0s>SLh>( z%gljOo9n{qeaH7_JuuOwM;_=nh3Yt~sIH*e(yVRoLlzsT;NF`>P(fgBC9nr|le#pS zWU^LfZpQFL@fJYkQ~)WJh9WgG6?hVo#)Xn$sd zr7^k4hc{=8yG>n%#U!9AA9th7SgkiE=uxpn9;0H6C%CnZeQqoX!964SrfZD5U9PG%)8A(bBlQAH9$3c>BYiCF1 z^7`4yC*c)CVPYc?Z{+bT#!jOBfI_88cs&(=H79FzF8;*HW$heni62tyojBQN?WZ;Y zRkPV!JHqf4f(!j4+K|AC4aY3c5|g$7tP+-C_5!x{9}C_`Ya@Sp8@gSvK4sqfLh{V& zmsj3*~cf;?BQ9_RqY4$dJ0fJWr$NoXnU&osp zPfxSx6>XyolNlRXdW!DqaQBh?0tMo*N%mMrt?W#H^73BR{|>pVQx?4^PIGdFha~x5uAilv4@<1@ zjKms`V{4~+6N%}oo_QIHLJ~j!H6px#=6qDB?EE5i_Y1kjzzOPR68kK>3fb9s44)fk zc-%aBn;vN=j6)tCro>|Ro{T2Oi7O*1(KPHEvg0<{`hz5p`N-^#bA?3>U1x=rtsv4J{7C>w25=qU7lZ zF5MF}n(4dzivwPPE!N)N#6$AXJ)AGple%4J*Euq)n>Cx!I`Qa~PrX~kH}X?DzF_g7 ztx#fLwk*8@MndG@h-pYNe(bC*nCx66!U~iyRdXtdm8a5>{isPE2{NFwYw>kKx^X0G zFlNaAw*EanULSvS5W4{FE*E{SGV28bZUKn9dVrc^CZgBqI-yK^l)>Kt6p;efEY5>~vF9ERDCHAjwk{{mIFJ3n!BW00Xt&Y1-3F8Ib%BQt^a+(XN) zfZi{*}N0g8s8PJ8J7}t>PJH4d@p9oOTpm3kq#Tpta zzZJ6N+vqle>M<(tK1QNpg_8fjo_t0bz8TNOH9o{-rKEVBt4nrf{D$zAv6TRuv3?l; z#pF(+s{*Emq*SUN^gz1#tiRglx#cN?Ub|{iWk)`2=x_|UNPplZTk}+_DdyS|FWAQK z4X=KGI~Esk{cY)Kk*sg4L6jt685`poOb#DBji5J!XRmWc{-BZ<{!?Q@zNpixy2#4F z3P{UAF#JwT#H^iTY>s1#l=ri=}8ANlEaUSk$)ZK8I_lpxpIzxxx0jH@WCyKNG|Z$>z3HmnfH7 zmM7HAr0eJoePdIuo+vk*tzRi~L&b5euW%N3+aR}C_i>?e%JxAc_~GKgVUzrpz1+E; ze;0?xr!x=@1(MhH-os~}ynUC$Ky0~n=#(&nbHUq$&=D0!m3_!FgJ}uu)SnbtxOpmvfKVU0P1Tl^JQT0Ot(f z1R$oMdS$1%MDA?{o=yj_INC3Lf#-hjlv)b9TFxLVW_DfoO$*7Bpi=*oolNyLRC!JCA=*I32zC#j z;|Ajz%jS^38}(a4XzU)UxYZdTYcAZLCeB=O$?+gf0l`Y)1tIbFI<49LvE$`^jhPY3 z<&izt&KhXDJVk9~Zb}PXnZ$GU=pu6_r0gqo{S*49F~ph)z`Ch>t&BL&?5QGNv3Zby zDVg0u;>WbqlZShEHrEXtw(PApni4eevGFf^&*o+k1UA%_?rWAYsLb5f)D>ZiwD$?M zIaC9UJuNqPU$Q_dbOMaCVaDG}s0ZU)if-8 zC!)Sx{?##4A`>Q_hvA#yH8%L1gI0-u$;Sg#XU zH@O*7IPWaKH&cqd=$V$r+SLp=E_OWrhnOVi1sa;2z{V`sdmd=LSQXT_o$C2{$La zvDa~S0?79tT9GNqo*X!a!8K>%J57I9y=v2Z-iaj4zN||}k9G2byP)UG1}1E|r-kno zASy5$)ypwopomxO?LguE&7LW;C~D~7=yKn{#n5)f=jyEB<}Ih=-^sHJBD)^HjMGc` zU>K;SANl$WZ4CUazCO(ardYX(KKd<@PA{vK6ZmEu#YQXQ~bA8aJtEnC&CUuG;xfxwh zL8+fKcbc|N7fUyUwka3A%rqF-rs);<1a#S;a_(TG8(flZEcglPxT_-)Oxg)*yT0lA z2;2~Mk!-_p#sm+|YC?I%)aSb_YCpzCLeycdt9ZmhpP7l<}DR=>555HT4}3!wYypPF35tPaY#i z4vKphjA;DrRrtQQR~$K|E?EazS0R;Y9MZU=)^5RXGnO{#Qx+G$X(&iTIlMD~+2m4T zaQ`FdC_xIddWJ4o&_GNd^U#xIZ`XBX#ge7Q@BP=!$w02{rj{QRZx;gRW7zqbH0E#` zB#CM>fnx0qMS4Dn>lQGNnFJ~%r1p6na>{#3unrHb2)Fu>;s(;}D9r%3Owsz6*ox)p zd3V%e8Gwmbo7g~8NgSEv>;-vU+#JxE&5rN3(AYbdxK5$EQhH;WV_sS&0+}?Jd6O}! z3wfBg=V(>dn?TX$z^`3o&40_>!{+D5NjZMSu8ZPAjdiTis^9Biz6sEO8;Sc0U}5L@3k{Ptas_#h2iVrH%Zjk2z(C(NPdp zH)iyWM3X|OL2V6gpQQV|FTQ`E-?#Uix}QR?X!j|$v?7m2@~=;0rzS~T`LsG^!K#^)trjUJr@_`5-P0Vj;(LjMM()ZyeTw%W0|8h6HMT3wTvs>wL=@ z9Ib<}JR^tObTilWJ3Ql4vGd|IlT^o1dh?0;Ve^*Bnz{auYItoES%shljpf=Pqok~) zWTFkbW6oL4sqO+x<(;hJ@^G=}O#4Olnlcu#>LpTK5hdddLr$W=BmSx`DCk*usV$pp zlLZE4>`bb96Lh8UplOfUP_+mid&ud&$lK4Ag~U?i!JcK|_WRyN?$B@+MpF~(ByEGm zb)S|ClAo59x4k2N^Ck>3XN7;;nxTo44#;-_eVCcekrphrV)QBF0yk7Tf4c{+tc61I zEVt`JRr`AvPn)5teg#LpegSz>bp&HMml;Itb?fc0Bnps`D-3XU`*zc1Q_-xVr6u)@ zPENy95HDWl6S*3nBW5@IpoD#umU%$!~2H=ukB zn?k!7(v2fU{x;_lgZnrKq7oAJ{fXrzIQ37z90OIKRKpaB36Kj#ba@&_%DV!uIZ)6j z{wN0M%fNlG({(y|8|Ug%wV1l0RmOe{Np!k6CR?OVz5ZkH7Rfqd&G6l|UjW)t4S(Y9 zd0^qjCbe^(pA&I-oqBu`&(IhO?iXE09J)@Uh~P3(L-RU?#wOS;qUXbUus+O`r}d7q zBey~&8K&5w<5*t$J*P2ikVwt8!2A)VmqVxY6>tNCMr&^O-Z~J_`NgrXG5IFg-goJ7 zd}?CQWs)b;=Tt)o)Lh3~6l56o_8Dg+C@VQ`-8z{t3JJr!TP>C6H}C_oTe@1`WiU8@ z=PeDD-BD+!vQzhZ>iJkMt_zK5$g{`S+=W=zTbPYCDq8TBEQ%P|f2lhifIC@CWnLH} z1#T^@KTms5bXDdwy7nX90Vu;P5snoSj+Cl~m;u()2)@lU4Qb?1KPp#(r{41Wfv5)lU)ek^C7G~upsc0Xe9TRzi53U>~p zth3wii{`+Q2VlFtaqKGNt9tV<6 zQxnaS@<~0BSTg^IL~`dg)4R}HF3iq{ZCEqkumi+3%S%MJSP9i3kBDAxil6WT{qMY< z7$k@Y{PVUx2E92uwq?4gJ`Y{Wz+?0d?*^E*A4&u+g!oKG{S}ITVP<< zj)y~+dUNG@d%Y7XA_H%vSreg|o{=La`~B$H5x8fXS?zI`&^tT{xr;N|DICw(O&wJw zc)%3PuOnCd*?+$<2vZ60k~@C@J4BmYSW6p@iPE0NUxS>T3!mbLXhXxG_RD_du=sJryJ-pml1mjzoG4EH5{IJ zWFPEsXskdwY54mNk-89w0O&^b&J6lzqRTwrncIC60&h~}ZXiGrYRCnv@t8W&8rI7# z>MyYUj@&vDw0!(JkK?#C00B)*`olm_jZT}6=I#Vwn0>Sj@bfQ4oGWqXk1Bfw=V#T8ucgknlvx~CF66f-JYi>- zL|CYu*1^30;oW>r{G$Fj8Z}}ue~B!^&W&=!knI^wXdy^*>s&I@Vn)q!C(61#kzaZ` zZ_!RqM3G^CUMMgb0 zS!9J9RQ}yKD>Omj$C8({!S_s^V@%>Wy-^hsSR|z3Mu%FkYOt41_$q1k_kP>m1NzoF zmrX7UDPyLnfvH%eeJo#>zmo=v$_34*R@3y(S{2LlLI0Q)n>~k1w$@Ds+`Y3nu9ar; z$^*1P_@{ZZJR6e$2>>@$Mfx!_S1uHiUHjWeb?#EK=qz8>XK16MlfPkaTPn#9d{YVc z*l2-2LN+Vv&*;857hq%0vts$6&;U71I|YNk6EC-R!M7q954sC zP2grm{K{=i=8M=dT+IKxh?Rhl2%-@6;* zSWlad*TUrOdpA4857SUCW%A%&_&ibOge0eu-ZDEW%V0X$d!psKCke;*>a4t3J=s)} zBb#=q4kq+TDxc*S=T#~doXl$_5M^{Kq{lGwu8rHXlsCU5!aYZbu3l~n=;6bLJ7(nM z-@m{()=UTf+t2*2N6A)rP_p!L#x}(^%M5iB&eEKwkRur@wzV=BZthUpjHvmqbOmZL z8PMf(I@bmR^%pcU`xu{GF8uj87cNhn4KGV9X}{1Vr3O)hF$+9UoRFu?kVy zbqnW0A);@Im4Wa4VZR~p$3M4VyWaMVc>{b(bQK8M7{^6~SjG&<1Y%XM;Mi_pSazP4 zW|F}Fr(e=BjvI+)KxFfqVs@CJO_o&lP_7jEi@hdI=>afW5KuB-O2ej6M9kr z37&!9&g$8jteWJI9NVKf#B4O_lFrAkVgE$xceAy6d~XL&{uAZ3o;z|I%lKYe6g6}a z=YtYR$1sYm=EV`>Kkl5AMJQ>lGj{Vbw`Mps`Gsf-FSL>uVhPg_14+7I51Qd9MkFRL ziur%eU!ToqyJ0snqRb;M13n(nR>!$>RO{Wh2~+$1yH*>CP^BRyb}%{!#UC^0)>9U< zE@5n~l#N9BWqEdq${*qkxxwA$7Vi2=C_H&z_~uw zg>mE0v_X-~LId?ipTa+y^_Iiuy&D=_&L|bg#9<#4(fPB#AB>ItF93ExiND~|v~Df% z@F$=9i|+^!_4fU$;F@VM$8iuGYg*4Gd@xDbZUa^BAkF^(aozhSu?42j03($Ay~{W> z9wz5W&)I^KLIg?m>*`}wzj3#SoWD-e5-Cze1FPrZg|yQdRaPr@e)Rf*wQXI+CBEms zUPjk;ay`AEYiJ=(lgFc=nzcyHCbmD7SBl_Lec{?WfZJzi_h#^=w*9!=#LLa9;?^}O zBKY;CkCm0a%v+&K$_rO%_P>92w)JhkZPr_AZZ>Nm5MJHGGSXPuGBn!E?^-d^SN;=R z6?6jf%Y^%n)qBm-*K^%3HpNY|T(&kA;OJYXic4cG@-!^pn<|~}8~Q`8 zI*-Y`Zr1km%NE;gyN%VnD|hno#S+hOiso75Dk3aol2+n)Q!3WDvdcDOX&_d~aeW8R zN!)vCuHR}WlwZ?xMiPSHftp!GNpAZGPZ+dTc0_Y5Z6>N1$) zbLbmWJdw9nB>+m{YW38p#A>B7sTqIZ4uw}L!%3;{etv^4NKZlQhbap)(Ll^G1%Rgl zY2Yz^Lw{|x9=%XBQsB-$uI_DZ+t4b-)rwisSkCO0)2WT$LiA*f`n63UW2kt?prpITk*cja1CiN})u$C^TkPrkbiwTB=tIR$nu=mX8SU-D(^A znL624Um{YQ^z92+^Y8D7OCrCLHfEuru1Ct4ZW^fAZ&!GFuwzFXvHf}z#+5kPR?dC6 zjWR-5ugx%_;3L)U{A+@i1XSjow^{j_ZW3}H~$Kg7lV0O7^9`-+7E?4h%|+%4?l zSqyg<5yoYbLCYI34D1p%pitV5M=IdmTSj|XBiG_NE<)RF?Es5P1(30MG$O>tJdJvC<>J@_|JD`3{l(XFYHS$e87VpdGP z1$Cc-*WZc6r(;F!l~uReZP%XW3!UL6kVZh4UrnRaB?6HcEUH$3smKjknnE3oBWS&X zJ*+jvi4;*JVWg3kIKg75qf-z$nW!piPqzrAr4Dt5S$5lqr-nC;s*bf^Dq;9knX}cV zN8ER7NMM}2npOpsrmC{(NK~|pa^Mtk@$ka$k~S+IHo%hI+THDNU0X=6Z!@b$ENmV( z1xZ~jGGN0xt`x7x)5aJxakhqd#BWDdT_&K=Y83FH`!MS0iQXLUcLG!@Lsyt*T)aNq z1>elY;rdMTzHXWx)!wJ-G-a%&jFdV_GO3SFp~F_HV)|{%%zXa<+ZRUHxHkL3vXfY5T$>+8?_`x!g^-&aq1^h2(2JyEMf?g>|6x zM<%b-XI@<^kr?MJ)qO-Z=eKWB(*FQj%ctiHkH3FOhHjUASK-*;>Dj(xLri)>hK zCAhe^vACF(SXq`e2%@HT3mWJ6wQv|24h=5XVBdFVHu3g|Rzf9Sw%lwMhk~qwDFHn{X*fjSL14puwF!r{^x(+ zq;-aU<8qO}RtY8b%Mb@L(ON_0hNuCyM0xIJki4@=JCV%txpqGmB;agXQa3D>QBgsN ztt51ThI5%5FiF86L28VSD$Ywtyms%^p6cywlHa#>50kcrDP);uHvLKU%tJ9IMtJy) zxiBPXug_(=&H9qqT}VN%Q{~@HTQWfk7e8hx*tJqhp6lCHOfqi!bUby!*H-ZyhvGu1 zibQOFKKAa2SVX(E%S##NNo4D>{-G|vELucYfQo%b?xo`_7cNL7%LNajMSfBXM0~ zNb}E^oGu075AMmmjh}k9+!2p=J+|8(U$-r_mZ}3^^X5q;&w<6!*1s<_pQ=SWJ8Q;$ zzW`@;-R+i}ZT08M34sY$GbYl(z|M-Tk56JEbm^ zOzH3U_V9z|tLP%OmRMcPHvyfIY{urcABAq%P55w-ncPV2-k8OJH8|9-1g($#%fUYMfD~Rc`yQn74o+g3w9?b9@ zhQtpsuF!xq|5F$IDhSXTaIFr_7TyW!&ED zvKr5Dxk}sKDY1$-h;y z;OLSUk+%emu4p2>hI_llF-0Po`Qi|mW5i3I=($YkXl+OTZvwz zDbNCu!nCC|&ONnQhTPLHx+RM^QPEKeItctn#c z$Q|WYxOZIJ^&Q%^9L0`pwRN?Ms)!@pbPe6Tt>a7ju)$bOy!0{7#15mXX+{fHb!H5! zn^Ox-kLBPW?xsCtB<2@~vklDQnehE4Vf7Y5y`8($H885b{{WdHAmd!MFvP(1FD-O1 zJ++}p^~fsfju4&|WGi2VYCMi(JZ`HqG-!5~KQY)cGP5%qJKKZ?gd@PyFg!eZd_Lc2 zE#Z!!sH@y8UAX$subms@g$J{37> z*Pjx$M9y|Rtx_3u?SHLz8=d?cS-oqk`^j`g6xQ@As=D33N+@GtrCTnhtgvp@NCv3h zh}o_0C6NS-QI)t;A?%^4!m;%Zbq4s7*z}u`iYVsjG^;2y5i*vj6{k6jd-LO#4fQE0 zjr(~Nu}gq{>)u;N(KB1jvrjrGW=IuD8i8P^TOg?v`$inHrVra?cX0ZJ+k2EBExGpn z<>VoQtXEN3%^tKTLr~mEE97ZWiuZ+KihT4ZT9t;XFtk7;s|4^+() z^u;_2(@lOjk3uIQ&63*Mdq{1Klvh_1O-P?jCufp~v8;%eITt)gEs&+U>Kd`*z+%CJW9_cb?VFC>+U_ql$SyDEwM&Vv3pryX z>#r~+ktl(J==9~xx~qV*avf?Dle9ZU<0&S~XaO8zR)K-@)dNIH_tzBMg~kE1{W$GO zd7y%8R&NOAXH)Q(sZWJ4RZJK+iT1lxj5)cvW2ZHQMtrm9MgpGGjCa9}IoH+N2Gc#> zkiBFRvhDWL8;Li~#xbRWLS(mwm0cy1L@6n13h}|aRyVh}9(Hf(UjG2xa=~_6TS=j? zGTgo6hq$?r$kI(C04maHr=(YuY4P)_zU3WTuWxY0TdOJ351sq^!u$8@i#v;JCYhx& zG-f#M?$=U=3dSy$q|!NCn!HIQl39&>sj>I63kNz&1I22s z{{Rud=r*<=w>wd)%-4u$C}K$E9^O`Xf8xOCG0x&*lG;Y;yw3z8ptDDp*o^1yz}Z1` zX|eSd5e7?Z@t~S=$1td_L57S{PIVc0jUVbdixQ}nFZqUGDom_CAuE6hR#7tAR-Q$g zOX(+(PLz3?Sr}Ky$}3C`OY5lKp9Gq8Hn(>lOePC#l1FX43zuh|OUDU;U|C~ME2>s2 zrKm`&3WHo{R-6`xwUd9MG`>IXrN@lZiu3;fDW2Ybd8OrqEbPrGN3WnIN%+)Y@jaM5 zQia738rsoKAE@;?@{0ce#fN>v9dMnJ-pJ(`kx+2WLHN`*EoCLJ!yD*C^j?ixnN-x* z;1<7TDdd2@$s#yj4YKLvwhOPN4XpZDkt0YXeAu#)(~Xxg_<^oeW>+ALxL{(E%M(KK zqCAJ_FmBy{e-)X<+3%h z$dOtyN;DM;=1D(Ex}n1b(s_Jo;X%G8g;yeIA-Z2k*PBdm(V*OJHx5{pqY9?wN(bvT zK$1DeK%9j#W2XeMKHa-{i&+%82y8d;-)whx3S>bdt3?}1uCa|r@W#@jGtk#R4{^n1 zq*}hRsbH?aBDLA9=Sl^U?=?xNnloSBNb8@I$k=MEqN`|ncF_pFcajd_B!UvhbV#Wq zN3W7ZKtfzbt`4DJMJO7nOe+mlc~`e@P4`@Z zMW|e6qWPz{r{nP*-R&&B(_fkZeo$$K=3149=`#i+ndLKR<0S%oPVC z@jiW1&Is^0)nx)r53xjBg^k9~EXx;GS-O=f2!>byr~{d_^q(T9!;N&~7g1NuSka3A z04zk8&8xMxJ*TzLZO^?&yFq&GcXK3m6F?DVjUi~H63MCR6qTS>UNqrWZ5WV z&)VIm+uQY)@g3B0C7g{cq%G8qq2qJ|<>_$k%EW0Ne~T<5?>ji|8C-Crw%Ye^)UM_3 zHnZ4MZ2thK)vg-K?)z1vT|@~ntnoKe>0MQnQ6i5p#j@oZb7FQAjn=xfUnjA=6QKRE zu0*AyySuiWTWS%+?ysR{jfiQKN2RJpph=)786Qad*YoUa-W$uN{lB7v+%KZ&CrVp% z5yhe#hi9D=q`CyHBRlKULfV*`@i-=$WUZtX1>{!8sVGKX6naw^1=J65x^1z@!R`@% zl|xBSA4&&mU|uN=-U(Y)!PRHM%SI ze?$AK=4U$xbL`h}1l01`Tuaq*1!&Pmr3f#VG_=DwmB>T=B>JncvE3uOv5pmNq}q0Sy~YJblG-xrtO-)w05qqG#8nFKK@D}fSl?M%?oeBWytSHk zlG0~%62MV(AeJ~>ipY)UM)nhmwVNx7jTGUcUI;pG7(*t;4Pw($!ubUxFcihs#V~Q&nuN>AV zm0Y5XtE?=CI#g5>?mP`Vw|YVvu;WfsaofnE{afwZt=DF^+%BY!UDCu!rUVLEp)_fl zHxaCbQh<=zaP*&0WX)=&$kT&`n^jtN^Y8d~_k8&wXeDS;d80)+3Y7r+fx{J~XA$+f zx{U&xs9rZUWOhPp?rv|h5u$QKu*ZKG1uUVRB{b?ZDpWB1CDbSf-f?`iEA77#*(SL$ z2)}ByLI#$SC}vVf%TsjZRQmw_I0l9(kKk_BYj~84jkzuh3W6oMh>!kh`|&MZNslB} z0v5M(iA0cEBr&~c7pTAkl{GUEPBQ1BY7Un}men*1S!O+=(MdUh#rs?cWN4#^#$k>k zYFT7=5=Tur`?46ol^r9N&uH=fj;2g^u6Ex15}0kRW;_zr>+kL;n~O}B4IGOCviL|@ z582dOXN|b)rpGGxYj_(MNhL&f*6}4Y)uihZa^?oK7-Gg{=N;oL>R7Cj#3@iniCh!$ zT>k)u7^n@hrXpMU& zdqtk(dvk3)y~MED!!+|v11_ZzykS|DhMhE$j0-RIaicD@Gbt}!wQnZdw#bUJ2rZ;S z(oW2}`kh?CaU~SU{A=nzXjw}xk51ksJLs9Nc59%n%Vx4oAwUagouQLUwJXWd{9b2) zt{Z&Nkh)`;32c`45bc*%@Y>Bjk@{&AGF&lDRsA$WGv;Z;du!q|JSprlxfz0F~H1z zqBT1+dk#4yg55#O)$`WGara{IRvCFMDEazmjl8;QvJi4)H3u7{%v?U68(U80x4plW zZSA3gXOZ85423B|GxgSe=lsA zVWUdeFE&)Q+w88a?WMWAL`ImxHA$#s?;uVzJf!?F!>!#acWpMbmYWTew-QP2HpLLW zriN#|ecXj%8w_fh?vt90Pw@W;-F_j(^anlNtRudxz z)XXa=HClRjD%5ideIsjR>f4P9rs4F2S=`%2d)*dD*Rymj7Vg`nBRMq@#HQqRovGFU#_zlM~^Wst({ zUO_9t%Q&>l1WQ6XiCpPHs8r$nVv;JZY*s#?$Q!$FsB1*E8CHUpu?;!?Ur)ms=>Adg zS4lc+;&BlMnvA-5V#_-b2sN6Ok2a=c;)u_qc2LQ)*hI<^qPB)27PPEr7suU#da!F< z0KZTk-8*XM=A6X@jFCwoQ2Kg~61q)m>ct?Q#Jg_g+`ET;G49q@mb-*IY_}6E0%etq z*AUWDt_p;XFtJmpSxHqsnqgMPi>rf|KU@3KWzwT=wdykjdwW?LK!%_X)}BPK4D|dl zQLK?;s3Py{7kWET?~QAuFlC0;&q?;Nk_iY4>?0A;y$edIg2#V%MeK~@xOX{749t*E zBvdSQQ>O`x4^N2m_Ts*f(&elk+BXjT+ywGj*l&B~z0D(coL^o?QJK>#^)$AF>i(Tt zxM6q1t`^=)jKSmmQ!M0x6~5r!oNJ2o<~!FMdjnffXzvo-#RPjf>rUsOvR~UUsI6DOjt--yHg;YiPOlM+&mLOU*4Eqnbl2DKI#ug% z+1G+`X?C?%lA<|js@0`CHoTdiTx}Q923tEATu^G)Du5__!isG=!K>QK(41r^o9-G;PgZH!(?1=*fwSkmE^lx7WHl8q=tdKq-v{a(M$`*2QyH#?hTzFE4fA`w54Qp(O#pXC zWWDV}^3Z?%@9oU5byAw0APnUrQW2Y4>hySiTx90nkLot`m}cAjzqwI^y!Y*y#46C- zTa^l$kSo_b*|;Ap4qT5PncC^U9b2huUfyN0%j%SDK38$KXte1Lv9OVKGJ~j8CRyU~ zbt#zopToL6JA1}Qc*lP&#ir2&a?2{pUO1FOy))`*q&$V2QRNkL<(lJ9DLFYRhQwQ} zyLUmi2%uM%Nu;$)cor_8RbHE1OZuiy(H#2JSC>r&FYU;?N2EWuY_eb6Y+J?Xp5EZw z?Nu&{gWL`v z4#zOvcf>{%j??9A+IW;L8KzfFEI%u*tz5oeo0|`5@_|wEmR+tP9^V3?8Vq5OFAXis zxp-u8>@g?A@SRoD$k`@&7+W$i;wWj1{c4O)w|S@WjXdkOainYkTW#4Yj^;ZnV$Vv5 z^g^1+@*1(@-c2C)JVRD{7Qk&SK1SZl<+kZwO}T8*V7GSDUIT_39JUe29W3%IQl_BbXO7`jau%?e0Fq--#4Z(Im~EU6 z8ZRirI6#MKB-sF$P?RTFb+5436D$7!tacsBcY8;xexBf(n~e<3=1~?u{TRoIe5R{z z1rZd7Q;fVYNEf*o{>&U`S`jfTZy^~SB#L-oQC2>llGs_xC8~O0c26%!rXWd`{_TbC ztgWpgQ1jnjL1hwh=OxSON|{xo39Hmvm^$bMszlp%d(ESMxVGF-t>vw%#3o>xBz9DsL~Z{hgzErztYadw3#Iv6f=m|Nb5~B;kbI?m6{`88vg(Tfp5hN z=KM+1?H^8icHLgn^VQRzVjZR{I}p4{u~H zE_y1=VzjpDEmV}yrgLu@Jh@QPlr^Cky4tB|i0Nm$EBmL{n@HpXYud@TDl2h)aImm% zWHGGD!-xFU{{U>Y{0pux5~scWYufR=k&lYdyoGWj^8J`yro%DC z9F2DDdtU9e9n#i4sy*Cp4IBLG$8ByceqBsq0>pXRP|B)Cbtf6G^7ifKY{z$Z8<4k- z^4hy{@$AyYrrhC<4Q(1l3C3LXOlQZn*GLY}gywq_7TVDSC`u%pTzSe6) zG&9K@linn zT(im}k1}-BYDP#r~G05LC+H2G52ydtzNhAZOH5i?J6HPbUPu$V? zTiIF?)4#{P{{ZVN*4i~B&utN$mOVtW9k4dgXihbC&fL1+hmY18GNx9vw)A};W4fV^I zktK}@iCKcVDI==H^ip)Or!_Ej0_4m7>)UR2xnoJpTZV^-+bT%%L|g?8tS?3Mvwjym zJg_vxkXbTl5VG77O{!xRwakFp&1?n2!EaM{3&T_$X=KPbIj1gFz+}L;AJXKV9QQwC zlCO04*Qi_NmIsYPfMqOAGCE4efFB>Wn9Sna9p#@Td$`8mH2fr8&5Z4LG*ex5bm#!0 zfE+S|n=4m`FCYB!xS7=n|r^R?d|pU<9)T*!n<{Kw3-)9Sl84OK+UxPczJuU z{0&*0FUU7d7jE7{t@j@5u-xO(ZzPSSSXjR*w^u<7jpFoWJhdf$(a_1otxhFXyNwsJ zcOR&+_VtSG`<}}Cd=OiUX!iuTcllEpW2>UEjcN*j8c#4v(-+Uw2NcmSblm&?=S9Zn zx7jQ)tMw#U?+J0+kB@$avrh_kkutcnoO8fLAR zA3OzY9h{X-!(-fTcLIwtI=5DR60IYQy9zBA@aI*_DtmLgT0pH}Yhj7@jsEv^+@+4y zEi4k+TZLy9z}`xU3~bU0jcXi@??OkWxYFtZYBekty+({C*K(7@iW~F_O+?_NfmT&M z+GBG3t+1n+f5YR<x!WpAhk3uepv@*V>eSF%MgaP=0)5GC5)g8~f#c1^mL))9I zJ-IH^+Z&*;xfj2wc`z3SNCEvsR*1wSayfPAsHXvw4K!`7JWST&tf#;OXAjhiITp^% zZmxFZCMhm8K~Tl2#oG9#!>ayd4dk_^6^Y+dR%eeyT<4xwo*N;#NoPH*(a&`dg(PWU z2Pk!r%8KTJ+cu{SH4TETT~@o9TsJ9bV*dbCn91F4;8dhB$5G;>8A1O5=HqS;pB$q$ zZ7x+r#UO1Qcz`jYEGAGWG>swB0djH&h5@D$XqrU;0;ZV?;gzbY1CwV6JWtz!tJIKB z*`qIi3%ZkS8|t1)(NR=a)5QK9@Vc(%(HgGi-qPoyd-Q;L<|XCq$QTHf2d z7jUJ;w1b*CQDj3;HDFZyaH}=Ab(GJPeyr{5vv;NBHjEQ*xr*Lvc$AtgAy}lXd`$5< z_~NUPGfKwbr8BhwcF1Lmkd+|p43e=BFqDs2Q&P12-dMMwDlBOIk7UEQ_kWv?c%_DG zNux=kaJmOw1IAnMv7UtR%ldGfxcM4-YpSsB+qT(m`Suc@`EP^>iYuQ2%F zs^l1fh3ziqzPXj{Zf3EAQE_mp?5MP&BP(>`LE&9oJUri8fo8*9^(dMxi)%OOecTpe z{?_u|6BI1=@mrsiplq0b(p|q$C{$HuGa7zs z)fSY5EO!X*qpF<}A<;BviZ_wPS+*7{LnKiZ!uGQKO+|0S zaGZSvJye&~es0+-8*8tc@Ywdx2I%v^)5X+%zS=aF>7+6|mrjFO498hI4yV=ln0aG$ z@Y$n{Tcq_Yy{`&()uGzqW_u32lE5U=rX-h1=z^eCQLCrliI+%md$Z= z_IrmlWLD8oUzK$@bXtlE3KQ?eW>w>>)T(n&MJNn+7rfDNe++Wm$r2%HktC8nUUgYl znuUq3mZybC#RpV@+k))2^WR?GL2zvvGdl}<$Y6rr0AD9`K($FECZd{;kgF*~!L{I* z4X4q);RU7SN(IcxanjtL;6!c$EK!zOovrmMfQqNvLCk8(rB$elCd*FYw)TeAYP+qy zzTImr&|7$~;({)uiX#fKiDO2Q$4U^i70G;t5!I!_NAzjC#1u~HySJHb+9!rZm#NhZ z6`zO;%0cDz*Bne4I*RS^JBtn(e8v;IE+CTHdF^d?8SP_|C}o)n%@WL_IeJd4p&8La z>eF66XB)6*>KoM=dXAT;X+wRzI)rvFs5>Ubw0RAQd zlrFPYsgc#!$i_SJ39oye?Ayfhr6CB>uA%-)YV?oITo+B3hB%(m9$XIB0zL9?VFreQM@xx6mvy!ECm%yC!jQmAx$}{img*hWscq)ysDYV&4-Hu zjk-IVQ)OY?#!E*a{G`&fXwCVIC6Tq7{{Z${v2B2=37fWeC?yP4ZWmAmBWf{I=crI0 zb#M;p2CBlH*V{WSp3z~vcI?*!Q&TU-u*`D!!93Y8 zgcI$hS^JA;jg1uUHhPI12VF5}s6L5~HB|~lbmfW;qGq(VmWEi>=xVhUrYnRL^&uE7 zY^0u8W9hxi^(SQ%F4ZSfpA%L*aV7$-0A0KF8@cubcxOKdzsbKXp3i&R?;JyKyttYd*9!%azfI;)->&lMV5~HeObT$uCo5gbTvrPFyY2Ro z?3*3Uh-PcQkhL&tB~jO`w%0~RZmH6_#?(KDJRapt6K=OH!tT|g+&hrBOwh#?GCCb6 zLX=b%tLYRPbnsg8;1xr57>~K;+L}P}-S;F{QKhu7OLZl)F-cN%lT?d!N_5#Yny)^u z!VU|FJmX?T_iJ~l~9X92^kSg_+nY=TZWD4+-EXvpR(>3`&o7p+1*cXG>Go1&lHS=iV^u~i=9eU z&_&-)ODSNvSG#VT2!yjuO?oJYDHLS%Y8<{8upq43E$VGiD@MVV_Dw;Qth!ku(nzQI zbyJ@V6GbL%{{Tt*muuNAw@9~kXclN~QWcICkT*n2sYyV<5*&J{F;#K7cTL0*d){H) zJLR_Hvq2)+#kX7ABFY4BsTxU2n&rvW)9NClEVDNyD!?iomhoxxaU?+1OGx!ap!G3_ zTWS8B@Qjp@)<_<*+P~BM&Xl)X=X=J4wz4|(c+@oqD2G5$JTuIbfOQ+l?Z(?>rKIZz zxr#oT#i>ALRxT2x627CP_!H$&O0{eT#M-9ODjA}Jg$|YyYt#zorPaA>L-=YvrvzJL zx}l*lM@;eykDom__XDRqq8$djHm%_N5 zxfR9`cene)c#0*qOV!-kDh`aXt2nJFtJ3MaQ-ZTRyJSEX^~XKw%Blj6VUXYf+sETvjCe$n3U6tB9qOYuuxAsZQ6ptn3DQTFPKZ zT^wI_(>dCU&5)C3neL2%I_0Pc4gEPl3LC-r#7u*(>wtKzy z-?ZJ_wY81Q+VyUNGnawPa{mC95lZV;1C(LZPz$@Vk_2(@1od9;Y-?(TRl1#Rr6km7 zK*BVCJc~9i>PoE1YTGvnEa3S&`!|N--lBO?`q5;^GF25*-BZJkIGs}B&gV&!Ldym1 zr0{g&xj?EplrT~?WHkdyV=BG;4J#EB(61Ydc;nOsP*4FKICxXT3Cga=JESo->VQ!e zfPQW`EPW#tZlxkL+pu+_)KkmH8)?usWy#m2u^#@{b~cH3G%p>L>#8}1O3UULd+^LX zGE7c>rQ1L{>^n1Cy}Z`R(vn4)hNsu9E2s5!b#m0-i^aUndiAl)bd!+w{=oyZk=R7e z#-O=r0QUzhF=!S@lcx~r!itIpK|latEl}%W0>w8@*uB}(I3t?paBHW#2&Q$UFvhW0 zDK!3@HytNe^5M0xmWKan9mgGzM`7LDF@QZ$}{&9gX9ZDU6XK&Y2Iu%Ns!AO zmDCqZfC`?Bf=UYIUR+u#B-kxJsP>K8>XK`V230KL?gB|ZOY+9zzZzoXB*Rv{&9H6+ zEvDmbD|jH4RnPYrhWoKhxg<D>n z7h7dRg<9b%i<;i@#gs=`l0aEPg2s#afZ@j#TOwH!HqFhf>k0&c9!{&o@vM-%hl@!Y zDvl;RacWG-AdoJ0+f7Hz8fDg=z& zs~$rDE=c~6HGS9)K`lsHg2*+3q#VH%BL=KfpzYAM7c{b+uA(tpMy1u(MQGrS9UoZD zmDG+#Wv2!>C6KFPfwkprAxS>mse+jWA^5&xNp!*LUSb!B9%h-0Om~aQCF#i)J==5c zZtJ>&<8_S7J)N>ZlL!PM(OHNuAsVD}sGc+&KwUvmv`lGt0d*4=3m%F+Z0ATkCTTAJAmZHq(#L%jDz(`&d1 z3Z%%!zvE9g+c2V# zjgg64s4A&Jo^`-vqhza0bH2M7=#-0h@#JEzAb$>gLvICF-jc%O6v$8}Qj>@Kw>0(O8%36aufkj@>XvJA2-}eGVcOr9O?j@0DMHfa$Dao!@wsv$B&sI08OG=)VOwIV1|()#D5(Kl{{@y!uBD74fD8;h^Q7#J$qS3rm)c)<%{r zs9aF+&lhYer7X>m85u(`8nl8y^QI3S(iL>sq4%F~4$6zS_Dg<^-Nbed*Os$-m?udf zc`2=zTSq7a)MRor!d@s!5bESsc9)|41ZV}fHy7?Gz+OG+HPIdwKl zxQ>sw7fUZePv84%yt{3hTdQMnVXBF9Z~C6#%g0~xt#I0lvVyw1#LrP&%|X?X2L#Y4 zUfsQ3SlpMWp4HqtTee)_Q&P5Ma35gBL16vxPr0myBr%C(GI6d%lbN8w)dOOyF5hz* zYPxdqqYvO{aL5-VR*jU>qb@k0T z#9#sX`gBB1XMOtPT9i}r?bdJSP_=6PidT7J&DfnS1b4Ysj8+NO^H^Ay^jI;%Y0)<$>h|s(}*Ua)8f0vq&Jd zl0=$4bWT%7GDaIsYLw4Ew*;<$wP2fNx~T)JRC0Z5!|cVuYe1}4wj0b%6p+SdPfg5x zt`|Ir86G}3r;N36`1?m}RtAzWT)-?x{5o(|s2rOKL)%s_%q-(_zm3sZhq;I?gJ_Tt zi)S%`9C56148n|;>=+h{L+fP7X+Ud&ss*Aw*4`rA_Saj5L317Kb4S6n#IVY_{#=^A z&@mAjJ#^c3s95hVmIitR*5WY4@Bp<2C7cl;70+^Z^{aHv$aym*Rb9AyX{YbSNg#ti zdu^)13wv9MT*h2G1dR{Gv3h66MkJIL+v1OKE^b&PdI2J|{pQZVYt*GS8@U06SHKG%!Qng|}KLQsnjL_hIm4e5t z$iZe^(&gf_FJpOgw%^>W7WWa`+!&>u%P-4ADG;QNts7;=qsq7&QN4;zG-R2!+nuX) z-1d#$%1ce!0|~i_6ZM+FxX(#!5S3YW-hH#Q;-$b{4zi-)W{{V4#Y~$RoF76WQc~S&xqL%ieB-cb{ zewj2z)KB5{VC#6&1zxkSdA1R6cNGXp;BNy*LqlI^G$RHtR0`fUeVsO|*%r>$2aXA( zS%fiyj3Z&Lx_k%SioYjVMbWNFzY{%XR7>DMp|p`kd~)Sc{cT0~<2|bDNM@R3s=UFR zy-A_V4R$KE10lUxnP-SISC(NWS~;q{9T_p?I=Y@?NvJC-q^8Z6=$bS3{^0W}CY|a) zid34?%GlQ9*PK)D%-_v_Xt$3 zyL8hm%KCt4i&2$Zz|dh%@sdpyqqa7OcadXzx!qjt77Mm*=uky!E}HU`MFouP&#PMk zN7=@>$;xh^>Hh$%-V0g1Yx{%A3z5e`*yAde&cH!)d2v)|o-1~wq;jxn6CiLL&|E`% zz*M?u(u@+BgX|Ui@D@_TPeZ+}WS70mw^}ix&2XfO`jsmrgZv0^cBG(Hqf_5hcO~3A z`tILHF4YaaiW;d8$@472oH?#a5PvQ@KaZr|QG=?yV=7lxYjHALh?}Q5V<}qcq;a&Y z2_r~t5w?(hRK@;J4uO6*KBbFf?>^<(tyCiZ{jvo`JAL-GxI~h=kr}$2v1toVVN;Un zsmGpoNuBpe+i>N#YW7dnSf;qyBAa2}Y|4Dv7};Jfp=~;|2_nhTV851!m2m$6xZcJq zSA*2a`?F)a?B3$M?fGt!@=FsCX`@{#;#iVl$V(L?sZKa*@D}w_hAsCPhYg*DJzDH8SSgJ-0s!wm8Dy0 zVmA@gh0^U+CpD!?dH7=aKrXUsxAyC_Sq>@TI z^68BUA#1}*i~6KzhHK8#`9rVV^M1j?hg*-8GNek@}Gwo8f3YE9!)SZ!BhyaN6!=wZ4osB)5^Fl|v~EQb^eyPLh2lxLXM#)3x_5+V&B|@(5ZnpeB|% z_)%4V^1xXPp>Km@-sMQqv_oq;i4mlbIy`!rYMm~p;$IqLHbtmZhUBd7*WV$y$+pR_ zUA(Ua;EV=i8j~E+#tk{xR4qvV0As6IEwf15?t4mjq`%+ZNH;jG#CsOqw?!yTmfbY8 zOKCiaw&wNND@!AKh|!#CYIe+p*A1sn?9Ml7cPrlL+;8d5PVS=GqQ)SCI%8mQ$=>}Afl+6vy^^t$fXxNUYT zUDn3$cap_7u>{auSs}GDh(RQ+$3CIy&xT@$3(bypWBNX!V{>IayLM&%>fZkVwxhI$ z>u|AszGBhaS_t7upQjOd`aLLvyA3oVt(ID~5sY-FQCmc=KB-4vFJIB;tF$!yX4YQG zetRwd0LBKdP{y~gvW6W4T?Vyfb)W}_9BiezS}btf(^QP^P%7R>4ZUC*+DH&tTwPq< z8*Zh`$DlMyDzQ{@>(VNryxR-q1-hFT(WG_ltHA0j`=p@h^s@_iIjiHA2K<`P8>Ec3 z^^L~!xXz3UOM7uDQ-D{Mm{ZxFG+LU2&Hx+GwYl6Pwz-tSZFXUTN!p?)S*BV`BdB7L zpv|(vDUX&&+}CcQR~z#=+&fji$-P}|)_3vlme9H)UBIOg{{T)oN|mdC$<+YSkHat& z@uhM90Cl`g1KfFKjZGZn%a37oA`_d5HLHLkL<#~Wd9_eflG)V%0J9nGxb>6n&)0Cxb46ybfUzesjWN)t*jYn>e5;D(^9#4q%-?ppmixJP z4aJqBj`s5A>NSx9IEranNQ13CLN`!<@Nw7tb4CxySG&mZFDp#T16?Et7DZkJHLC}ix*k1 zrx7z`dNH|<5lWsBs*E2gaxvBtMSPzhJQ`ryrwJg|zMNZNqTT;?o7xQeWJ! z2jS)`i%h16*L0q`nupISVAB@EQ~g9e0c~fsq+|us(_A!(qydqMBZ-HP{LEL|L4DRj zlfV0tq}!WoRs%}(ZIV?3&X<2PM)zXYG!Hfny6%qXnT(R|m#PT_tWvZQvG&CePCp;Z zf+!KpU8N~m7}cdBu50Gpab-d+-J5%^8El-5NP3ZZm9ICxhM1%hEZx7i+gR?GQb1q* zdL)e>fRkI70Qh=_k>!E12^wd1t4-P4$YH6!YrM$tKk45(efS4eRu<^_hi-@FZlbh? zHo3T!#BswKmv?i)RYqmKm~)5}lVO?rdwQ`(y13f!VU92z#MaYBRSuqZS5Q@XhGzjJ zb$gN_nt70$cwR+~L1v9h1qPY523&AbQE=^@zWZ(7cI((WH;>h^GV~;Vnyu@sF9hz2 z3Oe<4H92a1Txhn2q`;DFm*PaVpHLCT@XYyuWL`%jA^6n$u?W!u&uD@-m9C_XrGdhW zM{uH4V9I2#8p7IJO*xjlz<{n|(hM(i-leM9%<(O>arD+Tg~6gwN2Ii#jRQ{#jy}u- z4Hq8Yk!0E~3XsaS&It!B#;@$a)oK-(4{#t@%1A5*j#c#zF;Ent`z^xO7Lt3S-C(Vx(@-x% z{7zNEl|!?Toc+UfZ46LuakMjCMIF_rMr$&oiHnG27*1t64nW>dyK4Q58!#pn~p)9<=OVnl1L+x1$K}V zRdy}JGYXGjI&v7>)AbQp5!7_5?n?!~)OcO=+*pW^9Q04rZ8`8$8VdU>@y4I_)a+$_ z1M&9^Zyo2`8-!Y?+oYQI*(GZ!?jeDU61p;mM(Q)itAQ)vefTGutpiS)^>R{oF6Y@F zS+luWp?KpG+(=}y5wUeIANr`{_H$C7VWm|j8<;BF%YqHw5x0=cnzqq-YTYHYAci*J}h%`_t z=>w~7b!*YVwA50rb(!{=EYx~5Gf5n@(mM?$iOU)wrbXQ$g3E7;{$&k4-NQu~xOQ7^ zjju{q6pQr)jAr#|ZVDEh+L4b}Gb>PIM-iU2Oj349dPhV;W|7V@#M}zjl0VyJ|~SG0UqUv^tg83ZERX4UDQ0 zb4I+F3R77J?ZI(eikAiNv#YXpUvY1!sFzZMZ*0;ADDIKc-n1#n*SXU{?dI?UXZ12; z#_f8fWp_8SOeK4m)Tcmg-a1z>6R#MDJ95~a79BIax z9ATpP{TmqUNr4}#z`Yd8^J{B=bo+$U&+`+@CaW2eH8Pns5>)kL0Bd4Gj#J7294ZLe zUcc@Odw{RE*o$e)J53~#^^}Iwr4e*S%c6~&Gis>x>E@!scl&Mm+idS7xsH2#t9WIG zJsmcStb#(iMyipknZ1Jo*l{SmcAcWdv(CE1R?e()8l4n#W%zjgxHN^ZRm5USK*WPA zu|^2|CFNBF`!mlCsJ9YRwXyt8B5HbI%P2RGWMj%$={#^R7)tI!TaDD0 zi)V1-nhRL%c-Eo{svq!E3sf6ynil@pxp|;N>g%sg?N!+>h4XZF7v0Vd}@xj1T~BU1i$8&Ejx+ zi*`X>zqxMG-R#Z0&}~kCUhdT+YxYA$b+u`jM;|R$RSy$d(@q+)DrhaIe&D|CeY9>i za7DK7_q#Muf{|^TwUj0)2{Hp9x`rtrid9D-NNU;-t2Q{gbvbLY{X2XCzfW62x2#{& z?bhSB=gLp0OYiTt{yvl+3=1sKETE@om7lEsR<8&=GVcgK$e* zxvflhGtQRs+c+#^g^Gu08QwszpwX&)y;)+Y_VX_46JmRvcKeNz+i#XmlUUqM(w1f& zNln8wc+~|7sW~!be{;+DHsQp+-)|8!ZFiPEuW@fGbZu<6JAR=#vAIZY zqA?L%n@j4*cyoRn9xKL8du~=ruePJM#b!XpssXMZ*@_J>wACcoZDEXly}BN?3pR2{ z?3E`W$~X*XyB$PCn%%x?Y5+^kAp8{bX9Vv0UOeK+IaS;eZ z+yn)fuCU!S(_Hi6;Jnmumy4fpc&z!i;GoU+n|QAG?XK4CW#$BibgGRQ z9;FX%xQDnBI4t6t>dyAzY*O>(<`&Cz#A%PJ;w2IVVITp6Q-*ZMKkVMFS*7(gwuY(o z&v0F<>Cf%J0(U#BTM$>2D2$t`f#v?2O+C2lw5wP-82)RwvGW_bVn>!1*`-zniyw|6 zBNEI{3>1RVM{0VfWgX><6J@=G#TO$O$!`C#m2QD zH>C0RVyudjLhPNzZfq7QGiyWA9(rrZ_AXxxQViC2Sy1-&;TwSKF5h<%@CT^_r~a%b zfXi6<%FF{8QN_3nGl;HY?qGXENdc+On59tN$y+2BbM6tg+jkbaB9@foTuZ0 zY!Gby)3@sX081Rwy93~=KJqbYz`em+*5mVx6rfWHATaU3wiUJo$|?aKp~nzfJvG-- z1-Eq+jf*4rja1Y=G#HW-*p;yD5N}(GZO?P^FL1ILr)26VXya))lrkP|8hz&q%aPSc z+*s9zx2>JFOUZWUl-vXg@lB*BQa2v00iRBF9DFc1^77O?xK%=ZpM4BdErg8Sd7t!# znvYF*{4t-ZP^5QHc`prQE@L6-rLr~g$2PZ?Z?XrxGwpH(+g#nF8oh-YLW&eFwQS65 z%lM5mH1=$wWcuA@pmc@Stp18q{Fn$~wL zt9`JWCv@B`RpQjL31ksTsy|TaOQJ%5yPt+IX2jc+ZKh4Zn9>+r+x=3kc0o;v`B*Cs1{x$0r<@r%yWK>z5-?$!cR;7KH4+ z=HB$i)&w4(Rj)TKJ9#%D-9hQ7mS;^42yTG5po29!dz#6bIrNW}I zDA3GJS!w6sDW422WbQOu3bN3X-5u2(>(P|m`ak5V@a%^_OJDW|qSev-xwAK>RSPFc+S3fSS$R!dOz^bch z^B8sJ)=Rj{Dz$IaFaG2=RhM+xHYKPwmh#_AUrT($^kO60pD!hUs~WOBL}9LyVn(@+ zI7y^Salrz{Mj|T0nB{{L18z>7Mp`3lC~zU;OhTmSjJ8;!4kSjJ zMp&sN4##cc{4pdJ!~L(KJJ*dyl#wXs;xH^an{><8+L{oiPOJt!Ds++^bki?>Eimkl zn<12tu1`0!5x4*eZdUB$^B| z%tcnASZ&_Nb+odUMTT3MV~s^du6Ctqia{}s>o)N3*H+NVWQN{X5tlV#89LA(?l8du z(H_gCEYX<-VqDxG74{sk7h&T?ve#&}zOlF5EuOa^vtC3FlvzWu3XS8cJuFH5N6RxQ zNiNoc?b{?6%L=IJkQ9UG<$|jy&b#g~z13DBQcEP(cz?{7FX4h{23OKIhH#QDVwynD z0N7Ph&fKKBi*YPYm0ZqV%new17KD7+Yxd!SMFmGd4s0umEMeMMipvuekq9b5p#)-C zD+NaA62H2>yf9CFE6pOu(ph9Wn5bg(EOTyWCj(llg=@QvSzS$Idu@LeI<}If#9;6* z69-Uzgp5{da!_6y!!}l5pS);ovsP9^vCo7D{kCc=aW+ zJ-l5M^i-M-6+BJ?oktSw({cpd?dY>C*3N9?C3?g!QCUG1)gGh-m8i&r?Z&Eg5@)wh z#1QLsxyn;{wih-wRSMSKCwW|j(yS+FSRw*NGNG@knA1j@64OUw+iW^ZEvL-w&fMfc zvqssD6wt}=(@YIMXAHej8c8lBxs^`jRNIvjMJy@SJR@Z&1WzNAnLK|tiNh@n3f>6I zcfGW%R?Z`7T56OSf3~~PYt4g}c z0U741PA?m^H#KC#T#G`xal78j7e`+rh8h=?QJYa7-2CvzNJT7_usRFP#>(zH%NvO; zZXq>}NX-}=78GaAFGE4wd!uFVD^%J0x;Sm`03s`-R#?lBMM10RZWXBu_E#D$j0|~Q zb_H%9Pp8<6#L|g2sTj@kt_kTWRTPy}S58&btEW8usI-oE=jkqgNxOE=&gyO67p(1R z1-qh-MCOu-sAOppL}~bVaTw9lIl5*JuFY0EcNq`XJ=gbNs5`kv*9DWuWeKMN1WK}} z!hWVU-RdtDVyL0#UuFRfXyF;wxHKt^NC5Qbiv&O|i$Jr^C_VU$qSC` zI5fawmLfpU;frjfK4MTZr+Dw&sahE2m_LEOyO%qIG0VCsZpNDSlj=R-$$8DOdvq*4?-Zo7|O`a>rY~>f$?B->;0pn_wMF_~?0IQ^$ z(zUM;MF(QurSrA!CX&%-Vy<2g%*t{)fIb)^(LnFm`n&9xI+(g!LJD|k*5Dt!{g{I7 zxGh`!wp~1^23&l;;eg7ZZV_u6vl&Y2RSXVUF>eFsfU6G5SUO^%w5bh2nNkR?e8w1< zn;{UeR;kPbo-PVZTSk*c#0>nsm>7V$NjC3z7q$x&@R4;3CryJI7}PLj)54jJK2-MN z$VR_`Ew1OchCw_jBP>#n#T|JXfS*#(@x@iiFJ3G$+dKB{wkz4%O3xVQSY<5n16q@# zQq$pt<-o4wejcNVqU~MEw~8aZgP}b&a<;I*d({BPBR}If>~!&dPsA@2yMEyhUfyWs zT5|5AqlqH1@7OWQ$hlGPrYH-zHU`+@)kB%ua$Dw^bsu6&; zrJ>0lHbS(og)uSLT|%>z2)f!f`Lk*7HPCD4Ri-vqG#PJ>MyA`O4$Wsf z?CUM2^KjYkAX{lQ-pOsPR!G(~2-Jj<=s!<@%Dg?gdSs}anR2ULqpK%&E2Q2A} z0iiMgMn@J7!V+x`73u|ej2Z#zNpvGGjw(q9VN+V-^;9?qEDXKT6Ui0z$tJV zu=QfSJOnr<3ZG0CWB%Bb}6G{snzgF)@NSxZK0W6;G<$138& zMRUE{&f;{6DAg}i7M^fqtF1X>VCO*5N{t_zKV>#mkx#K@p%0r900-Ys?ZWc0m{C4; zE20o+u4>f#uoW%6L03`q?g}pfha@J?QIaUcdE(>|P|2V$;&CQQgOW=~d6PQjCMOV+ z2yPc9@olGNqb0mH?BAGMpotH^LMX*00b0^8W@sS14RI)jCybcf)`fu_r|dYKidf{m zEwv$zT4vNJe8-r;T4z$7d(d;MQ?B#DQ~I&kC}*qYn%-kfyiIf6*GxHzmQgfgm z%}h5P zc#=rWAn8~YWYwmfT};HS3Cl|;&&HyZ0Fkvwh<6stI*M|#9ZG$;9CszCL%I2g;C6U@ zDo(Y`3}}fI?EuI+Gbmc{6=HG&1}!mi{HkTFFdRS`;9CtMe#v38-mc?udvIWi!ouS2 z6%p$)+(w_H39BZM?UtvnW2I_c6|-lk8hjm$w5{U4nN}D(%R_G6@3^DbTIjZK=8M zv0BY?ELKRT0(K0*F@i;m5Wkto9{dohtw7h84{EntV>PAM`h_O$043BQ>LT|*{#*QKm}-|mI>2HauzvabfPoKseuIf5mSo2bQzBD z!ClAP_npx?bs>V*+T0NZy_{w^lzEX>EXT!|mOdonLO@K1EG(w%`0wqvg%Mru8**s0 zAUuKIK=Eqgi6T5gqPl{U=2r`Mw{XhVt8&%aZe)aOBsh+(P;09y{jCIbG8y%KULx&%`~ymkr?$kqE$i4b2*%_eZ+549j@8GMrLR>=`1W+dWFDf z7o$_nTbDAjar{lI;4m}bN)qDjP1VBBJt4PaP|UBYhlWK$(Ek8d1U3WTZ2m)YFoq5a z2B_c(CzShXLxV!@3!P}d_+W*h5alC|IafSTQYPka(J|AOwZRb&MI*hU$65etFbxEl zo12Lm2*Wcpkg?LIfK~#u{{RjtBHIY|_VYE7WrvYlnUspDKI{|}nR(my&6jaacYxX~ zBVEuSv~H1r@1cfz>N!(3CV_Pv59FP#Vi)=1lE>{%*HkfL#oQ})oPJI%yoWa#Pp^8?g2FrsjpS{YBU>%0qh3wcWYX)3{Pl zn$wm=8clt)#~U|6lCMLrhp(mg(6Y${R{>Z7Byy;9VVzX)VeG~y$k?4hmwO$%0#c~o zb}eLpZHYaB7aK*yM^IgAJVrHFFC#zNf&}q0kfc!g99z%_p%Pi`tyIZ!`XWr-g0oZL zp^x2%VyF*NMW=Rb1``wA2%ewwW04p9WpsV`(y_EH_F{q>N^| zXf)9t^2y_`0zUi=ax_i}fr>SZt|aAw+N)a75(Zx$IMeqziTiv7GON+ngm@e-AT;?N zGB{lWDtLP^)WPC6aK&1Q%)ZQDFnA0Ar;b&@awVuZVv1DJtCxii30wt09p`V5#!F_P zKBcWjA6({p;DSFy+tXd9qvFY(f4hb|rV6VhJh1$Q%rPm6-tV!C=;Ghcod7@-(BgNQ?#YCN$BndJwDC}LxTnt(BE8tPBk z8HVEv6Ghdh-&{pvty5vz*`(Wocu=YF!LpH=?C@?e?p?tIQM20J?otWbfJU-f%WxL7 z{9meT?;d!fgMTX}#@zA)M2&)wL9I=Fl*Kd>*mGv=yIJo=)9#8T5Il$sCr?mm(E})I z^XK}SsI=3iwN*yUx-Cr3(`*+4EfKq~NnL;hk*nXH3i)*^xbz8HY%t9%{aBdW+aNOn zO8)?u7$5{wK2qNdRq|*|c;)I5+pM75HtU;9x*K6>BhN!T^~|5uB>Xu zOBKt&=Ujr~w(lOi8;0nSUv45xi|D8ts)ztk11x|9I{yF_aI1B-S~FEY8+)lijhg7P zt+lx*^QjCe!D&{lcxCtD)kY>6PVl$cH(55zn;#Q zc!DZ$^+7gad#>S+ZMFUPW86`eB8wc5u<8k96%oq=(bqK~>RmZ_)B-N7I_gv|d)s!r zf=giVrhuxD^!-Sb6%F9o4_2z0oj;T~gzQrLhUiVW769c+YoTg;NCOl`F(hd%y2~34 zTp4_@Xj?=er6tU9%MgbiNMj_;0G)VgELNV-M~}P_?8ViR1lvul3Jy__TtYNBLrEY= zz}DFScwksnBu8ZSvTK?}AdJxkP9&Bl5%C_3YC3K32wx$6A1KXyi}%c>IB340v(@?4XCuJYUoP`I4R)n@AIsCJ=4kY~158+^M!L_#FQL>dO-z9wlVI zARkF1bAHK+#! z>993C&sPwl>2H*0#WhJ+eEQ0DtO}ZA!vBZ_s!nLC?LHxcZTC!a)0_L|( zg`vCKq-NZVYuBBP0hB3Yp)*}7{+?d}s1tzCjX3fj(dM-+@{Mi#)$ZmmsH;N4cL30J zC)DJRX{R^09PX#3QkmQQe_6>i{Jrz!7VTg%owJeSs~;-UFLpZk)E;K;LEYJ*x5Ns% zTv$X&uM%dPo8wA+J*OHZ&d?w=b74|3a>_{|3o{FffIO+}#c>!z+(JI5&sM5u(!N3wq7x0y$M^-#pjZP#HI3rJYHN_S~%ASxoVnG`imB6KQCltt#(UHYf zJtx#L5o4CdGpG}g6)JpjWioA~%oyCgO$LBGLjM4>0?7n53;UFEwxx~asbv))dog{) z3f_<&RANa$D^W^@0f*diXr@Wotv4Ge?Kghjy^*ikUFC|#Nn*K+OpKaNQL|N4Hzuw) z44C;K@#ThH_K&Jt%p^g+M7OAiLpf1zGSiNri=mJANA}~Bo1jl)uY<2kam~ZIHqGa_ zY%liOy0yQ{&KVtAGpXN0)76>$I+yvsE=;{UrZcJ2Wmzu>qu$#aTiKunqBklSc!kQi z)r$P8E2^&yhmmyvads6~Csh?1t5Du}84al)5+#4sAebHueM94mRt4isatWq|i`ZPe z9&xd%*N+3rej^s{Dd1s?4(`6&Wr|CiTYHX+2aahi;+cc5;!dhMgZ(taaiv|1*)g`D z^jogWC5^?ZT3Q)q;VN1|6ws*wX%R@*L8sze>s)<;s#q5wPO2pQ;_Dge1Wf> zPAd6laB<^RJ3ap7>YI5#D;vlqtcH;Z3X!RgH{n0{rVAY^$&&2!TZD#FY|)7%QJIK_ zQ|(d>2*6te2D3>c=9Z;oMnZBVih=fF7RJ!kkn-F_Pa}_xSdSYcu%^2zC>RiMN{qVM zA&bqv8;Y(%(G00U zNn=w@d2{$SG~1_*%$4eAV3x}W4p$3<(W7sRJ#dEC5oue$^Z9ye-`aRX>kX0CXJ z9FUBMjwpz2$;1gBQJ$b^a3l@b?hs2FTlAJIxaby9Q>b|y3oRl`e5JAdWmRSXlu{a% z%oQt9h{1$2b0ilEUNNOx@Yb~V92y0lwA$|N_T-&n>qjdYD<7#yC3Sh_rxiMqNr}4M z$@+J?FDMGu?JFwtYbUCOBxWb`mcs5@CT$-p){bj%@yukaM(nHxbzLr@D?$9Cn8*19 zl@7Pz^0k;&^&M{7Rs30;bGmDMr*o~ZD}&>%tZ8h-s{)B6)|@aQNU}koI24PM%A?_k z(Q3ouGJPk9hAWdr@KzGmwz8kqLlbZ^C;KqkXEWsKNiYr0-9fgW=i(IP&r;X$g(p@m z=Bx5E{{XFIKMwI{Kj~c|g}j(#*e)!sAmqC}jOVLtbehx^H5q#_&%DKbl!&t2%VjS@ zwD2sXXT#grOg-k7;OJogEo&jBqc$nVZcGJI3Q%t=_EOnAOQhRXj z9m301g{{0oF;mx7W|B=*^=c(KeT#~*6`0o1WBbVYay0I@5@~@{8WZ_K*8^u%wFyb4 zMf4K?07Fkdaa+Z1g0C^MEgC31>|5r5lEQBG#%%)zb|w zn$zrb`YT6_#ZcTV+?Zmp&i(-d$X(8^t&UTSUUG?d$T$e<0 z2==vG1Cn5v(vYRYrJmTbUBYhS6G9%}S&Xs=4ItE^ifW89hM}RMq2N}j1D&Tsv+YJ0 zcJ#Klfn-bBq&G4ve9a*=MQ2eOx-zc1nDr^83g)GYCvc}wgKJUF)vtizi)gtb++HoJ z>yfgrDDyZvqy?lmxA*(Q40g9stJ+4LFaV)d^IB54V`&S9`-;DHO|>x;cp&uWN*y^9kj2pfmFhEgjJ9oG05Dj06Q0vhP}9@ZUT-XPPE6`TXPqCj$N5l}25dd0bG{=Pyib+GQgnO01~W60x)DeUZ2aMgU=frGRM6rCHfTLKs4Vd;GX{s*h(b-HtFVsY z^r^@mIH)5YL8eZMzfhK-0~Faw7mcpk@(3f1R+_Bs3X1ZI6h0Io7ZfOK2BR^E%c9hi0UEP7PEnO)jS5awrWb1rL$>h*ZAx^taw7wh#Z;Rl za9pN~*b;I0WB>;q&0KLYXMi%{kb7;j+di$bW^u!%GcKy1ZzAJma;t7)wiYng=`sj6 z7))|km5Pu@+lM6vdsOPj2~n&DkMPW%q3^{#L2M;!WP&>D>k4uI0O5&U0(zpmx>>EQ zx6MZ6IBbCMt~$#bId38ZHT}t6XC#k2@v9$5*q4x(v|Y@blrjb@BASXHFmO+%w}2MN zk}m6Yb2JYpSe+wFw!G*#3fBStwSuN)vj$jhHUM-C>l=n6MRAIbO*u3tz1^HT0=H5} z6#!?E@xz@3EQqk|(+5JMGBKO|g(KB;00p;`xbNbNLfkS5WSNBrr4>PHc~k7c=E{;Q z9d#3J0~@I(x&m&Kay3IC6a+H({i7KlTU%^59y!z{xOWYN(Lrrey(SV$K;pntm)p#6 z{8m;5`Y>h2`D${vhE_feB@({?TLAJ7t|edUYbh7FvYRa<)GMg}01Pt*;&a#KE2=eRj*RGXqfxmMnSqG}LliZ| zggUyFfH?z-u|&(D3oC<@A`FE+Di`}OD3%~dKT!K{L_#$CGB|~Yu4k}r(6h6KCKt)L z%{1~ES(T8&i5(#!f)%54DHX1D%T0g)H3JkG;47KIHbE@j09OK7y(f9Jyb&Bda zUb<9*Y3#)`Ni@x;b&g0?+JrFWE5Lilh7|#(9*Kmg9E`a8F*z3lTbxm~q`C=HrnK;` z6`6ddo|(mfRw_(7j|Si|=Q8PI)YDp$8+o;lX}#HREKSDcaXb^*Ze~^uY_Mcu5mQN4 zN04}Wv#ymRtxGTU=eFk?JsRrLT7ObkH`pSATPY$AuW%(Y*V01h(Wm|GUko2krwoIg zy5>laD?v7a^sMA_hUxp*9chco0xAD1r`CM#1=%$s3D)Uthis file should be in the contentstore

diff --git a/common/test/data/test_import_course/vertical/vertical_test.xml b/common/test/data/test_import_course/vertical/vertical_test.xml new file mode 100644 index 0000000000..68c5745f37 --- /dev/null +++ b/common/test/data/test_import_course/vertical/vertical_test.xml @@ -0,0 +1,9 @@ + + diff --git a/common/test/data/test_import_course/video/separate_file_video.xml b/common/test/data/test_import_course/video/separate_file_video.xml new file mode 100644 index 0000000000..b90ea9d8c4 --- /dev/null +++ b/common/test/data/test_import_course/video/separate_file_video.xml @@ -0,0 +1 @@ +A4+Rg1+@9R{~I)gih7~~prpVka6<9qbrQI-MTzImOd@k>_wpiqno zFVob$py{{5=<6NkG4I~xfN}b>HUXC~5c|a1;2cM}c&v{KVhxsEHYjPtfB`YuQ$}ko z>(3=oj1JG)RHi0%k&Ee$zj8=(Pr2aMa6x3ZCJLdF()^Z&ag!mY86yn5ehczq)0_no zQ7jaUKWk?fcg#!A5<)rA1lQ1Ly3?eD?W6YUM+AQSq@cri6A`?s$#$%Ab6` z*Z%C!{Y##xpSkCW3O*vq#o|Y(5zTl`z87Qc|L}`K@$^fJ8DW z%?9Pf;@CV z(`fjFhpAFAl9<#MYo6Z| zddLQgQ{vm-Jz-E!SkHLgyuu;suYQTqUu)mGd7FI*58Ge;-GlbYXOB4za-sdrFI@U~ z)}Hb4wIT{#|73%!o+X!Z8%7|%dI&fSSh%``Nw=c3WFP{xn#9-F58EGnueE>i`?uQL z?|;}j`Rs|Kqlcq)2q+fIEA-_D5K)!jmR3kiGfDA zwHYV9!W*UnrNNe`HW$zVl4l7X$(eO!lAoq!cBH9y(`KAy(6(69hlrnwzB!$`YftMs3lK`tXD{wgs077*|Nzcr}E z6L?36hXJHm#m|}hp@inO&Fj>{#CHF#gK#%*a4gO#gKqR04<0>W(FddS3_Mu~Ub}U* zUBAUg#Axk!~q4oeRv1vBg^mQo1$ z$y=UfjOKG)71V3z*A!@0FDt&W9Dv$hJ8u8tH!s@1`lsJv4*y%MPJ0`X(R8O}HGlTF z9X|cM{qdjgw!i+wroOQ%z7!?lTNP`#v0&d7zgNJ^#U0NcKjFCRb>OQ=rm3cU?CldN zMl&!5Sux>=@3_pIk=__3K_W7mM266cgzptorPc2>LnhJ~CWrb=a*Z?1fWly0Tp*ls zBi#X~uUP6XRNy)Ti8^A!sPU{>-@MCNPDpiohlzCtnmnz+1@f*N{^XaRaw_4=#;P%z z%nUT4TvkdhaIm1+VEW?54NfJbsk)4!Uttmc63yHNKKJ43p-WdYQ8T%N#b&Z=hrLW` zz7pT11j z7%2lyvs?N|LW9p>7suk@E&u>P07*naRPY&XMp6oQ0UB!73ew6+pS|a4kx4E?4w;s-+%h!#J_7X)Z0Seo zg;K!T;EaXbdOJM4L=74Rr7I~o5=i&zpVk?|+RDu@gIG=Zk zzT4^U*py5_h0TJ$=B}DiASlV06uBJ!zodjU;htvT=$sSR?%Yr@%b?51!Xzll=n_5X z)PN{8Ucno7OnfD`lbl2tg+%w@QJFDGLJufG_#!5_!DF3OIJdVin1f}VFa0k!fZO~{ zUzUvl`z*3>i<+yUJYCR5``U_Yjc;*)+x2T~*;`-A6weJtJui=5(8^>AhNkf)QwP#C zQ=FD@@Tt;M>BvBmLhu(O196_n%80BnZfn%GW}JOrm5I_lwXDhTvL9@%n`M^Z8P;Qb zYjcIb{3kEK@m%4=35`vBUKRpg_Nh|~%MbBbsKIV}3Iu9?t8e{)weYqTUP@2;c+st& zJeO*g0_H4ayrD+nw#ZZtpFUwaX#N4GJ92{K`7+aD+t(T3dz_VFt~A?Xhmbh#-~WQa zKDPR?j`ZGbPW)gB?ni(3D|Xn}Wh#9uovI|7Tx;iCy2)YlH=zm3t-|${SNdOcQYdw1=5`-6Y9(cXIReZ==h(lU3#Hq}?3wB3h4YJd9Y`|aQT z$wmA0Ir0HXNJ2aKeHW>0!JBgMN|pl{Zn^2oI0pfva!iy!qZ7OsTWp<8)~?`bNg%di ze3Mip;`Amn!UQR|Fc+jmN8#pHndaDB;xKNfI-EM-+coT!e1#z#?fO{@gbpc}Ab<2#@l=mP-Z+RZJANj7$oo7 zp#2zYbZh7EC9@_#=6%Lc0hCd--w{}|nDd;NL&DjcstUnTT>HKnq&27r;3a$Uo!5=5 zG(-vGFMiPpFMW;GTGN1Guo}Gn;xqoCi$wV6`?w5`_ga?t82cr=eJWoloQ3fQEtM)p zLZ8VzcT`yg42*Jzrr*20b(_ufc%D4sBVX6~${&M$PCq#Z?`p7R(nd7@8;Ya-s0O=u&th#V)!RFfljEjQ4EA@nb^ClO~V@BK=C#gPH{5z2j@!CU! zjZlW-K}Zueic=fc?rpVu@7-v3*n7E7sbwNO#c!Vxzz1JEYM+0`ahs1B;Gln#z!^qL z8wO*REb0*1lwv$;rVsvv7}&3#wt6!N53wY%K{s#?kmA|ZKJVYz;`1CHMOw#+I%j){ zf;%5;^H2614fW$y^1JUgI?!xT6Rk6Kah*fH*=mcQgDuIgKc z_=53WySl=Wun(E`!4rP&yzvgH)J;Q6Eefo<>?ce`z|EbM!V88_j z(S3Mxp?&@l1@h7(&cIq~A3d0FfAz^+`{hGUkZ}2ym;5NBBs9b&TwybJy@o+1SXOXo zXLg|~To@}0?F#^Ou(|^UTj;VPN&*VKRD9N0jTi!xL=P^62Pd!dEK6Izwbs7-{kPf= zKKP)$b(O&wns^HHgnp7I-dp`&Xa4i{O-?XC@qcvnVf*x>XYIk~PrzW}Z9ejV;gpgJ z0sNtn#1+%lS0NhU3YYhI3e45t;wPBG1Y5!+qM*)D!cF>1HjM!wy#8EM?1A4~EUd65 zu}SCG)-~`8YNRDf;kVy@BMT_D=nr3N3@DjRVAB{SAvsxOP!#bOx0iiDYcOGLQo)l3 zCM7jmr!}0ltj0|t5ZOc{F;1JtJ&k3!!t)b{&0m|xnHG`(@Y(FuXf%cnq zD=rN)1qZOgv(epESmJZEI6Sd}%9BfcNgo#f@D5oA?sDO_z@m623|UzScqIdHqf{D& zm-D@IELyq7!jsP~93wAzjXDQ^(TQ{+34$j-zM(%uf*Fp&X_jjMQyVvI1FIW%$ z^tlJxb@s_S=S9^S4F|_WMB%Y07QbTl=3gE9e}!o_vdDk zYHbF;3JXqmbP_mG^aRu`iR_3IAMd3r^X)qvIQ1Lvzu#`tj9FvoJ|aYbZu7dJ zCeYYiFT2D^3EzJAJyyaTx5tn6QX4NA4s5PYSA7z{KlMuflE(1nYrz{H{w?K48uXf6 zBIvlgpK;ttv(1;!ukumge6|8U{ZN!=72UaWn{K7zqfZL3dsRKvd^sWZWL1%aQ zsMaQfS952aq0#@at`YMjo zyp0fKybRJ#tX_e|;oPc5t6^Kqx-f%p$fmi?9C3uG=3Qcxh9B}{u9)2mHQ=N%GtHGL zBx_`0896oym(;$O;5g^h3MY9|Q_G%(3Cug1tBh5u`eC{lxg+Ii@(E}D@H?YMw(n}8 zzD~DeosGT^IWX|a{m)|ZM+eMv)AHG3#n~F;1()ZXXnFlQaXI$r;1PuZX;GHYQfI`O zf+Vy{T!xLSoWo+)VMkIp=G58W~ z(wUcpf)i}`KrkT;&+jlQ_`~1)UiFz5P~u>&-XXEv6=%!(BqDDF-oH4TSz0=}e7F z`s}+Ecj`FljbbBs48{c&^BSW4SL9%Z2y0E9chf`R7UOcIlhZCfhrw!KCfM54%bYfj zSCVIqMwsEobwEj(SqMnapEZ~@Dz#{g-I3~jeoXI#SEq`o5ifz!U@qBZl%iVuB;Z#~5y+6f{~Ji2Etd_Fash}xOR+nd z9zk@Kyn0xjSJRouBGMZwqMD~EbNdblb-NOXC40_qR#UNXIyE9gRv_-FR3X-x?7w~e z&Gu^_oU~`GE&V$(!84OWXY|*Sh@vGu%^tOg(0!M7rPKv-{Xz*)jqqr=bdzo!-OznE z4HhCh3?ePj^l)z4<#^ldC3X2BXwpPG;Bc}H=4L(R!G#qY7?Q^7VdU14ul70VL4{^O zYMGI8Ymi*v^TW`SzMr+AoI}^1?E-u3GRlcxMhN8(K4Qcgi*9Qt6Kt($zt%+*8ormg z9%IMt9Bt?-58iE-x`CfV*OGYqsN!+?VsIMHPrJq-6eH))P_?k>0Yi36p~gPPoN)C| zi)MyC=G_wLIa173cRn*`I;rt2l#n~=oTpXg*nq}6Z=ajXFup+pn`Lks$2KQ{+_}N& zj*N9YV{Nx*Cf~ltQc%uZ_6Q-rz~_gu{kHDmD-K>?kY|mk3{ughhSOnM$Y)&fE01{M zSMUiez;X&K{0OgC5hT1~i%C71!uekD0vg>Negnnqb5hTAyoRZFu_MysU$Np{JwZ2c zPAa;IyEyWwqKce|MiQ8t&NxI<4D*P87{t?rtS+;|g;g%D!F8ZPL$#kI(M%kJ$eK0y zXcE~*-zF9BGW`Ge{af^FA2V0y7Bu6jTOq965=D}D2X{U~Eev5MvqYPh39vjFxx$TE z@%5JB!i(Y1A3Zx|K|dgy7^fAJyf0uVF0i;sje3Pc!(CszK%@(5ogXLQ)YKp|TD+RskwNMoXj z&l1NAPlJ>eErRsJqjm5tJ}#t@Xvu-dJKmsUdmXiRwI-mY5y5BW6EM;TuLi74>{@Iy zn0IA=EgR;K7(G7bn3{zZw(hZZ*_CG+zB_BI1~teoGkfh5^&@fzND#N@gupg zmv>(3t%pyzihlJd!DZ~=2VTROq%-k(4_;ynym5BePJFh4BhkfBMd&z-pBQe(s}1DP z0_39|6b|!&|G^-bvLdRO>b_D zR2GRwsZ5oLhG;x%Mh(_H)58Qrql}L{FNtd*0*`5O5q|06-~>~$0AqY`A+QuKq#<9E zXUsvr6uvyHQEfVAg_R`aQ$F~mNh~NR;{{I(9!_PU#h(R%Z$kKP!71Q8X<48^bOhO{ zvqh%SJWKi|%NOTX=v%Tbd6B}z&yl#>%KX@&#k08Jw|BuYd%=%BHMoOY{@}kxxQk!G z!$*hhZ^q)UI5ght7d*e_B^(|Tzlt~F%*LN}SNN2#349y_XCVzQU8f~fX|Q>YjlrjX zG>--D5##ES^YF%x|5+nQCX*Q!_Q#a!Zf3U0X-8dD>W+$JHjzWokp=q&{JFHt5_XL} zs@FN${#zfu+n#>;5vLe(N??Rd6fT7&%@QdY5G)Dd1_5D(<1g<*t7wTQ9KHtS0uMn3 zd#RwO*;o#DDiDUNf_TcH3;perx~JAbhjj*=oQkkEHAQ}{ZvK=*yls|hNGQF>(#*4T z(e<{q-xXMJCI-ToW7=g5m}J!$Ollmp2ARxYv{F6Pu;;UU(LSGs$dwUPo-!{GgH0I( zO8QBC;$fIEYMQ~y!Hlu#e8xIr&STct&etC$C%ur_d%m3L1Doq!%9IN#C@w{*|VgXCe`% zA~{d%9#~I^@O)qgpq3UnW`-3EK=uU2^s}w*kQIrPcxY>lai5(g6 zZr{y*zN(eM6AUjT^-&(kOk32bZKZNx3-&}IVbj?Z(0q+?J0_ro0GCu?n5o6UZ=tD# zt%x&_B5rF^l@a_DlUdE=e5xmF%z4rRMxI`_%>ol525Phv2)K=FQ(0;r9V%zm@Y#n; zv)REX$01}g!98~X6ji?=chgRWDi4pkkq3w{9qbni*s{kq$gA^bd?th~kbL<11v`Lt zNWaIizWvU7v}RV?ox5+Q*?otv@GUKJM6KzNZWIN5DmHi@e$s#aD%jWkI-~KdaOF@q zPvLXm>Xt4~22g3CwUN+>9%$ut!zlBY?mGeYSa)>o`_ zQd#ru9h78)^~MmQQaj6r>qu6Uh~}*MW88BV zVR(FvHDdC&i#%vVWgf^FJcJLO$o+J*90>4hfsWieo#OdhyG*V5F|iE}Uw5i)mwn4S zoH2dv+I8}XuOl3P#zuOkrihy~gH!Q^RGt=ImFtFknHN*zD?h`^-yG_I6IZ>@#<7OW zWxVbj4+q}FVXTA!9Q>0=;c$R|!&7)9-tmZ~c$XM%W2hwjD?bM@Gpb=Gh3tfKF)*Q9 zQG>W(K>$MRzP)0=;0Xs$@6#vUWcLUSFRAbw$&+c^{xgy&a|?`alK=E0{R|{)mDwO} zEq6A@2xhnxhnF-;B=wAE-2zv>QPTM0srHBQ5~KWVhS#6W_Yf7H*0U4<_r8feOKSz z!4ovjbEXdlUgLxh=-9MPJm~`C=(B~T>2e2=XK$UbF1=Xcs27^Zn>)Ai*$@|4c*e44 zM?Zh@Qw-OZ4F#d;QrfhQCeI?p4qJGKH3+IvL8OZ-np+ zD?A5|fx)2H(E4J$iZj(u@QN?M1rzi*^MQ5JYztEuOs5z~1meh&*GNcSa_jzbXGzDO zmOpM)DqZ1cvG|!3=@}4MZeO%K_Ar>I#FC;U4rPRE8NL*z^VC8?jNJbST@V) zn9dOfBetfA1t371KgK^~QTqk!tMNh}rd7%KXY&(&RbE~4Doz3M@m*KeC}RLghpi1v z!o>`iG);vgRw+G}rD0w93(IrrtXT}vqgVWF*GdN~k z!I5mIbXNF&lD6$84ZqINmZ)v{;Oz;0a}Or48@9a0$Hg%C3)143392~>-kug7b;%po zuph(gGN4|D-_Y}aJdRew_hXoi{%$8t(iWU1Ux#E_u|FcNC~8QCgNPwP@PXp z{}2;SjzOSHiXjGFPYd+eldGK7WQi|57lTmtM~VDqj~fI|84xku%S`sWw#N5ET`VJtf!c3ZL9LZyoUtV%@773Gq^2ufLgWWx;&AM#O>2z`o;;*Y!&!*ps1l@J*}D^nQGUNgBkg$x zqAoB{MRn;iMN1!3*b5Z%U&D<2tg#_3nt13YAg3GRnS7E6{;nUkrd3w4KvU3b*$EC3 zX11z?kqK#0#uoN#$oYV;#=CNApQbT~(SV7;KMot%W)9br7#+kie>yY4C2OFppUAmN z`Sq;#c;=Q=;XU<_90Dq}9xki_KD^aKM?XUJw8{LLZ%oT38b?l<#_En}RBw3mjImJxQYcBd82;sElavs7b{$ z_!%6PY^GvXSt=Ep+_(q_Py_J!ieoOH)1+Tzud1Jr)c9QgusFv#Pb?7efbazjcZJgw zRX|6?E$#jO>ydoJuY*+cwuO;TnyEZ(rY?EzQp!PPN191v4FuUGpcZw#vatC3G(1ga zYDedAGrbCpIZiF~pm5Vp^`2&}Ct#Rk^5dav8ilQw7zxi>X-1lfkBCWY8GPn}Nne`L z&~vSBM*Gn;>kZXZGR7;7sXVY*3MtaZR>MqJ$xjuw=1=k!CD0f}AU&1PKK{8a1@;pk z(x3K_cMm8ZgJx+d&|goVn9=N0(sv@yu&~OCy31XbR*Dk7{HmUZ?I|n2`?Il13xoGC zCQahVI$UuN8{Eq`Y0BGx3Phz?j9;3O^9IyJ)xb>z+9lm?JvPnqAIXL`xOA5(O~Xlo5vQ6P{u`-$@BI&Tdwl5wC3 zU(;dZEMEa%{#m@y1DkTNj9J`oO$jJvQqf9FhJjF*AT9abDNu%%9_|O)&Y4GyB zK3hV~OI&MG^GQA`-`v_)-}%|cNHy+PG=CTPV7L2b_xOI02d|%S#LO<;56|N|M$T90 zwpfeVD-fNW!FK6YWw_+I@b7;HPcDJ=Fs(ien8YhTy(U*+dzA1`$Ur!Wr%Y%WJi^1w zv#BJ~gm(nx^BUUmGC@1=%DbLX1o4m(!YC3GMYQJr>x}fsh0K=%7{)UA$zldeeS*)9 zFF1JXxc%MMBfgZr&b%8Rf8rb+@{Xn&FeW7ZC9zS`BXtL6++4oP3Z4D^LrPE-IE>1t zbwD`n8A5!*wj&gz6uyrWp~=yZ=wSU0FE;OBeC?H3du#E0XHnh9a z$phQ0`Te++ie&BRmOLBFTZ|~5Gq-xiT<$70@jiSn*wj@37^2@Uwbndi$)WR$?k|o) zkQ3tAYNmnFN+aX%z|X`uj&@^t=n zffhscFO{@HfqP`2^c;9ghAB4LPGLzHhG0cY<0h+Zl)oSj5{bcu81f#Y?CFJt7^(ou z*`jP*bBum3Z_bDjAm1N`=)VaxCh44_`osPa<3Fj6)32gZXnYYC00SW}alExHnL>p`H z4G~|J3D7JiCNT%E1!wE#^{X`dbwA(Il%~sKd~f!8GS8_2IJ^QhndQ+lHEOE@9n$=Hv3I~XOQ}szx*;4@ z3~0C}UO29VmRjHkFEM}_0So49*z~3c4@-IzYCp)2COel*#YXARsw|c&I)LE%Vhir$ zA2f1A-p+Kg3;g!inE$1(!=3{dlxvwq7TEkv-ExD9ZZA&752@N8jy;L`ye{>xcwR zN*9mhC{m}esDbVGE>QcVrownif$aZPvz7q(gJbYmAf-?uy=Ux!jRC+zeU^hFs1k+0 zHE7AkFSe&|%)1JnZ_C60amN^I0Z0~-kZ~5A7AS6^bF+U2qLe?p+A47#*3%@dr8Tg~ zm0Et;Wt(hAG@i1sAgHZ~$XdooLJSjr?PJTI{%VPT0q_Y;TEiTpYQg4V?pu5)+inK% zG|`{G+{X|3vx;knO$CmByyEKxo5=Q^x9%{-c%LS;ecj4)MAk2JyWoMQan3SmRx?Z;PTnfAVEJq%d;V&zx;Lny%GXrPk6&NJl=m zL4S0Sxn28Vrzr0{hU?(h1uKB8p+cAR+G%VXriB3KckA6Ij3knMI1iu9e z-}7@eSLg|yobSXUi=0jg+fIF0m^ig{?m(n}@NU|yaVfbe990?nxVF0ZwhteUrsBjX z`AjGVOOaB>i+rwdEbS3{oUc&xS{UrIF8uM+JyuRtGrtPIh_T-^(w*<2^q+hLw z(EuGNSQ=NEPQ6XW9pt$n2Uy;ULfs36Ze668$6&!oqqhD3D0{OX%aY_y&tu=0jEIcf zDzmaGt9sw2H#Mh84mlcPfPt2QfB`hni$Dkh^djg@ulxtZR|ycHd65JO5ClOGKn@#Aj3nYo#po0|(K1H_AEDdxsVvVn#u-OH?mas! zADp7D$Cb>}=NsjfYrEz8{tf1oDtz#L3vq8V2Y!G{&5!QhFAqL>j0b-$sw_gSn82;z zEL*Yetn|vi_4&o}^)K8gw{CFOE5~%4jV&@#kC(mYAG62xae3uqVE*gPa`(BF6ICpg zg|g(nrFuLuhvgur*RSiJxN<<}u8V92*uhlML!c);w+Bw*sRGV#6$!^V>d^&m=m72j z7Zwr8M~&tv7E%XfGo_M9u@-w?o6(ckMiHCM+xeOX8FKp^Jz`tQf#?7pY8iR9{YQXP z304BO-1H4Vgp;KN;+%qob?l{XM6ztgg=u9luw!eDxl;}Y5FMwRcVGj}_71Lrh-#6i>tQ1_8qbgoLA6e?GRWd+jv9qjhW zukFu1*a_np~Y&Eg(c+L{=;dFQHiv{CT;-HpAx4HI=Fz*%26{r z|4QGEqC;7{iFC*F0p^_$ZOt@y;S6dg#Gwjxc?=2$@Ruv^2z#^*0WuN%0P*GN!=3Wp z2U}(B$|FvlTZqUxjVJzH<}&a6@`G~k9mJeXJodw_9Nm(I`Gbpd1ZrPEIa~mZBhl=+3`Ac6Dpz2&Ckr3wl%add!{Db&WE`qyTpMSA(}`!8^(K7q zUg737ZC1b3_w!%jq}{rpn#XWjKbW?a!}soc7nF7;VT0u$q&`5WcKa|z9VK?0R-*B< zAT4Z1Kz7t{K`!K13qD9=3A@aW@T=r;4?DP*93S5;pFF_J9)xlUr{Bwtm@Kv2zxxDd z_8fPJ2x!hhhOQ1{h@o>BK+G+zm&w^1I00iG{P01UI>B5JqT1NPbaJa#-s4=CA3s2I zy@OH)y^6RuxoXNl&BAYPV6;HJmSowULc=}s8Q<&282dZlo_ zcz~irql*I&`xG;}X~auU!<$6mrlCl7pO{C|Bfcm;s+xhoY1Y(_01J^ZR?aAA)3$en zMOBR&)Up~gn&xmj!iirAka7SUj0hpnTluEwPZXY;cGtOT<@nauz8goh6tPXOaw~^8 zW5AI7zARP>Q-II&gnQ3kr>K9 zKw3Y`<&|4F?9h9@{QvuL8jx1$;8$oig>=y_TW$DigFp?hWovO-D(~&Ni^seFgq%E$ z(beZ1I^ST1yZ4`_?dAtIG5JItMCHJxKrQxu(I9(fEx%27wzqk2NQ*)vea(Ln1a#d)uLX;n0xa>kB? zCUPq7x|Pg9u!q*y8Cw)IB5ejMi9v-8_0If~cL>Mrdr%dzLGV<7l!jSJ@^+a?#13bR z$O|#x^J$c_WIZ>vF`>~_MgfU-k4-d0rSZtI9w!y^?E~7t_K=W8ifk@`pxp184egZ0 ztVKqDqG`^Zxi@X5@(Umttc)W^Ha}rUN+Gd5E1x#LCpDO-hAcTYg@Mn0a09r5rhdhu z8=y@O&{IqUPaVmlk6xe)q5cfC;@QQk;$02%n}uzc&$R>>lMTnIDa3u-D}!Fr*fKSB zgI4b^=ih~<&%3s|P=<@C8;s2L#nqRt#<5#XGIa&b9X}%;XR@3g8BTd#R0*rTQGr%g z0@6&iErt`O;|*v8X$PI6={-T&VAE81wIc@!&4A8P?Am2-?FpNm4_Lo_z@qiyBF6a; z|2~9z(u3i z#d3+!XAcFLqOoOE=`@EaBRQcBpJBw;oRM;abQHCmVw-G(R8iG=9#yaoa7x-2C7R_T zqR}jxp}vC5P9?GNxo%4J`Y*5fd9;B{(C4J2BgB^C#YzL-Op#EZN@ zCCP&r&T;KOs4-<|3Z0ZUEx-Uyy`2uV0b6C+DdP1uMil#pv@XYPcn0ku3btuP;zQ!j zaQcAj?2kCP@RX?q+1QL1uiC%~qq_Cp!YrK61LAl&br+Gow10a}U2@g%mt%iQ?p^d` zlFybQ_?MD)@QGE20Pp5R)D*UbRxtjCMgZWy`dq&mJ@Q*Vbn(x@8(?SE3Z&p&Yt(1I zTQcw5cx-JlD2NgBos7luTpi-&*__FPPl|lj(&)z=((6CriEIZf2WT?s(H-Gjl*3Bb zH9!)Ot_s0cR=~ATfgTQsXb7C61005N(3XY}3s^+$?QDTCM<6KcuBjG@0?^+)K&a!Btd8UyF?Ue3;n>#S*$iSiAd3)BLS^w?p z^qee#lDrKMm!J!r)>n>2LAcU2#-&Yc3K#F}Nv9Q=S}>-+K+HaH?Ur)!fMU>nW3I^x z0izubbgpe&#eYMsb71?-L)xjLfSSabNyst~F7YQEr4K zxQ10aAloiV;`3awye6-oUW&k{%6QJ}BY~ODnBAO~05eNkek`E`)V=4G&k|8>o{AiT zNZ24Pkp@@Ske^bC3f@B0I4KVC5|v-`O(XFm7dv)z7cM^*X&^czk^z46C&kXaJ|}iY z<|9oN*B}Ov*7JJwaeNAbOh6nW!~shSW-7NBIn&K2NScv|B@zhNHAKZXxAcv|nS6Kj zukR+MjEHq|4$CjHo>1-y5WEd3Dq|6GDkY^ljyNlhpUG86c zmUgloj9o9E#44`(Snqj1kL7EtFHmhR=dIq?q*Ex$PBGw0ggLTOja zqkFgTULiQ^Qt`bDb2#jBNS2xyBSh(TuODcgM8luK-6IAU%D%Ez~{ zcjUYv%+!o0jn8n{@w4pdRWzpMAvPBMLGE$NIxfd$O?RFJfA@7xvJ9f3f{Gx`6^)w? z1FS?51fWcoLB){mc#22*36^uh;Xqgxf`|cp=GSU~duT<|{SZ@M?y8YK#v6*iV7Zil zuP4gJHKO4I?Pe3E?;w~7klUj(Ci2T}AHi69+_SW&f>E`&*fF$MtFx+N?{{Yl7i z1nadiIJ|`9_PJ3+(wuQi8Amvgz)b`F6CBPFS0{IqM=XOKa!Ng7?6HFd17O5*20_K{ z6Ewc!mUXMUx-0{O2JiMsy#r3E01ob8;OIEQ%7a>cH_J~Fch6%+kxEVtN3^HV#BYdg z-}C2Bx*oW+fAe;kkM#o)YFzI|ZMaPlyQ|`sedlFe+VjPfH9zYRVzSJpL9?6wAZ_3W zN`;y7S$pI8k_hKmo2aJiKDYOF?|RP|{DE^0BuZ{6YX?lgwNeBOKGaBt*J$UN``-Qr zMagFxuvU#5l9wM&_}f_H=R%N{0i&WkE#ecoQgaCM8l3 zLk`L#+^i}tDzS=#DrQigAU=Grv_47x*9+ z?G+nyI|QQC>@$4_S2jp1G{f`aAZA0e=I!nxwd%srjgig)^qb}1F5)uTr4{|2ya1No z=59HurKRiseD^u6lgGp{N*B$t_`!SHhrH`YLWNl)7>O&92T$hM$IwFz^y)A%&*yP# z8pYk@F$ANFIJnikfD&-ji=@#J7=tjsTShRzthww}A54%4KGcLlNvxVb$WX6%#ehlnzAD%08N9bNo zyc>gK;ED)`aw08>>7e7x*I1eY$@ZALPQg{<6AGjpgWI?RXR_n8p020zN=~b>i0iV7 zpw0;!(Z)rKBC#T+c!^vSu1}p)GwxA5IWHu_sExv$HH2xI4U_mlasq>-Dt4U7I8Plx zgoLtqr)476ng$ra)t^$@So>t)IJJ-~#gK!$z346(BHCACY#@>r;r^ABp>S$|H(dE=ZqF28v`WEb#m>@^Zn+aV@d{w z3FgtGDzE^UqES7igo1RgiRmJz6kc7!mlVfy?6dL^qE88^24E_RP&y06RS}+WpS$0D z@F#I(Rb6CaB)BCeyb^Hxp3lGfW4@F#X0!B_@IqKG#{}+qIf%teyp>*vSIbag)V8x; z^%^|z8ltViwd1=u-7Q5<{F-U|Y9q97fEQl|SKth#4*~H#9ZGmBeg!}gNZit-G9*yE zsh6p&q3<=oz z`+m4$+oTCg3cO`)S=Y;1)dC74IO2+x6s5=7}gBO|tMMLfaD} zOy0!Je3pSju9s&y6!piQ7dJsOG%VL;)8lZBK8bUF$F$#jdLtp+*r%1ym8ReIDHX&PxN*;*a z8XAm_*bG5Plo~Zy@yhYQ$4*3)3#P48UI!Qmk$7?utE}aerj;)HoJm9=Mnp#lwg-1o z@pOtt++`ib+&;z!S^o{vS9rQib;H7>b6%EmG>DMAEQ2Dc+G2MOjcB03SvhC4(HR`^ z%w;h47CZly$Rtu*)nh<4Lv+nrrBXAEv~I4iveQ5DAVb=qlL)A%#?omA0TF|O@+Xfb ziFFkg`5dthufsRFtb?lB^<9)~S6}gHmaCN8;f_>kv!oW<-+tMc#0utyxOn1zAzi{7 zmh}CfN51fl}@?`+OrV*!ns`#wTW#S>g=T)hh z`P+)t>Q(2O0q@VRTo$GkwHAyF?W-K2u-(;&9Hh@49u!C=N+ zasc@xR5@@?QD_9pUHTwG#L^Hl#O#t%${?~6$2Cz~#7-S|E-Gy;5f*bZxyo%jKq_L|Fi*wDDm^%`7|^3 zlwXQBPZPI+RQz@@s#1nh3++O?oj5NhPbHIu@c7ua zFhf_Z=THETElk{{s)p41Q`h=j?>-N%#H()&=Np_soIJFMrftuD7=BK~{3I3th1J@j z#jPnCob&WPwY<|`z#fmrI3@FQ`!kQ4a8MOeguH9b1!bgM4QPF4KZ@mv90*SJPc!(ZR5$IM>X;f5qGH)~w z&2s6&9Xx%to17x2*sJk?TH_EAdAkhco>~!hZW^0c=73r~ITS?Pm0QqeRU0T`0^GG! zD($WriB|DlrHaTn&h8#fCSCWOU{J`~X*eA)WlmH}g2>W{QZL|6A(#=ANT$IgA%v|6 z>bL6GiHNS}QnypqkWUp~&sZ)xbqk=1DWLLAxN;{sv@PyjHJ&Q?<}RKVR>f`F0lRD0 zMo$q-okA#5+m}iVLZFc-0AakyS)sdd$=U&|4f8D@ZFBxI=nyCUl=e{%z`8h`RC{MW zQ4R+wo`D(V6$9BUUMiL^tsuUSqmVkLe>lkkU!M*zGZ3nnU+tzhGXU%^{?|^<6VTDH`=Qv5G_qi6l2K!q_f%ErNo=8b5^jubZ@Y01zhTWBXTSW*ns$~kCJS1W(jxR4mO9Vm( zV~TOmU{2%3U>uQGv9FJ+y#Jg;gsM2H4RY2z$v}mT#)YliRg9!8%yqMI9puikj!21CT25hjmWEhb z8u2s&)VZOx_6s}^sI5n_NmQ#CZx#8#-+>r2aEfhI@w69~B zQCjs7A%zU0GoQ3wdcckrmnEd^=E2ZJMH5muY30t`A&Q=LWUx>HG{KtimAX0S+W*SkQxv!?4Dx$oW za7-}D0DX8+-O{*zzPo{e3w^cC2KiW2QGyz~^#%!p_~PBgtKqcm=OE-|T3Rc=J{dfs zZ!pW{T5$rbTFs@!AzH#J%%Qxy(nu*Cxf#>w0hOOdxHV(|yD|V_wY<)s8Z3)p^_T?l zP-VQKf+U~(Sm0LS3eSdWfB^@PL7lo}gEU?17&}$-fyj zT6VX}5_K=rvuKW;+cK}y2!onKeFy(} zbCr0_tHKElm?t2Nn|Gdl2XhS-XiCawp4%gDHoos69Nz0in(k_GE1?DyUn3gG_Ig6cfOlm8kS3-nFKkEK986+j$4?O zg~>7lGTif6#r^4Wdz85vp0yKt5?-@YZfdh2+EL(T$T5WlmJH|4HFs1;L%+=+{CrS{^6>Qrm zdMpFtukR5t$+Pa>0Vrj+Sh@q6n4(+`idrht&MJz`OYwW0EYfHX6UYu&1da$a8*Ot#&qPU}vcsyOGizBo>0V435vtsW+ti%VzaE4Qw} z?dQt(e*Tp6X<4%)uMoP3QIUkO^ho#Hy>d%<8>=ir7viofEE`T)b}hd``!;1nq9)*0M|`)&O(N!uAufLBwSM`hzxHNXzqF=+R{s2FACxCsI1qC%f?JtX*D}W- zi(GY=eYtwlSXqR#8V4m%a}4Ktrrb)RO#47c;V2@lg(AL&7_Nib-+K_<5!%!dJU6-q z{OInx<>sj-0pb9@g-_(Cm1B; zbP|uEobBeNX7`Y?#D$E6Ea4GPu~>^GIlTkI4kyzcI(K!K6J@qR`yh{mtNT;u?bdad zc_X1b)0S{u2Zjlx2Ywj{m`-qfhd!=D<)k_F&(Q@0&ESc2p|QsfCWtW2C<3Qx8;Lv` zbfgj2$4@+dlUm4udnX(;&Ol>57fJNR8&}HbF5`{{Y2il)PZ_w-YHKZ)J}0jZPg)z; z4(XgBd@-nfH<{NkKGl%(Uz5rV>OGNbemqK8pPsf7*Qb9Y9e656*Q=I7=|st=(pvpq zrGzeCR}OlhOIg#^t$AIXnplENTXY4i1-05~2hu4$#^2DO5Va9PWFjCDTik&5C%_Ho zh3A@mN0y0HLyTq=yWTtv&F8$=I0@)rX;ls|gwE+QvlDh;A%Mfe0uI@BGm=jnL`qSf8%9J_l&?1;D^Xpmg4 zKp_pwUQ<##kYyh&VVdwtX!oCPl_$@)Ix!gyG+b!!-bZjs4I13wjX$a5#~>)0QcgZ# zt%Kby3@yxkClx{A_AY(&=x|RIa9^daVh-i7%q>NNx&%@=V9v>*Y@So(pm74Cw`DSE z>CE+A{vE(&b$^5WNmp2N`jiM2VeFnd&$aRQQK8G7CpZR0DdMoliSZEs5%W()W&3k( zZXOYKE=Pk})-&R5Jb%XSoa*xQ1`hdI0fQSfh;5_fn>{VaTfB zkzaW-s1>tPz6qkz2Z$YO7MUw0qZ-G+q_VI+bRg=DUxE0)g&WV86qvi=N$7}D!!v&v za8c39%ZQf141KO|rb#GawY(!`WoESe?mzl+S%lkeJYj=7oC9YVWp3?8`RbRxLi(}t zy|>;j8|=;9+So=UK1K98!hOvZ5QBAF8f*$J>NxIL%+VBJiWa(C< z-)FrNPWuAiz#}+RV{NA_E#Uk+h7iy#L@8I)K00QPl&>0gkPSqsS*Ap*38t6gt$TqF z8Pvw<%ZYGlMtcUBXl?nfX4fMp4=`wOo%R_=P#!QqOib@#^E|~jTAFemVt;(V0Mo

gG6rIoyo6k4l>d(sMD_3bQ@Pzii|Majd&5f7&rK=o1zs;^8men{` z#!V8@AB?L_?z086?&W>swObgnjIkwev%LNO({j+8${9<2{7PloMx8WJBh7$_6r>S&jh|oo ztCrCvt?_Hi<@4Y9d)Z*|!T0`(El_n?vLjj&beWkXq-z0f?5@Ur6gAV=mdY$UNcQmW zbjF&W@rGL{jYd!{41H@kYAYq4kF}aLxPGcIlB*vHv?xAG*xH|4l+`A5uw7z z&J~1xeQmP5iV56f_F&ooTK@FmY_Ebryhz zr65k}Bm@;K4s8C}?1PH<4esGMcnsAZux7e}Vqtf0x9mW82m4M+5nIu6KY4~&4s6fB z)dBj_l~>_dBPb#$7o1hi>8^Itej^Zl=$c&vP!2&NQ*+aAvuj{5g;%E%w**JSx7diAOl^i1;xNho_*p6a7-1rd= z>|FlQ0;#``m&60sxSeN1-1|BOz$DQ;k1+DMys`vwPT)MC+G&r?uSb-9%z$`w@T6Sr zzs4zeoG^bnU!Jn2ytnzZtY%3EPD}eu=-hsi$up%qWUxEqaJ;{G>lbC?GR_FtSG`NV z2iT5hxh%^=^u?!M4P##VNbs+I`PGn@Qq?p>b6IMXeVS(s7#W>|0H*g(?5r5t@Uqd- z%kYfeQq)19rb|5f&Hix4-UHp%-ndbI{WrgfbGz-b^Y(k?DeWgsuudxov#fU!wUP>V zIwu!##xMo^*6;jwnK~RPfBt{`VcFkie5b`Q2tUtfcw3w)y=RZg2k*XD&KSv8-@H@i)>q5v(?=kp zTizzi?dw-Fr}$*^pgh~9F&4+*7Ko0nxgws)z2vOUEh8d#8JSmXb$M63%vLl8xJ@>1 zyEX1$`)PT`#sqg3dB~I^t@}gea{4@S9_qldyZx-(zyBc0G99VQHMeme`T~ys=+Ra* z+iu*qOb7g3=46DYqq}HPIcAeN`#5t#*PIWLtQRoz)3N#y&dOZ_ouGXtf#;!GSPy2t zEV1>7A5%gk2nvfN;C48cz98ZQMPaO}w&NL5g&A>ahdsW-NZAQR#esX6xX$+xw;8-Y zdHAqAe)b$GV708URO4Xsd~=JtQw|3bm%kpCd0en69XxpO0O^2oKiR^g$Q%RAKV)ewTaVo#)0=@cq*V9c4{R@4R&n zmr8&2Bc^fB9x}-71m`T2(ixY8(Fnm;3)GPwi3M`DdsyE2{*ORp6eDmG3ltI7=n@ux z5|(iTD-n42{_+(HGiJR)e7s&DLO#9xejeYaPXSD3HkJR}z&>{RtnA-^Sa42J?tk>S zeC9X4RTfrp%gNE49C$Ik~3A&hy}DS^F$wtEi4kRFJAdO}w- zz|3K)sHCprwi(jcyCaCN<8WR5IE_H0YLnyV8ICsQT%UnODZsfjj2$0L_+moLH9rT} zJvizvguS)LcL+v~tcdIq*cx2$&KpnK)>Sqt}4=`z9yj7w@B`W#~D0xDeL_-CZ^d&`T!!3dT?!T{?4Tu1D6 z6iIAtPg3mtOAj}#@BGPMmOJ16VwqxXEH=4_ejX6D03x1Eu`GdB^uR%h`QF`+nB#(| zYnV4KU?36uUL;+WF^=?>EBipBQ64GJP`vEMi=)djb)Gtd@xwuHygWy&ajr6t_d$fy z@)XY;vxs_n(|227-;)P8Mx~GcsfnpY=H2~r^>YjYh=?x`wU61fKSA4_plOtd53rTq zW_|;mg7EsUVo-qSj`5|XMv}3GJK{3jUr|^?ifI&Tik`R_YgM^IbKqQnMA9m zTW#^-8Pf=)5S3j=a7wwV68>H zIC(-*e?sjyBK$TmPN-{^`9uV|!M-0{_R9c4hQbGN&cn{RBm`?3h(z=hXkrcHCtyz* zY)?52!^;Vh9C46U@H3x=eqv?g`qYZEi?hX?fVW4-8=F*$u4fv?4qcCoWA42D#4*-B^7WKi^wXy8@Fw#8pe&rR_@1EwGY>VN@v3 zrnMzo?dsifdYv~yea}e6oXocc#L)iPE}|d}&btSKojR}l=##Sc>P_@Oa31DTXw{-T zS!CYV19==#rzW;p@|h`jKiMu|`(9 zyi>mQ-OrUD{ormH6s~!kKSIizB;8e#S}L7bfWz(}s6cO`}wipPk-2PZ1c*xiKIcZoKF{)x|khG^fzA=@eQ z&W$H${Oy+a-@cCsOuI0rwY!jRAzbI~Q)OitU&d&b*HEC1Db6eMG6=n36Tm~<=Pt12 zZf+JL03m0HzqY(wcFALTWeo-4Q>F+9Xws`W=nj6y9E3bZb=a3u1`A%; zG^04q|7`iBQ8R|{VGexMN`a9h-;-e-9a^Dnj1HVP2{j-I2Y|p$FYtU$fg1!r*2@ry z{yYugBuMl`N$L(@O;a1~;&bXD5KEEF+js78S2<4_PlP416?X)W#TUq}-{L-VJbmhp zf{dq7xe6MWb*51?u~{!$^5j-)^Qe|aXUh-X-YUz>m&*;b(Ml3kJOVD5smOVP;eZ5p z#s-8QoJ1mAoLMQ~`FH*~1IARj{D?W-^b^G14HQbx*s!o&Ub%IHbyEnAN}P~RuNIy@ zc=UqPpUYs+>(>AZQkY}027;QV`v9aS@^>PIkw+1O?t%Ag^)cQ5kV z|^A!ixEcp*ZSE1=mNFC7XRj2tR)Q>(|DX^x#;^R=^fd=7X+3LLt$0mf#T8U)Ia@18SZO?4 z4aJ8`4RS;5=u01NJEh_SYnx*bSf^>|hv@rp9JgVJzJ9fAF#iJQVQpjOwX4(2p~lMh zfAT^3r~mM4WgY|mGnQ^*5d=cXd7XE?=~QFPEq9{;X`Uws`RP9`0jR zDj^Dk(4G_Y_`ws>F;{avn7lMJIDvcZ;^2Mzg?7O#?X1Yx;B0Wf132gb3aN1{sWi9L zvxdHt7W5mnw$A(;<<9PAxjK*72`AIy$ho-^fq1PI)e-qS5LnkmltnKP_cqWAd(1abiY?{VWq}2G6iYn8*a!v^P?E2mjosQnYZ;j-!YRM=#U(BXof%j*;{Z^Y?@K5tiUDqC{vCG;RJ>q2*AU=w7XS&@4;=A2u>-Z zc|&y&<)YNaG6)=>umia4F_X*L3Cu2!Bwp5%0~1XvODB6vm&(JfC*{=z1{RKlJmtW! zsi{@wh6}WpJ7`2Wqz&ervv9l>6gnE-@320+ht<{r$UDg#YV(lO=@1n^`s8_eaW-8Z zp&)z0Ne(VCDUmn|9HJ3dOkF{#=Vl5CS1Y9*oX~G#OtM2|cAt+#gTKN-XP)9fC8!P( z0fXftoOXVGqx|p}_o-vQeC75^`8n&bU;ErOd|L062t)C=3U|J#I6VmhUtU=*Km7SeATbEX^VG^UmO-fFk%k9{jyT)TAdo$jR(mLvUN8vgZfy$fHjEX; znsw`EE~wVKngOECrhF@@r-E z`kgYee6yU)Lg*7XyqjJv`^@E4Zs~$Y*+r3gb8DA%G z3XV{A^%(3HH0q~)$0&yeOR~o!=E}OgRpaSkA&1xY_{tTQ-5~g5M9pLBeDBF2b6)0Z zh|6F8hySG9`PT20pZ@&q^58K(j*%KZe`TUvTbVBpS^D|jFSb~(W$EAo%3be%gdYL^-qb6b|hT3k*keNzC?+^-6oLES_5PZ>IzlIqxQvAjvC&%x_fRm zKprf?;8Tq^h-^{3glc3Xc3~EpFs-r@;!ofW|vhtJ>1MZeRKj>(+Z0CM<{}@iqcr&3g@v-vhB_TtTh32spQa2@rtMXX=SBI zr(#Qm$|qY$8BbZl>iu<@JKBK3Ar3FEluIbIc3y0AQXvcV*dD+C!M*a6kGING#Q$kD zk=N%K>=0$uLZ7Nlr~Qv(6HFUm6O3SAo?^4DCOI3{!6>4Z9h}Htq3v`i?_A?JR#CJu zioHH_{Ax!HvY{wbVKzs751qTCJ@3!6g!A;Ey#L5k^PZO9|NZ~5tY3STHR|_S!`>>N z!&}9xuU_Mz^2xIO_6EiUo)Evl^abwD01-^-@F6G>4B;s_&j+=v^#h3CaKr@ll1N3f z*4x25D7bOGB;_aC>cDPk+Z{Nj5AI1^yaMo)DbOj0l~1g##w+E>lg%=&lZB3Ul6&x! zCo>`3*WtNr%^@H2%`}Dyf;C)G)7%IlPD6g9=Q1pic{4={=wnTj)Lo$m33y2Q!1U+S zu6*o1406l6h+8k?G!p7y`e6XFrfZNKAH3A4e(-p=+}*pLRi}Hy2xMlS&BJ$!Tc3XB^<~#M2#4hv3vbzzy5mp;l~^0CqI3+ zymII34N{@x+>sdpGXk|}v z7jtL6l=TJhP+ zD%`lifm<9g`WTUWZ}bvMAO^PHv$EBjF6#^cw1NRh#=-Aau<>qgG1A%9~++~Bx2}haGkb~$oz3|UL7#3Qpl^||| zZ=NYtO~$X=qw9+zLa@GjA4C~?piu2<#O-?vtto4B8s3P=(k4#Q6Dz%I46l~j{E0V| zNBt%N*En{N#XpBNoaVgT4z!ybA@coqH_GR}zF3y8q0ZmIqaSl3t&V2kNbYH!L=1ic zR}%4iEW^w*kKE(PkU8eP<7^9aeHZ2vEwLl}wM!%Aw^)<>@q16p!$13$k$) zC3a=k7)4yWT)zJIzf>Onug@TIcKgH!GMrluOSyAakfIpH^+2t9QXnS`!abpL#8XUI z75#{y6_!N#@T2?GlR<{VveeLfOl2Q}mMgkZVG#w_%^T}cQmK%7cz+u$KEeSaeGh-i zlh|Y5yfTkL%L1ni;D?%}ttFfmjB@sp?YPC@=kXzA9DJ~Bl(ynh@Iox}75wP7CmQDh zI`O?CkDD5hAnv>#Vmi9}L5N)3szB(wiew}Z>iN~+2|JODmyCGRP93YMZ~x=JU*7oa z?egKH{qm0D2m@CJ0u$GSx!0vu(+kU+4*{5RXSs)<5N(S?dQE&1AU-)BQKvkC;7C`?(aKf7wN$N|mEe<}bbZcE z{qa4F8@_eD%rd%bJ!jVm_F7)U^gQw~2(tVG% z+EcVywig0xVdjL57?;o%uS8P#@X>C0gr?f1B>h5Ny{6?E4)=NvsvLc=bX9 z2`_68N=&NJHBnT?YhMjnEZk2o~`~VmqxBfcU;!xld63xZ~!kmOMD- z*4g+T8p$1;yKQ5h!ro^-sJSFY@gRrD>>N|0444zf11zJ!LFVDC zUtJq3i~X7M94nwc3a@$QaTE)~0y$~tEdujNiG?vI9Bhq~0FTisOK>ozD8+29afa}D zt&0$AX&?v{F_64n0xOdMWdIuAdtN^Kh3k}cwrs)4pz74YB`FOtynDKygd+?O4V$Aa z)|cJ9-}$_fgP@tVURpot$UQEAh&x936a$sc^lsg{g3n^~2@v@qT=2mLbAGudgGirs zZPZtk35_^w;p13o^?Q3*LZKYIcNbQCo3+X5^7U`tDnCU5_Tk;99oE%;78yd2K5(7m zi1>W|-UznF>LSw5BMdU5OG}j7a~5np6Zzek4Oe~kN?`CSuS2M9zGKw&^RHhkYYgl= ztSWdI^y+1F2^zN0;bB(6Rm(0o;hh5!i~Tzf1FO3VS^oUdY}JgB57LU_Y$HkU!;K-V z5Qe$&KcGAKX6v7?#%XDJts#EM)g@@J!}pF@Thyw7K93Oig9?yO_iT)mGp%5i`Nuco zZ|se6cI{cYhDFXa8`0gxa)2^scjGyP3&Efv-9!1b%iM7i)5cMjOXkp~F3^e37bdBOi6~WgaY`CG~aOPNWE_=&5j={}! zz^ws+b3;g_EaPo(5BBWl0*~exVY%oGPIJm!yN_j4#AXP`wNe#nF0;(CdEhyO|NMwi zA0k?W(@URo3(F9l=9dtyBQu1L7(xCJ2;z2ONo!?kmQ53@#)0j6>tYF#z z59pNOok(m0^PX>Zmhb5`?>wbq1S7hSF`qrV`ypkfjSM%waRQ(|5C68bhE)_aO~pAk zqAV@1Wk2*|2Bu%Uf4^+J_Yr$xnKOf5dbJ!AAHbV>b(ytH2lwu)py`Xmq7LHF9P{-~ zdrWXwQKw>F`YkfD?-?Pw?^zTGfq{I)$;G#D#xAXd<7EXKZ3!!(9dYuthICoC*TLr& z-&6!f5b~T#MP!4R+%~5pw-Xwz&!{gak^8Fr*W7?2SWPHm~;))||xqi`X93@i`Lo8ySz>NjTPznXlNK`(cK5I}g_gRnf(6YrVg^kEqOb$0j35GH6KU^`GpGOT zoy+A<-hPyWw)XLOs9$F{M5wURu30gCOfJE|Gf6wl&_)yED9g~o=g=_3XS*O{0}_LS z!x)3lENj!tOY>!Y9j!X+va=X=>%L{I;6~xZXTSV8HUM^J zgXO)#PKb|w1-?|FT#SStclK|#F}PJbf0(Kzo4oCS#im#r4s3FEB!K^*#iKp1_vW>5;WCt(-TE- z%^InZ%O#%W>ReU>3H!PS(Yx8fxYH1j8_xH(*(xSy#t)Ng%%gBEcgbj{tmE<8`!*w) zL(mi!KUFUqh7-9cx7#kbZ}0`3?r zy@y_(Evaa;EUcyPa}YUNFu9HGF80)ayyTs_1;;E$3$yo(+g(TpRF$SAP7ur_#-O!r z8~zgCXG8fGT*~_eNXaUZ_;L6!;V9bm5B~Ts%g3xPplUB?&sd@gffDIC>sm+(mN|(B zg{pZ1f#8mqQzjyGS$0JPkNOyxunEH~YZ;AE|$_k_1%;#bn(H=WvIF-9d z)WA9)mD3*XYPb#;0wF|3mtLl93WlF$B@0|1@P${5@kXt28}|)ZiwOvk{b0*cOkTz%kiOz;bn9 zdW4HDtt6s|uR(zpXKI?=YA5HN7-t}J5TTr#tkG^`_&@G+1ZBVG59weX)%!Jwdk^0OXya>?|=2g+exb+?6pJXL{>l zK27Wpk{DC>t!YiMqj9O1;sCEhOY~+ zgU>Z~N@w2Azb;&M%6V9;SYY|XpN-J{A9P;A_|YJ6D+E` zW+_JiDUUSrr~LYDl8fL21`vqD0EE{=P}MiEM0^UY9Mec8wYua0g$x4Dolg+W0SN){ z1H5rYjh?tEcWP z6G46b>EaLIbXf#eSL$;RmbWq=6`x+#-K&CO3n-g5x;ZU@gTmAY z-&^C$Fh(Delya{7wYYX^7(G?Ly-v)F0B?PAwkkl55Q=H<>n5$R}5_U%aXGjdwjJ`8$!xOQbudZN#p-&}E z2t9UEZXBf#0OfFFgxQ|(B9IFr1B+f^G5xKd;l1!^0aZW5#kWNi651uZN!@`iheacz zA7J9BOH)l5br>*a$7)xP-B;xwFE zF}!dIiVbrCIYN*tEHEk2HU9lgLa)-!w~B$0ek4#GJ9#=4fO&|{t0e%?mxwK z4iu{<`OQ~m$`@X{Uf%jQ-!EUeig{z|&%lJm*wtHC5e1P~p$? zzK2tCuVm*NFNs!hBQFjt1m_U53|>aa|9n3G5o%2zY%)PR($k> zSIQdFy%=6URy^*06EyfSBVklHGA*%6sq6U$YxqI>WKv5sT(J6Fo1+16iN&iCOlvN~ zDuMdo+t3A|Hqs0}4b`-jVUT_(XX73)S94?dB1)GJ zFtY#J8>_gtVJT=A?-ke_%Z=T;tCX_4y&0qWeRjH7R-01T*4sbVkmDs$&WeV$M;RqL zHOZP^%C#0j_SZtf8IVv8(IX;$I+Q{+) ziXqo$&Hw89rE-gwyKH4YUT6JF_{X%Pvs!ysN_5b%pU(9m6!E2NKzoMs5;CH_^2*Pz zipJ?3a;Zof-1U)sM6RSafBq*%jV5ArDTHiR_G=ynune#Go+p1QEpd0SH?INBQ@3_C z`Z;FW5KbFnF=GqSQnc79hMAlD`S<#s5)ER${E!qDysJv9+spBMZ}8hU%TdcUG-6u7 zPr1y&EAVS8CBzX5Txb4|M+&y0DQYNoX>HVF^qz$KxTD3*+VdP6A_vlq>KTR^?mJb} zdy3e=jyyWRaxf1@tac9MatEvu#TJ%R5W<%}bESOmuOF7rvQR&c8RWtKCJ2Ww4C3u= zJYz=*{uOUY}5i>vItQ9P>}dB@wDO z!k}^aMtSn*Kf-j8J*sfuWeo4-d^SKIMO2@u09u_Gj<_xwjGzq2xi&h8ID*1xw%kN3 zd4vS;FOSs zL1>SCzflC9c`V8*O0wtWKl*on3k4nusI3R(H^2BQcHf(kge2NYmeZCmtzy~4G8lIj zeOVS#5(0}O##M|Gr)41(b%%e&2CEv3psZp@!yOyOXa6An1OtygeHjOZVZ6*SjYmIZ zuwX?ZR|UaiCA1DyJ|$-Y1_OqJ1^Kozna0asFw3UORpU4;wHU)`L_~175r_5Y($&;| z!|$h6tZSPKDbC{w72x&%g*Y|cr{_9IZ@Gvvcprk0w(s7%T#}FRZ3vY`hO11HYm9;n zGuX5qVQtgLLEQw-v}fm*AueJw-_-5s6vT?21i}DOU2}Aa0)Qfjh#F%K-gFoj5tK?tH{1{KRpp@R%! zjs)w8@288{<}yHG6w;@Twvp%VUdH&rT}SWYtdLqJ`R3u*qXONFXx|VNmBFv)bFO_uZxi+aw7|W#N5y&7ZK2P!YM@BIh0ND zxx>EG5C$@@ZaQ`2DN)#rS)YU>M|@?TR)I6hfsk@+mwZIz9K8Xu@4nb9-+E)Y{KdPQ z zb*x+3>@}v~T7fC*9WmdQ5YIH7#HH#YW|{x-Z`>}sPac)`|Kxk+H$Jz_2Vo7kbA==@{!EaJhC=C6 z20BfwM<|onmn+eErrkqtDFa_Ah!(LG7B{QfB}uyaQ!~nMS9lBMb%A(!Z%tKUG)NZ< znS_I%3ftG#FqLEy!cY5N3A4vq$R#(G%F9Fau2K`vYc}d(N5x?XN~sT zuU{&E{r->2A3eEIzWntsv+aXqjxEV613u{iOw1DTC~LPUgU}liamCgB@t~c9uJqK!G0Of z?F02L66~qY5CC9UmpggaI&=k0>1Zu0QZuoTS~;0r&6Ia5Y!p9ofy2C?hZ#2!Qd*wO zgpj1`zNG+N`Y0|2MQVY9u+vCq8h%>-dF`Slbf_#Bzxl>xT;jMcJI2z*mL3oDtmg#G z>n>epIbyMlJ$hL7S#mhV1aKVVdw0kj>tGFcDRa|o8-vS^p%|ez5MkvgG$myWv5|=e z1jktGcm}OyU4UTS{W67>({F$EdU=NVPs-2mQh4p!db!08mjw`UYWbi{Vaj)m zk%x}X6v=h6rr32z$J@N_$5c{CJ6ZzPnI~q@2(xj0lfAYdfAA5D_4mtb?6o~)NpK1U zl_&IB2Psz5NXw)#!;uzY3#EB^8NK&0A%Uv z>vvYzB6m>k-g{c^%`UR7ZndnfETUjq3`f;!%%!b3jw4UodWt#xF$0myY}+W?wl^`B z|L8uOGTue0^sHQ2JT2e4z5o%Pl^0u&Gv7U?jAKqkY+KK}r2(cevL7F3DuLc>ZwG%- zSh7uwEr+w(!Qx^FRYLbcd`!nXePEDk+>lT&+d>=^H#Jtdwl-h>v)}#gvQIz!{(t?) z?DVQzGi&Vj>nP_{^UI?hl6>+8)O@Wk`L!(hpIBs<7}1Axu^OWrpfoYhkn)V$zL_p` zQ~~%Ush#i%xDu^FM&u@)hsbI?<8(=k#MOoSXguR5sD*QH1X}0hegNYpR!iO=|=Z03HRgZem-!#Yt;dK!Cr21CC>$|Fv*(w&whC0`4O6D4OcHTm{w&9QL=r zd=+mLW91VzDE#GLJOI(A*?EJDROV^3xPqBt&OCyv#Sz4GMP)fathAgvLwFvexeu%( zkoo{a`)zzUPaJKQ8`JoUS{lJ-8XwANeOE7EhKbIHYeqv(TPwyYsyTPoP~wdJoTDmn z##oPD;Otj>fPq3zt|r=bmBV65l$gU#h{={=ov~Nul+_9^NE`q5a?Z}$d3D#_VoE=zC0LB<9-)(tt*bAxz$7goChd ztnmFJ;nq0LN+8;y%a~nBD#7`!hDn_Caw1EnX|zF7-bk0^&YifDo`Ae{KSB9KG_`@i z^h$%a=Ni-sVuN4IuZu^tl!CM^M3FZzJJ5c4xOr0EX5;#;FM>2ZJR;uz5M{w`S-E<( ztlqp)i4pU^6EwcZbhMkjYhwY)fsJ?9XI&|afr1j6h{xQ^WuX!6j9IG`S^BKo$~9tV z3|H@+?lM1HF5i6PYWWhdw@^?#XQ#@Od(X-y%Nd?NaKNF-q^#n$e7-DMfST=rt!AJ$5s+H z2Koi=?`C=Dqz2_8Z@v0|)HF$MseAxiZd0p$7`+J9&gT?-p-5)&QZ`SZEM#o%}i7U$2eid!|^ zd1#UwJ^0(gsT1$@PuPCvhsW`ZLI#+UfZX{FS(&-Fd1^nEz<5=qzT63(%SlvL(0^I&-&&J5roy=X7_FBe@}R*5wVsGw+aF!I|B(Z3LHrs!1RO z0C&vthaxHzKc)#G7%BqVol9^X&%F6OVQNBMbK8di<0_T1>l0YT4y(0T3E?q?{t2!l=f%v+&J* z!IF>pHssIet}=rhc`&>+SbY|)&kK<%`=PsPg?(YXgB#f}rk5KyIHV%^9frdfVAOmX zr1oCFwNM#RYp|LvB_J#vz!&q;hWD?Tmu(+ksgAeHDuxd%#o#Y^1}@?Nt+`yzxN&|3 zft(?>*0!)lmfg;;rB&Qw)|C091m>|DsSlskQhPS7#I5$$d9h`z8etVbkRu#UgLO>| zT~D3EAVOctv5sPyL>pNZQw4ARa&woKJXTV>V%?}p1hcH!&La+wW60pi2S+N_AUX{k z%vZSX0nXGS+*Shx_6tLl6Qk_cMXc^KPZWoAZ%US_om}zx#wMOP9>$W3uQ9`vpoQ1N z1oY&H@-kjIw?D;tOY5@J90=wbv}?$2K0ryQ(>KayTa0oxQd=I<*82dlbss|uiBl=X zmes1szB8@6r8e2;>=7XAH(sNRBjtTG+p$>>a#VaUoycOkI%a@m*Tm18H6>p76nTT!~8czzx6dCftLB@2-92IWE z*>+Ef1=wY4xOnJ$Ho;qPt$@Z5d9pSKJmVU1Ks-FhxmpLrhH_MwlL$L0Qo@(h@lN=# z2+|7HwHIk=(h8*O0D;#l`4}-*GObVbLlZ2i_A(o> z*`_wuiHv6;NFN2t2?*Rnw4HH|1`=zuet;dXz@phrUczDqNQEs$L`5D{>P&$3Cm^9- z2E%cUL@@~f06+jqL_t(&OB*F+Aq+t>@#1<#L?xsR7G%X78p8`%)F@)Du#+)}djL1g zH{u_|nT`=B-BT-4YhC1EGr`)kMCwTZdX31ArG(?u8fie$(>ghNjF^_AEqZV|@h2PH zDX0D3Bfxd!Cvd38c(B}lE~i40X8A_%4rkM7gCWXnX=GC6juSZm(@>nysa(`HV?8z_P6}kEKlwFhGY&Jo&uFj4rH2Yh4v8;oj z5$#$3<#B+c4J9%F@YU^wA=vIihn}f>npp?}A>Y9}57oHE(zMH`+MkJ@;PbB?kST&*Z~5+r|<^R^UB_@dmDqpCOjazHzfG z+`7*36Z>WBhd*a2QA2QZ>>^uf{!;AoQYVi_Y+bIoRYEttSJ%L#Gw+L+zzno;E$lGx zWLcvpT}_;>;xQ?Qqjz(hnwur9u&u_U7ya_DetN%r_IKFS4uY!z6@iYKbGc#tWCw3~ zgio-hIRzIRf#cvFKbcsUhRi2|Ao8;vQvDOU(RH zg-bRQSZ4^xeY|e9JJ#I|h#Ybzn^W>v5hVBRIWI(PcFpq$g4dswM^x(TNlUJ2T)D(1 znK|<@1{QM=zH?i(+S8b2j<6A4t#Mx>>O}t@+x48Ms}*-A5dbo1>pfD$Hr@S)7=SQ^ z(B;l7>&R|qn8kadjglp0+Pla4uhv`+S}uQ$GKf#QG~-@g%6@`&eN3n6l+~j~J@6~i z4|Sbp{;Vcfw?8y6#Hy7RT$=ID`S3BD2K3eCCWDU|K;L^n8#w@xwsyM=#nRL_pL20{ z5D>5OO{8ld+uhHJU-6_RZMazRv|*5u`7#8ihWzTUxDtpRqT5=P-I5)H)G-o~jtNFa z*zUt+zM&r@^4CP-MpKp`1nhSrw?Sw8>RqBWrUNSvKGPmpppBIe-uh)Z z+uSH8kDhX38JYwVMnp)0I(j5`)BlZ^ku7q)%%e6J56CP2lPHpTqSdgPg+;FgY#kt! zj9f%8PEC?ndCzx$xB|&|4X$?zP+>C%fI#m)FXey#%a6-{@$Y{P?#7N2khBL!>xH|3 zOs)~a#dg?*qe~i3Kc8N?!njdAP{>t9O0Ue%5W78a+0vPh+<07y=86>+jay4CM}jCV z4}X3`vV_16K}6sxqVr5eVcR(-n)E-^lBJkQ&WTyMd>t{GGjZ`= z`SHhS)Je04k-spVubxq7H!dy28PF?4&wa=-Vwc}5GZO=8tfDml` z;Ky8veU4BSM*<^)C~*R)i+!Gk5vU7uFn;+8rgecl*CO#=``jW#py7i9nGwVV5!RGZ z7*uE}n;6N<3)-r4Ji=8t zHX>anFfZZr(%w@Eg9$gSS1rR(K?n$88pjAUq;X7}G|Iw$;IG0kt!0KYus-=e|C@*9 z-~P(g^6k%FOXbyS&MvVhm4Bzj}_{ZH6R$F7}GG~GMhUSM0)fyh2MgQ9y1P&L zgtCuQPQ~vjOe|H9Ik;#5;&N2PY5R+C=iDLqw4+*P=78?*p?`NdKINSUc!5BCR%O+2 zgKWA!;lXhGC{Oi6i>KrwqWdm33@!<#B3J-bF0D0}Yqr9p9)rmBp+acC!{3KvTShR3 z!~xHzl#KjaEc4BG^N_O+zSJ}=tzU9=1BU5rGPbhN@P6?-fdZ~C)}_8B$R7$*j~E<) z%cnF*2fqg7xBeu&!l~eZ(KTQy(S6S|DZHn8l%?g8)Yim{yJKE!0>{TRLRUd+kobvf zMBezHgRgc+%762J{<3`L)@6>u^f->{A4*S&wu>T}rg$uTvJ(Y&Ge~hFXIdB-x@U3f zoTBjK99AwWS&L{AqGeW5WNAZ8ih{&FL1a|Zs%V(`E(olP7U#p3QEubbH;pzVsEEU8 zxjkswV=&GjSj#3%#q>pXB`xqbiwR+ES8J`-T5bh`bC1(>=C-aMAG6G*U9b6ACsEHi zqG=^8jUM`#44&Z$(f+}bu-v(GsocB&VGKP^h|}+5M55FV@w)x)6p{E8(Kp9`F!z;! zt(&&Y_QHs50|85{!8Y1KrI`e-3md&+$VtW-xME-dA-PsOr9UZ*ab14;ym_qF9#Lu+6L zf`BHJf{82snJ^GCSQzG;ml4k6Imm?b`bCWz2@{uSzNNi{SSw-WYXm?0OiT0Eu3EI3 zQ4Once)ZWx0;x+_8`X?!eBmYsek;rb7*;=vltY_WBdjQ-n8Rs1s|Sly#Z1I#r5TrVG9p7bYK{%pr;{8<(K+HU zOCugfDMysMy2O)Cqik-0b5plvcNs-47ni9F#jY1?5HD9fMWJ;@UYaxtJ7>#^@QCuG z7(`UAOW8<20+AfTjom~cmwTQ0?O)8kO&rQcQ}!6K+?cNovc@J8%vU{YcZ_8(vdg+M zpC}i-pTP(?)C~<9Wmh2ukrHtnzfh{CC!BhKn2>(j&a}FvaYs}Cl(Ua8g8A{gPYR9= zskfqG#e;OOeJT^NtNA&n;nwl#ii#{gj2D|9ucJq#oIthJ6n4C9DV zV}P&iq5)|$($Hi)8OR(y*Txyxsk1=AVP3CSIJGn0Z$c02`+oX27kzDSGR$9&qvARAi(JGP?iysIt z?{G?57Lp~1XGG(1+C=AabL`RejGj$g&gN8kl3C*;)@MhhQeZjFw;tw~lk9*y&VZsA zXL~q*b(!oK;+?_wm1rt~sgUZAs6U~Cv~6|yLfQs+{{7>?Z#sWzA~YGbrY8Mi(AwY^(N-|Ho!>31pGLxUK}`)mrwgBaRjIu-6vHaV@1 zjx||4?%U1OJEP{e}1GBPg6G{T zD%N8#+20Enoy9azQ$e@i>9B1QSE&vXhn$8tI?AD8m=(G!$vRjU^N8{ZIBK7(k_Te3 z4b2s)!YfGR_+tB69uU|82o-ol^+To$^RqZ*-KoYAwgGdBXuU_+xS;n(iE~63IsT&! z>WTqCS?01mLyW?voook>z3juC`=pf~561>0y12!-g>!lmC!j8rR8>np!4!!BKIftJajfx3nX!1E|6$pDfMHeVNM5MF^* zP$_neq9~CPcb!0Is%;dx&X|k#S^J9?lg9R(SjABHb7~Ev_%&3tj^noM$q$eth>v%% zu(@-2s@z)DTF9$5jL(P60e8`+zhK`dXu{m9x{Hx$MTg4a;EZxH{=&f!KRr=EPGI;U zOCK)bG%lJoNjYh_ZRLz{DFd+{p>%SSforyM+!NsfXA*a%daTg1p>s&Nt;!t<#fo@y znfq1@(e9xAaU;K9NNR7@85rnA;5MW zSd<|6Gam<>5UYJedP`Ed2;adtxv?5*xw?I!q~YLXfz8wHfcAC<5c)M`=DTMsogflZ z*z(~gFUo)S|2!yuA-*UH4}ub0#97v+q$&SeGy^`4t|q1P)C7vCIAa#7wX zwBVmxJSxBTTEG0-*9u3J;@$@1k%OInd5r5G_&-;L=aX%94$+YFtL2lB#2fnNdm>O* z>NkyX&#T}1YT3nI>@jAV4*r%)1*QX*aWkLwA^)qGUds|dDGNlq{Ch*$oIifrQ%zUD zRJgTjL*eaF5hl+xSeh{}?P~=wJX#I1Yw=QWDyx4bv?4u3LeEM$193yRe3x^?L=aJH z3?dm}S!4uc@pAlb4K1i#T|pek4%jyGQ#|Y6Ugi8*?GGS2mq3oOj@rdN%>Mt&*n2fw zmSp#RdA%uf%e%JIJsx8)07(I$1b`43dQb!;0(ubT+g)c>Z5Qx5%U;yEv`ZukJe^(ad%b7Zqg%aQn;pPZ3Xk{qtg|cWvj-0`C0vKT2qk#Z-Xx#&7%~|Cal*9wQ)>C2 z@)2OZmQhYLPp2~u1tZ{=pX9>r9fRQx%UAENwNE}TZzpdA7+TEa+t{5&D~YWQyi4GE+KG5ph}a5m42ZXhI1~P9@r8-YuRlT52Z9N6 zoOoUkOB-6lEU?Jwsq}}~rrK+GX7K?gckXC*$YgN@R0S2PxbhWZ6G)hGTyy=1yIjBf zf4tlN$3ME!{`6}rTyN<*COscVvFu_c^~3j$+h@$D{--w;l8aY#A1?P;} zG$3sL0U~)GBG4*?)Dp|=(Havdd1P4UunKG);%1{kLIlD?!O!uR5d?TTEcSxyIhLs4 zTKj(Dv2Blr>KW(kF0&C~g-gCn=WT$}-(kA=IcMHu?U~F;LND#`9!5E0NbmN=QgIbC5erI$Lyc|MI;xj=W*03d(61QlY{D5_jvK&_h|3;W2?* zM)NDC443(-Ph9w6>|xqDB3vTxYp;E=E!_EAG;%bMch))E>LJq)j6#w)O${JMkSqw_ z4CynL2=veW6hBjsBWD`^d@!myq?Z-UNby8fnup~{Dam}2cY};+1ArIk!Q@7yZ1E~! zL;yc^!P!7STAEPIfT|F~O&ch_sQH7Tr(~KSGDaq;l936jm96{?8M5LT0W+R|Fxd>t z8t2$b=XH^nI6!QH1GRTHSWhL7Q-t^)mQ6=PUJ8ehhFk}zRxCb%11SG=ZnS2v>&|rh zpa0co?QcGK-u}}!IlX~t@+R}j51&o9KY#bAUA}hO{s+ql6M{_2X>XEsU4+bK6i=LB zR2ha;31x3n2{U*1F@*VZ|KgEg+_SRSpRHNSn0v!EH_xEI$gY!{&)O=Y7*2(YwJjDVqV^68fX z>G;YzkturvXK!to<+tIrA)ZCrRkqDhpR&p#bMnu&X4}8M&vMr(=Ui!lBEOka9`)`> z%6uV!A^a;BhWAS$ub>3@S_qO#MEwt9V8Jw~>d_I#E_3;u6>TpoZ%zWKEbxJL7=TEL zIAQT84+l1$#m{4`Dk6B|6&A{m;r^MJ{{ZP+X?)kTYk6ykV@zLlB!SiV6W>}^;M4E z`~sST%P3~N^mvOQ;K%nLw>5g(=L;O-Z0-9uSsr2FV^-!}EnG54F*VIQo;uh5^25XS z{SQ~U#F?HJmF))i!k$cB;D$UlVs9R_JKX3duU3y)!@LYPsbo;e&{ENKZfZH~Ma|Dm z@$!*^5=|it;}Vd9?MUGOVR8r5MXZ~a*bAEZA{A@-?1kJXtH8R)mF}4w=+XG+ywP>r z>zunZMI*M&ZF~1QknIttO?Z#bC9dRfXidg64B$sEy3ZP{FlP>-yMBwiZaJFg7+hn%In5d_7l=57a~Uj51+BBuK6EJ}J0@`-uuD+#g3|^S zJvJf?NJz(qd!{^|uVv5gklqo6hX6d^@X$LAF-T`whdS$AA_sOcmA3vRQnK!`^y=>w z+xia=4v!Pog;%k!fAqlzjMUhN%;NL<-Mj7SQ)p$wGVpiOnRamIJ~icPg5V6TeCpju zJ>PLykK5yn4sZ+1${}Zh@trT=PTBE6cQxjEj-ifL>UFc#r8OjsB>kA zLtL-1_y}#t>tDo(p%PS&ALOg>uDVu|rzV zj*3x-v4|@J`?r90RRx=Cv~#Qldt9qRGa4F_m&|{`TM>wNj-F!|A#hupmQ!FuTb)#hmQ~K69*XYfnv97KA^w;lgl) zQDt_BY-3|-ZKSOH8J-k1d|)r(vXyJK2P|oMjF0a+8!me~%)KzASY>I5v}B`jPu7*A zo9^_V3@iUI8*rqw=_|gqFTULgcb7cgeU6UcZ=SWcKmBpLNFCh&?4bSZ>1^9z_-oS6 zHRCR>Ly<3qH^7u=`8Ee}=&rwy%Ml;qR>;`jejf^s^3@!3ej+?I8T90$8kR&KbG-`W5VV(V*Uco+X$>pZrPmB7#tgz`T)9DAvQN z^%D-gvUl{#jjZ*tF6+*Zy3q}@JHC-@hUCoAP*_++f)S_W0Ib4J_yxY_Qx)0~tFI(A zh5za154bD#mmfZFfBI#5Gyr!&G@cDyDVJ=W5nPA6w2lsmHSvp5CJopCmGY=+H9x^g{z#Tis zj$B4;O73A=8Jyz!m!G1iU*!_eKPs99i8~u+dXGlP+P%+K!@zxL&|AOc zHKZQ_5rhQ`Vpfs^K?K3M-Nog!;E%Z2=-nsJ8`q<^H#pJX`(5@Js%@}8aoXh`wV1fx zbN5CjSph%(z&9%iWOkAySr5Oa7bk~E`n$3H-@pH;{r5X}+Ibi)Mgia^gNm!Yw89z} zNpgr>1{s#Yctci1p-kZDTIjWFS2-s1a?;lEFn}Zd3~Qe=tR;KnfkNy2bis?f0CGb9 zE~8pE+1pFL3ay3$^Yok!W9JZ1g?@^3TL|v6$E(1@8p+*7Gzy2VsUma^ulMLFcd<>j z6HP_J zde5;`rBK#b!*%jO_eofdNMzla7;LPw%6TFaCu~Qv0r7XB=W$KL2dAI!?x&%&WdCoq5;+ zB`@g{ei^XS6IfN&eBhB3d4?Ji21o7IOF_aXh$k#Se5<$uAL8;B`_F+KNkfq0u!$n$ z7ytfO?df};I`+W_%!bT_HFC*kfGM(p?ROO_<qO9NNax21=^_qg2Z z<44==o42}VkyyZyX$xMUBV_beEYH|>xF+g+&UvXM*6)}FeV2kRU%A|_-MA9;%DxnM z=0$JnQKa@j?)-6RJj!^vAvcy+}zG7`qyr9>mI{dvrI2aUNgXRi0c_ni_4B8imqOK zee4b?8xTBgfV6K8eaW-;q~wx@M$<({CpH3(EQI24_z9}xhifz-oakXgXnMo);J4SQ zVV`%FOA=P>dVeuig^hjrCXE~#VI&|p2S&k52_qlH7p4uL=5vHud<`okd@-!2>F?ub z52*~vpFCBJ(dPl8LaQv|VUE;|Q27y^QbdVp;#Oer)C2E~1d_r`(;f$eEuP*Xt3fz- zux!5jxUF+Z@eCDW4A&eB^bjdA<5Q>%i0UH&#kmW@ENilt-ng0Jk|*HrK6=8TXwOlW zV1jn(8Ydnuwhy?x_lPAQwURmu>z^?C6Q~9xJQ;AWt5tZzkXDYC$#c@9!(tUwEK!)3^Ff`xuFZCinW1bt@89i(a2`D6kR8D~qV*6T zX%hTHnkvgY4V1^vAs}=8S&)Pl5QcFb}2sYInj#?{q+xDg<(|3k_vy*4C#i9;}<;6W*8X3ViFZ$1h!z3bCtogm1R&jNf*Wh zr1!Aig1NEB>G4Y<>LalEW^9lsVr|SJLOd(30P^V%;dvB7`bQ~3NFVQej;{)gAe}-~ z5~57F4$EhE1vA7GuHI`KZR^AL+dP*_eTe<**4uC$Li5bga>SKIyH+~v$8 z3@eD

={;$SNp35G_5{ z?Ty^@Zc_7}VLm#~J+LPzvt(oi+MY3rCwj6N{u4LrvLq6(2r2>_O)JVEH`UirN*D+V zFkB{bomAY+Q^8dT+1G9qvNbhcQ(|NoIalzImKrzUZ3rHq#LI0Xrj51Fq&*z|oEa++I9+fZrVD*OJSj zzW8Y!=8#)u)ih~L8OHew`oJmZwm$?F=fSz@eFedQ}TE>nbnQGw60McKS20RE&`~hEM8=>Rr zS80!ZtCV?5?V6HIlBiMw&30|lp+rc{`K!xZz{jg?33*$4O9_NScKN9&(P<4isUKg1)CX)KK37LHH>Hrg1O zE9H^l#K0{130@>sc%z;tnmxp?9vpD8BU{#3Udei+jDX9WHB12vTSL56=%~Cor90op z##ZA2mv%he;S!1=uY+`MY^DwqJ&?_%6PHCil@M-*xxorxC&QA9imiR#GY_Pk8uq6m z#S?3>DKx}8+bFo0D&*iag_lFY_gSYp1~y0$(Lb$jd{9yw8(`U^JMZ*Hd$q^v31q+; zIh2xV5BhJhtfYd=M#3J}dUHf!&1F;poy;PFOk?F;y7t;LDD=D=N$Qd%iKq|fw!m<% zD~_K&MGxRVfaQ;9I^WBUUic&vojAV48!$NVvr0x$jSB{*$v>ggd&M3iCULkx?*V?# z#5nz3C_PQ0j(#RmfUS#8X=K{;je1 z&fWXm2lJ#&vEOtWs~{0|;GPz~BgA*d$?!vt9_@wp+b|i|Vd{lfu0fRjw(;Nzb7N1V zNQ5BFTbS_v>QQ_D(Qf_ zpX8%RU$|EME|j@OgzHQ;m@A8K;#ekJdPHE911O=^X)@5$u=4inbIu?mm?F3g^>2L$ zyFL?|nE4sNS6Rd%Z5Q-ccnf0Iv|#{8QFcQ5dQIxW^=obJ5;rY!``|tY@0HL702vN| z%|zkKvmT>z^nztr9O4<7|HkXzWM`2&g2IW9@Dl=J6tJ?G$du5~GG11K!!3t_`h%YW zqT8eO#@c9_;aXbmjw3-;)Bz^`zQtK_Aw)0>Q4q&AclD_K{r~3MoaKA2{l!23Uc38q zmZ^jiV)VoqA<%IA=)Gqsqi%tVM1{KGWKS85GK?;hI=pphpeo8M+4+*N2l)OoF;R0rmoEYdFb8m-bshBy^Q!|R$ zLZA-m4Ly=)mxjS*Cw`o6$N0nRJfnmYVe+y~5${ECP?IgaYW$mR!zCe~^aP4ZFI}L4fRm=-S(F!mITQx&9t#pzIrJQdI1KEzF>3wt1*xEc3# z?%DZMwoAPVVHP;9<$t^X{kHjhT)|1g&lCm$Kl*5+{lmX?tzATGZNFZF8JH9T>!VX} z?J3+`f5PyXU1%s=VA34A22&1Yvyv6kD3lOF*Isi91kA0_y6!2?DtJ^dIj^<#^>#TG zKgaZV_K4{%=_N^xA6OA*9xXI0Vi^{@4R8-VzyU&+lNW;qy!az;el-j z(A&HmVtWeYDZ}pVgX#8g{kYxZfHk-4U06JAx317Ipa|5M!=6Enwili$j4G4VG&=-# zKFh!ZnbFvv`l8&93x4 zgp&{j4hxEC@7Yq2j=;;F69^G3I0+NYV%{MGpcNph?PUNy?-!gpVx$8=$Z2xnh+m!( z06|Hv6hBFbImD|uA|G&v1Qt8z5e`J{?XO*0Y_~4Vx9b?)|NfV6x4(RCp406$(@WI` zs`)BBo}K)wdzHgna(N)co<6`&$Yjc*R|CXT&Y zHWr}q-Q+5a96E{ zTxZT{6~sYaL6BZjtu1w{w%>WP>%1zscAvPl5F{y<%S38(AFc1VyH9u8>ehCabS^S` z*dof45E@fQ_W4db$K{`gnLFHF;Oa(&RjsiC%l|yx?W8rPJF{q)!d_3dPTRYfUruq! znDsJ9(@;-5d=tP6&ie1(R%;hP{d||T_c11E?9b(+onGY9HNsBZJdrr^f`5~41aagU zSc&G3;V-7a2l_eU`Puu=xueKl*24fLwv7Xv@wo!%NAQHR9uOFk&@i*)_S)CqZdYgs zu3@q}HGiISvL3d_?|nqYaA8~lHa?k5;3R4ao#AH!h*Zpv?^9;FJ55fRL)z<{*yFLt>$W_{78>w- zZN~|Mes^QP{rWyyXyRaY%|;3oRSdhAIej8DAdbT=@;}Bfknw!;D{j4cx{@O>L|@ zYufVgy!i^#DFkYRZGGTaZuHVUM$gW7diY+fWc)Et^vL?|acli=K4dzA;91v00%8xi z4lt@SAiY}JPexWVtFUV6Bn47AbTt_g^UQfrDBL{6O<~>HXE^$Fv;E}mPP@A; z6Q8v62+b`nANtyjbM5Nl47c1N2pq767=m*tgJe+oC3pMar&ii|uQtYq_B!Ax(|DAO z!pY#6&YAftqk>~6qTJ21$tH|FEQH>FOz+Ro`5Hocj>hIZz30j$%p2L5?{PQ;kvBav zf(SUY`q)Y%f&Rm8KGo<5CMkBAj2mv&Z_uJy-#xGb%GQs7>Tx`hr(*?IrP<%wq^oe#4L9 z70=}Lem1=FF;e9@QHDv_Aet#0n_spV3jg}Af7EWgcB9>=D63D`+8RCW6g;Aig!WCx z?-=Lq!&6UwgOzW@tN(dFTcC<-tVt*QmxuINMcn>1ewq?DS+IC?X!We%A?^^p3v&0( z)9Cz3vJxA4h+FlZ9JKpS589V*W32>p@20m)P?uE59y!cZtSncw~2zkc4ne&-Iu zMYoT`pmuw_#l($>fT{DK@Hfo)sRBNQB1gC(Cy}&Da_w^uMUuzMIGo!;3ADPp&01({ zH_xHCqFMcn(-fY=^jj!6b~xhGVbdRc6BEc21dR0FP3utC*5iQQS|f>h%oXP}r(_NL z82f3j{j?Q#T5t#gduUeo=;`*4?Ws|qxFZPvM~~UF3Y<&ypo`?Qgfi=#8}gY#EYjV3 z{OLSrJ6Wb`^EIsC)h~KbhsWAatDLjfcMGE@>#NPShc&%cVJ?Y1p!4_?3%VI^sGq23 zh1FV`boufp^;luOp5tkUBMoq4So&EX;IduA@df9PR6f% z1!moL*VA-;ia~&ks}c@^jHgo`8}kLWRyvGdd$xv>^m5zddePla?&ko2@D2Q^x+{PL zHU>C+3<&hh9d0CkFZ9_IBzn=o(f}=5cId|GC6gewP9RV4=Ls@d%okzz?+&Yfli!dP zBrH!KothL{WsNx5D?NO^)&7bLn_hkWc6++E-5zk)4db~edom{{uE{(0Ds6u70jD1P z&T0GN72y(Qhr&~(P`0cgptU@sF#)|9OzN^n6ihI)LtK|`_BB+XM>=GejmG@jjElF^ znoe;E_09GgilQB^#PGJb_qmesLzF9P2>3SNUt$U7wHxQ#$L~L6K8{)B&6V6n=h?;@ z<2!Wrvf@Le{($XwTrQBgvL=hiNZAQO>M<^B&$vNQ!xM++>%0Tsok4T#?OkAWu!Py- z#p~V`gt3VA4{IG`9+?B^D1%BXK+?NHGs^D}*mYNL?o(l=ZL+6fNT`f%h6Yfr_Al>p zoh7!v%pgWiI(L36s>>VtT4&ZocjXr!>mU_WewA*BHwbgm5>LHKK`FlL(E9Ui-W{&F z7u4d{p_LlAqVt9=_)_dcl-=1-}+S!O5hK%K{(BbKVXdAF zil~$~U?9v0Uia0b+*zUO;T@$OtH<>@AjYE?OdpZ&-xXU}6;j{*SHh0dcHgtljz{^9 z>$NJ|!k%d~%*MPxz+Uu(&ENm-dq3uqIu!^9?a8A@@TH14Vgan+fkzOg5rPTU&;8m@ zr|i3PX?fweUAe*eRcL8<*lPFo!9sgNz8}&HKVWa{6L#=S?a#M+_cypKdyhHx8N!Ql|#;1T4d|tGAB3&YD3Tij$hq@nR1c^Ws#w*R|PW`Pc&TokI=?c5g~RC zTWgV+yJDsFo2RTfPhnURVoSucby?o22q1T7-T7bMFf5;WcT$sx)^SgeCvhjho2;BK zh7dh@_ki*n0~x=`6XUNWec@*uF7N3&RP)mCL=C!AqiO(pUW6KvsoJR)aWvS^-~XT; zdzKXR4i7_H8B(jUMD8}mV-RLnt_!P~RN=b6M3Q_ja|5J2j8TL$W&~*>Rp`ZcJ;xXn zflTr891QuKBWlIAQU_Rg$Wl6e+;TSs1Ld%6?}&Y{+5XKx`ymV2Xj&h`NJbr14j(}* zDFa;1?{iNKJaj7;!@_hGhV#d;*MFMmY3%QDFWNXe*IPCW!e@8=@QBKv-k4&;6ZiJeTOYss8QNY9EP%Pj zl^#>vl;8zjNA$A$Xv24yhfgsVou6m9jQlh=MX$yASRVPp{Fh#IiHob}SzGnQ#ASNc zrEBc4Vu)@pwGA`bzZ{|Xa)ZBj@7kO0u*EIL7?dlgQn-tV@B`q>Xf|op%Q@w+B49ci zhmW4^w?`Zvudz&#&6uBeRz**jC)+cf}n({9K+hOs{ zDu6J@!Z>6YumY~IqDf^YGJU908S6157#c_rPLQL(?GDqFwc3%Gb}Yt(?A>o zW0$$H_w&g7!I%8K>sE!|BFeQR8ZnhT9x~??>Im9pS|l?$!K z8DGrrV{1&j?mFMp_fQ#7-QlBrBRm_O1vWM;P`_KxwnFQnb}Jd+x|X*0#_AhJ;s%Vf z`Sjd-t^(kFV1^NF(Fv;lWEku=7a=-@8)X*GG|+~gj?+%=1Hh0kn6#KKhpi-4BH!gu_EQTR(#{~IpA?w^Rm zcO@pI;2LC3x*LMa>Brn!1+jijEdV-XA-UWzQ)_4JVw_JIfGy4q=SJJ-9oMU1pB!+{cTy ztY}Rsoo^zdnTroQT|hZCi=aB<;PgBiF?%zVNISsRqDZ_c zQ&ppsDrM6&y|47tVu}LdP5shWY@|6Oa__yayBkjiJ0AD_ipv$@2m$>+DnUfly25cGt(kGSW%^MHstY6v4Ni^mt zzxzjD;X>by_Q8LA039I&-XYNMeu%}R zDl^0+L-W)bHFcS<`WfXPq{hHfP*QXls72cmMggwP6{DrC3J^DXuiag3|LhVz{_Xaj#<)^qe)coO6Vrd1kO@|JqB&2@?U5mz5Ql zJhU{jw^v}T1cmn;LVup2st2h#TviBFCb=Kio?Gpr`)xS^4NYRrm8X~w9x;+|=)H*Y z!reHB2izG;uX~K39daa;OF9xg^g+;F{_rRrFMnP@ST%V(e}QG1K{4e~tlRESX794S z_7;X8+glun4s7v>B^SMgG|Uc@4xF z@`VqHJOJ%}^<4l`dfQ9AU zOQU@uAJ8+3i#?GIK+a|Iu337%gA*_6;^w77I9-P1B1dMr>xng)n=_z zgZ~)>+~MGyb5oRDt`+Yg*qHXF*K;F6H4pf~VcGTv<4?KwmY(&98wxTKfcd>wXdjrz z;IZq_oA2ByY~7hwictP+0+2Rokg6PW;NOk&BZQO<36Ap0lGsWWy( z-|;tL{4XBh6`}!-dYx{+_&4uy(KfB7i^L>LN-HKbA4AY9L+f}=<~5E}K?C3TVYC2D z$dLa_(n^Rkz0ix_mHu^4pRS`Be?Wya9%Mj`R+6ay?fIB4CA?VPj}o8_wxT$zz{O zppTe8dh((dJiCmdzx0XF$sqKEHPjS$HYq45gnQ9dt;NE82=Ns4G0VIUI|i1OP*9oF z`4j-Xkq$jD3MB@o?5sIpbAxunj&cvEdpFAO(c^nEqe?puOJ88=Cuk3q;E>3U}b1$B#8`T8zXgV;BpnE551*IE4?@4Uzt& zOs~KB0a||I#+W5z7+i;Fya@c*W5i3NxRwj}$=~Nh+%a5BJmCg`)i=p4aCZS! z@ZvqZ_Tb_pJSk8urB>RWm(dF-{`J4w2LhNiRhTXK0mp^zvjh;PAh1}}U)(^DQ^nlM zPi6*4L@M7pAVrWLGD*5oK}4g7XUr%R@QONnh*R+m)fvbi05d1z)x*CMB77r28MH53)t*=~yvDH36C>$;;gu4i#*K}BhG$i0O%k<#@ zTj2wC56v;3Jz zh9lvjA!YJP&ENuV=P%qu2}i>dN{AB)@}oFb1(bk|-+PQap`&fQH0{I5^iHW9se9Jx zo!9|4g!8UH#mLhb^6+nT-ueQ~%eCnp=Y%SI_z7*?C#yw~_NG4VMeL2`e&ah#&GtMC zg>Yr52YSRbkf+=F6k4s1T=Vrx62(J7Es^vIIACV!Nq@?;&(W+7q4~^Gjq_kFm$HGK zvJnMm=+KT*hV1FZ%cmOwY+1Cko(DGlD)4JBZmtp~-2wb!Lvx;yoR@}Krho-*JqJEW zF62gebz^}Ue`Sz9jEuob;bCV=RUgUQp>R1u#lLcv@hFgr2x(Pl?9_ zQO~04SC8}gkd7E|gkc2WCEtnPQ-)RcES{R6Kuo$KVEHH%TTl1eyZaxqF`nHhG9^I; z&liSmq`!EZ!_IGBX%9cX%Xv*TY^{o`n8I|#arMSR`v-saO1pabdVBPAxBcShPun{` z{W#|a%kVlz}i!hz#;V!b7FS@Sum?Y#&Yzb)lqKU(VOvW*&q^z*9DRF%(6N7Nm#0m*d;5002M$Nklt^~D6kba#XT{%S#up>v^<^kNaWZkez3jIHG#Qha*n@_+ z;tKJsD_-d^4gWmwic<~CDF>J*)-%cgB88=|XVZmuse8zgGJ7@5*qx1Qo|PQPXHYQS z*rVj(#WIO-f@Ni~xB565@f-106&hsZ|t$`28!sP)Xp%YF*Wafp?J-vAE(0gibG zZ@&vM=c31Gc16^-@r0`>ihLEP2$)1kGv!8Scb&k%fjWTdD7Ys1(NErQtDNMoOxbHD z-ta3Vb8xBU$|cTjW?A8_OH1urZ@s~my^Z!y+jjfG4;~H7Rro$$$IS0f52yo{3{OJ? zVh%|{)5Na$e$UcafBW}e#V-7?y^8|3;0}@UFyFy-)Wjt~dsu(=cz*lWaV>xI(}#6h z+xOJvfH)O6f?*U3ULKKj873+CwWbZ@|CoRY{M=8{;@h9&2!&V;55~ToQpu~LBp+Kn z@vFu)p+x2Bq?)xv**|Vb)05g$?LFVc)|iW;jUdrRfqavUMl-GEO-u*RBecuY+=`Cr z;Z-b?{^)nlw@*I)tg+c5#g?(3F>Kyo20K=h6|Jf$j>fjRE}ta>!B&cq)kg*{g>30CG528|Vk21y<&l zx71_h(z0iYD{-?#Ka#U25Gh$^hc;qoI zbomx@7_S4Kj4eLpAL2}a9H$t+Rn##YVMl8`_DTUQYlt)gYM#Q8K$X8tHp|(=m$|9$ z;ss9sr?c_yovYl9bL*Am_Ss`Mip!^58M06B&6OcvVySHZfcwgc>j@AiCum(|W*FV| zSeZJs5+52m^U$EfURc7LwL{McBtunuLw_8crW|68fJL-O%i2-s6Z63cDhebFJC3pE z)f&lpv^KuIP)S01t1SvHg3|N)P5h8aBKpUdZLv!_T5T}UK#EyBH3##pz(vT#pp+3kpL`eRAeBM>iP)Va;IaOD` z1!NRByhyj9jH956A84bbepT6vCe$0Q_|!=LRsJ29@Dp@>=O>?{d}PghP?Vak7bW0_ zkAjs@e@0m6WB8e5&2f&CF<`4n;3gPkI4@l`Ekp-)N^3c{;tmq5Wcbsn$ZI;DKW8r) z!fU}QzJdV2o&g~#z(oEjbUY=7e-*=Y!qC-YfJ4tD^CsE|uIY3Yzg~wp`IrbJs;3r? zqM9_x>6v`tzb6np!whhiyKIi<7)k@mjQ~q4x7(M#eB6Hh&b#cy;gC3c8U8-xyxtRz zR#kE2aC64EXp*&*l94rzaQ|w}*~lPh`U8EGYs#uIzDKBP!6f}1T84B)cOBOe09mCc zWj2|fIOjYAKl54z#dNN(Iuu2yEQdh+m-C~{k%l!aY&!BtPe?+0dTDOM;jIl9R=8@D zrK?4hM$3#cra3le`P_xrMZ0X{us#214YIuu7=XkMnf<^Hc;JI6jBPv@$}-tIFQ4Pg zD74hBI0Fr2IB3Ae3A|I4lEgnBly&Unh4Ghfex8YvuK~adft`&;OArKw0Bg>9FM!fBj&-pfAPC*{r>&-)9=0C{_sz}-2V30zuNwn|Mf4phVw7%_oksthdI>h|wy{GMOe)FXL{x`1EL#(vz>7BNwT`hv;g*q~~e=3XK z{cO8!?q6y1(5L>{gVu)dDS8=rRMKIr;vsZaq(gA6fmG&H@0a;C0^rlVp9<3~XF+8j zD14*I;wi(_IJiDB*E_M^{PzR3!y>dOV}dA z@+EpuZ^>J^a*?fg=eg#xth0`>iS=5NB{nWNU-dLbb<(a6+jt#$|-bqQ|RU)G8Vnz_$*acKo@JC<8q;4RH1zjV>{XaDfK*Jb?pF@F6jRO1wZGXbv) zi4|b1N@vmWu* zbYGu?kS0#`xJFBi9u1aUd$iI1#lQS%`;^@>Z@uvfrh?n-HC=7LdFL~hN7%2bQbxf= zz*pS^%M#f9yhAGlgl14-b9gxeu38u^Vt}FadNM^7xdh2weH!(rEZE`Nt2#$IRr5+j z=e2R+Y;Xs@npa@>oRme3H!sT}>c=0nN8G3Ltv~n{ zYlH8$Cm%k5{sNK71VB0w7$H1{s5e%$2_F+f-?L6Ib{7Xv(GUY#xE-7}xgBNwd!>{w zA!(-1!aLg_5G4ShxKD)Q6ou02y}kC=|Ki8(N9TSKqy2g2Xd92nKXW$X^51P`&$jv7 zIc-x?&lX*6iy}I+lV@~LEH;yuEy z!|Bd(_?zpsa~Nr`m`jx$u!{*D!3~a6`3%h-&OEIL>sq>mOZ>=pDk$-WW-RY#eo~1; zd>#Vp^LEQ><%Yz<#6_HWt-9~<>sfw@HvIJP?pHAUDg`QaP93MS8=_X8J^t{N7nAfw zcojbG#R&&b^%-#;=76uwFRuGeyau>GkPiTboVTjYw1>a^r2VUhY{F(pG($!0tZi|j z)YW#0;~t-U^w~fn;hJ~l)VD?jtp3a`OP(NRUMP47S~02s0Wk`X-vGcQC7=~YA@)0< zhT>F6hw&26GZCDl(6g}JYvvNCEwah{h@(`GxWi{x0-Em#guN->cy41@@F~aDeEB-_ zSbI4I7Jr3O`SR&)@rWODN-v)}!2o}kr5ThuL{N9(jqF;0&LSu-8_h8%wk)QN7MaY1 zHAT-hO~arv$a$k{pY!vcm5ktu9wa=+sgm~irgbQK$niJ|tx6^7=Qg|r?40ef=dj#b zTC%rg?Unqq{tA4BRd~XQmYOdc39YkExZK;G-HrRZY}{Ytv_@|p5T7X6sDN71vB9DD zriXQH*15g{x64+>|MH*yn|8SFa!L_>OlBFr z2-6FpfWfD6f;M-Z@QFI%>Z5S#0rgbCCS8QMdys*7jB%)dj>qs-j|t-1>ub<)%t2cA zn&M)bp04LCy7A*h;r-2LyX`;!I=9;)2rlEmd0{$fp_!pV;~JGqBp%@HQj3D?oOS{D zDur@X4MSzEq!3%S%bkZVuFZlbxOj4+jP3?}EsLmx2$jR}S&STdnbhp6h?17mG$I~l znvDktfqBoN8J|Ib6;uWL6w^r0ri#$8&}+{)x;#sukl9|)-AlVPP;N)mfMXYP&CMN- z$l<6}zg;4dKG9A)dP0fg5Pq3nRyxKEl5(Cf{CA5>*9z%<)F1`=9R*- zm&m*bCQX@*h!b_Ox5D+><>*B<%L<}aOBD{IbKJ}l0%p(V@HD$)$ZwkYqzywD0>+cp z8uI5bHHM$2w`WXwK+JQQ7a98cZNssMpk@e+@kaO~1OyTP6ITW{FRiphR4uWZY?VtM zEwfFJzU{U@aP=gM=WEzna|BKAJ#2)ej|w#bEVGU9(h7O9{ZBmgWf;+Z{dkWpuq;pI zoMe}j=Gv1d=n7tcvwiKq{d;Zs%7ymq-H%j5%M^nv$_GZM*!`vFK-bD49x-r{R?v3w z)$8rTYqziv)6fGtQ@^z&NFi~Pko}r;RreKd(mzGi<4jP;j;F7N@l3t)Q?M$Id6sMv ziOQI{e3`N$+|YOgo4SZ83)LAx zFgj_1uE_J9?0D+(?K2VLHU1d_3q3!|C8|GTua4FTUixCz9e&#@!8q<-+TvD+9hQ64 z9y=W0@vt}=9Ic;pp`kd*U^tZh+$Zd?oMUWIbh#Y0=-Msw;W=pIhJ#s#+TMI{nR|#| zxw+FGAFj2>@BXIkesquR+2{hMP}NHnVAWAvLxm-W@sBi12w9uE_2)?+vz&Kzjtj}Z z`g>o2HVf?+Kl}w=jK(k$VV8%lcDK} z4zI%#%;k^`hl(i+X&jO+U^NvpKZNfbTME67(jJSwHKbi?o7{-5CDjs&A*I_Mvo;nW ztfW^c6>xXrxYXosAr(w6?$4pcHV#}`xX_BYeB_~OUejSZ%XUD|Y1d;pyt&>X;}Y{> zY2dXTyM!yO=2ctu)DZIro*Nv?T8bxxJ>{)N&m|peMD4%=je|XG%s3H{Mdr~<%jen} zs}Fk&hfhz>F^H}qwAZ10`HjnM`#0~m2Y>#M4I&Ut(-M#ge@b@i65Ph_%m>~~CY(3Y zYkU6JJXUSD-n!nt`o%X`0eBUf?zOwW;xIdw3|Fquh(L2M7JH7#;-QRl2Ha$3lL6&l zvLShUn9mNwIJ)Aa{#AbC-=vVIEFpEY^T9{xE6{zU-teVSd+YCfy?e@$6>j!Q7 zV~(PMnS>gZqM3Ig6>Mcxnf7cP`ix!0u+@(@pJ&5+Ou}u@`X|qV(tZ3YAAj`?E5dg+Uq7)9tPRh zEkMMtzxjI1_coc6O?e0!|LTCVGL+CKb6Tu(kB>R|(8LhMIc(;iFnV^CAaI`Qu*H%R z^2)FnYhH$j^Zduk`u}rQvB&XvZ0qtYWndnihK3&75yBklfB(%p?R!7_sO>>-=~I;U z(JO=ZGmiYM7JnFuPhDoYirrS1R_5C4cb3|l+|%?HJanED?3NfMeDjr+w$Ae1YZvy~ zH{M|L!pw#Cy&pYkfAJ2q;S>;gXPlF~J^mVJYlhTv_@Q??L1Ktf4NXt!b279d4L%Aj z5zKd*HE&48pMJn6>GBQimw_j29CMs&y^Ry}a_T>( z$*8RdbV2dgUhSF|ZHrRjhwae{muij{HT*DL<{XniZLGiWl z=6rh!%r`EdZ-4lQ-)xsyKVCTfUi%3rSw1*+&J8Yuo8*P=S5IZbx2ofM>M?vzyiUfN zgeU4Llfxd~rGGoe2*Cutvw}Ke+Ma;`UvM4w zQQQ~9DP0MAhy6l{UKSs0EYtx?ny1`ByNK

yf7jNf^~a4i%Wfb{H>KYWE*KLz2;# zFD6OdpYg2^^C;ZMfquKJVT1Vk)r;+|+gI8S#_w~q@*i+h!FOJnZ5Qo>8QwXh6t={+ z=Mr8NJv=kqMewj|3R6z!l>e$c)5v5S+Z^yj+BxWOkptb73=G-y&=O!P3Sc{c2C<&M0@aZd8+T!hN?aI!h_LWQY zsCUk{xB0$&aU~|UhYW#tFsocX=4Qmh=WGOFm`}sDwt2z#rRXc5;v>udNGeup4W;7#Mb@;k3H`e3`~&)$bN$^h}eQC=IMjovVQ zl@awWv@_&Q#vL%&&^r_!yw?ly$F2*}*vDw&tjyMyrTduY%iMql`LT>d<^Jeje7}9; zcfQyzV?y_Qmt~kUFh37LsAma0#4*1i^HCo8?ih7gLYVaPeT=J1DHqVjzQT1G-*}a^ zT$T|I9Qx6_-s8~oC+rNlw32};$&?a=j0|Pw^t3|rn?HlS|3;>EmU2vQ>+sYyQYobp zXnhmG#kmCIcD-)wpDQ7pzOfmyL2)`DcIQ?uL=&#k z$-KOkVBdxZl7{cZpEA#~l%=+Ji#uxf89L7+%x|x>_Qs`4?JmOmt0#cr3Id6Rh14#0U-RMRr-$NSWawGH$GVr|`fjIv3y zPx;3P`JKq2zcM%bHj58E{KYl{@qP=|KFj?Mk$?5`1wndd`l-xfz8|>`Qwx@dxWN8FW>G2$GoU*me zoAoyL=yAb`KAlF(A@@;xogwopOhI;z*kMK5AAhpe?yhozCi85eGJ#;Y2#q(NvlDBo z-MEU8&k8-NN8~UqkPnI4rZDi>=VCLD@_B$g!H<7*h|M@R#UI#U7Oz{BDoOnUKD6># zWf=SGBOP^}cShf-=>ZZCdAaygKMEG6G9~rk13o$WjI+&rCoFN3ykO`plya2k@7y?K z9>5M25`Kmu{)b!hDU2)9B4PWiGaoB{<<^ZX(m(n1o>&J|dX5gWq8l`c8w=+|pw-uS zVOqwTK+2)d*vTkzodBmWb9?3^t`jGeP2nu|KyYINb0oZcm`uSF-Y9K>GWO_g{2~Am zld1I$k_mnli?8dL3C?ZaWsBa;c5jzqB+8Wqns9rdeHy)c&tUd1Qx?)nJGZA*H zj?zFp!b&_19Z}A2|Dd&Hvlgw&MeYysRs?Sg^!OV$TDTwAMun@B+9ua`s>wc8c?ZsP z(ox2paESRXcl&5Ex<;>Sg)TE1A&bEJ%H`?y_O;vGCitxV_}&hCnXC4vJd>G`%Ixe# z(ztnD3ol18(g~#aDJ2^{@4`h!NawhgWSySdwQ1>DCAMIe&HO&^x~I<(15h((3eRrtqK3p}<&Z|{` z(WubFyD?ywhT%RpBplNrOksz8Ox)x{VsX*3i+kqL3m}9TmfYK7$?Sy91`MCMUJSEY zZUkib{Kt%)zkcUZ`{~`S_AV!utZsn!3b!zvEVEvI1s0?1G-g_4iI$C;BmG1Cx9HWk zrmizY=Z>5y_C`xc%Nv5GSSbfTzs04XBRW+i6H6OftPW^0^#D6wR&>3X0{jL()z7pE zt~gl@z)KoU#+&`UKl>AWVTP^M7=tfg+v4W$N9}JuQUTC2uv{eLZ(m~o{)!v|H0!Z5 zP8ZyfFJPuvi<{=8#~OBxLIfOPwQ_YmZHLqIO+LwLjFY*S6Pt-S3ErlAk$@fckf`E} z@&tx72a-bI+Ug2>N^f1iMrEJ2Pd@sjeTlj3?VC5-uQs?_hsa*GtWxHPBT5{uefH$I zJ>d?WH#lQTp%5dFHkG3_lF+g!N4$WaJ&*WF4@4$IIU9^@H0Mf5uG~RUyqqLj`ySTj%VRH3#rvx{7chtmGdpGxJDUCUpWX zoj!g?U`Mopp1dbVUx=H$c{RO5P!W{|^H^2b_(*@x%5u3U2e{Ltp|WdFP_|4H$K@i+ z?U6LcA_jDoLkBci9zt+|BYZfZwmoN9?1A3vM|15q#u_WEWgc?A_C;>>`v+fLX}`D2 zErx85{1vwFzXXSy*RHqQSKv`j(P&f5Rl&_h!#Vmo+VT63@3#*>{D|pHDF-c!|8%_m z|8~V+)gVZWTHYG?F0jVHI{W~aRuk?_82&HEfea>+%`V*f4h3VZe?&pi6ux!kLHqrC z`|W4HB|b0UhOMbsN-(W<5*}KJfT8GNEjDTLq3ntKgkuE;!5Gunu$KeR>%F7^x78AvBv9=yenncib39vKCBwEz%q0Qj%1u zgWY^8)CeiWEyWnL<6AnER$RqWC>&yS`lK9zuea11C@9T%wXU4QYfv$Vu;jdE8Y-=& z+^CSUaXZ4})oHHkJZ@Zd%Z@O5;Iz~3eiCLuUZ-paoN#e@g)26>A^sWm&C+Cxp81pA z*>-zjw_TrOXw2H>G9!m;*f#%{Z(nKOV5iad?`^jq|NMh?ab~lfym6h|Uzi zTM6%z<~??5J!k9XA$Halsq>d~8h^>x(gm;2xp68r?^iCf-+F74olcBEso#S0Vj_uT zP=0tRz@c5ezL+52(!Y49aP1h*PIEegZf2I<9&>O96@BZKSKFnF2W@SCD@92r18@P& zvcz%YtsCtbljr^CZe;G@0&1eL`jh%h>C3OujKfBZVLqcF1jxb>GKoUypfB7XcxOvn zP)XZS8R3i9L|{P@E18?#fiRZH+6~Tmx{cD~5|{cO&0lMKwDKFAw)o)T6Dnl0eFFx# zbA73O$f*_wRE{lcgaAR{jiW=W<^Bmb#sA%J-Nv{81cE~i5%bQs_uGoT_y5=f*Ab)k z3RDm~O=nxI5G|_}mfmAW1@NI6o(pTGARhp#>5 zZ0ccz5k9K&pXJftCa@ApSHt;pR~FlU_mBP-I3Bfs`hWiWcK`nKjFaRXd&rs|%z_1|Z7dYemQaaDB_O(mxGB*UA!&dbOrr4ZX;-K%D zwz0O}o<4il)>fag{cx+jedijps+qP8qsXfB^ej1GoZj#<$FVFRAgd^f-v4y1t)kGo zhH}rcEwcS>XJUXPPN#8I7g~w7fQZHZawz5>EL;7ic{i2GbD4k>19mYIC5!d6OnC zi0~eKh-YI%8sX$JYs%uc8tVmyxd&*c)viB9nC^4w7FY5CyTF~ID04m?#Ttps*Ih&$ zr$>3W(Iar?HSu#$9*wtiWOo8R+(XfHyvWs`D4!_kHn!W|tz;ek-nHfS;pSBP^{=0| zkMC`@ORqd>=dN9Bdo1;+oxgZ}rEP9-;s!gjm{Ei_V}TVj3-&Une-ERq5PIY3di&{b z-f7?Z_LthNS8ufk_g7O-UD!e@Qdyt6Y-aZv9~*`aFToQ(6@MZ-6ALH~@vDT+V^mgb zZfvwq?mcMF)~4E(l@;tncVG<1F;*bI2+=L^ua1JSv}jo5v6^ zA_{)On1eB2;aJqyuAXZb+;@uMx@SN=r}uia@2vnX`ZK@hkT&;y?l9~*z%s}U?$^$* zFfX!aV59+~DjaHcmzPkEF#C6&c$o{Wes%v*dyi`-uYCPFxNB3)FqW_g0#9*LknE9M z7CCU93sXlCg91nTD!`VX|1tERr7>|nyrgv#nq>uj0l_^+aX8H}C#NvF<&#_)DkFdC z!D;VOb$r{4S?`@DsT&1Oe4v+U6;!prqq@W|g!cJ@OM3jokrXAg^tR5cb&_;B+c48nd*jth*|D-?FH0{>jfEx-RcH9iLOI@xYy|K&xF5f_+P?YLdFG)E z1JMG@jkRVdUJe|DQ;g#78?~e5_X{3HM;&-Rd^8Qok z!h!IdD|NKTol)F}ME{x|_ZdUyee%3bW4DCf*-Ehi*m$zrUcbUI^0%hj{q@84WR*=0 z(DpIAjpjK{XL0@#jm$$7b4Xn;Rthxn!RyPM<<)heJcW-o=(Xc7{REL_umg~`zFV^T zEh^(@-4^KynRGo3!Mp}`j|aVGvvvog9v`f=?WZ5NPd|Cq?yq8`%#dt-V;wrEq-Ss-nf0SJ??$G;l6dDW${ul}{gbkj_BL%VK9b1dcu#h3phwt1C9u7?^ z2_a@P&?lrZ*#P$Bcsq;pfAIPx)}rUzDvSE7z&T)<;|Kcg*7$7QV;L@=rL;_z$69-Q7X>XNyBltaa z&y#oxG|YVt?e6c~SZ;s&%dfYGPdD59&tZ~<xr1gxSnV6Z{cN9YstA-@FHID6{>ZEoTi?5ShOtp zpnP)8-?Nvu(UR}cfILTex8vy=i~=YPuow;pw(#vBJ8fKDW<3>6@g9QgoYw0twEdy0_eJ5^r&u9+w9Akg3epHcBdn=|}MQP3rp6 z$!>e~_#D^0TyDQa&>yU#NTp&UbVU-<;v|WPz0^Su%bsw5dgRW}eve&32VR#qtJUwo zc}h@S_o}Zm--$8)X|tbv{PWD+-uc!2_HTdmsJ-(5D;&4@L)z;0ks+;c+ zP=?J*o<)X{qd?KsikFr#a!_l8 zo5$+O-d2ILx7AtE-je4D1}E+SAe9Eztt)AZbxt96E;;Fq4< zKx3Fce+l5T=NG&p)QMPeOkqC8$2q*OP(DDznlQblgWK4QJCD`k$@!^U`JOPPSzNwE zSuH!{r*Vsd5g=hF3@Oy&yPN5R@_L$OdU<+YwbHT4VN0Rw=X)DZ+w28yPG83Mn{9T} z440+RAwBL6h8WLX7e|wRec5e|SSqt5w1nyDCcCf}7)jk=H~DRpir>0(qy7Bhx%N{G zVjgfQxR;KpOba6qcafhS*(C{$*$>>B#<1UB_=I7yLVXj()$L0hvP}_=Fu&XKR0oFF^9buK z1G%YrH<^1H16EAg+IG+$(6gUIQFMzXfdv``ZHD835dl(IDN~k>c-qq_d@{1jJPNIf zs+5Z|Nh^gyQE`KN4jq@I#L?d#Gsnds*uuBB)umB@_PP5982qn4X-9ti4$i#!PMjXE z8V^#`f5RjK&XndECgInSgmZHhnXVx_bvS~LJf8X^6i|=%=Gbv_fgLvXo-AF#Un&4k z5j1;U3WAj-h9LB&Hi{|(FV7-mj3m$=v~|{0caCU+l0A4aR2J8HdfbdsNOulFKhGT1 z^Pe}l*nA(@R7rZ{C5$pOH{E4ww8S~qbJWSy=96}tA@!Tw?KHo1v8}mL0>$EpFNd;% z9FB}T4+n`VyY%y|(=q53k_qYli6y=8I2(s(ldb~s8NH0&<^S$xo@`$D?xSZ84F|7@ zR<31A&~;9yu#-5%NMiR8%K(JjC7+oYhE}tjm(Nm9&PHZB!GM_!)_C6j_3`Vgj|9y!6o%LXzh zBho;Plu^*T_u!*664SJWRlKWOir%SjUiC{_e_Q5A9-0%3Eoka!w7i|+L_tJxgb5VOSQwM29kz^==fWI@8Ez=x-Hj~l zB1GpP%3uv4<5lkeBJzzWUltwr1JeZN+fGhCzW{Tt1)qxy(O9_>^m$^tO+!IC=$UM=Twn z@IbH@XyIp=CoRzud$E>t#IL`4v7N`(mux5o%)l^BUH<~>uQSYP-TCIQ&A;^K^7p& z+~mOdMb4%2wgZh^G&XTlKPD{gDs$dfubyY8&;=S9c3i<5>Js9lhh^qtz`&NP!s6$< z_0(hZAiukth@WVaad{sHliTz;cp z{rNkkN1Ea+VGqbojSwMF#5r#jZ``^JOogU)jE!-}F6sS&tIngwf9v(vax2^Z7UpR# zoj`0`?D=ySv2bF8G{a!nS@lF)WBB`gbu;G>U1hR#fg$b*hj6={&S9z*MIXxESo#M# zfw%8mM)8A!gNk#IyN3a_AWGkYW0v{rJUc~VafHyH$c2P_he{t4tEp93X`8}` zF1196i34j#@e>?ntw^IHiVF^e_+fn^7~&LIAYvX7XTpsVe62e^)Kn`xX^c=bseIaG zgyELJHMS}~#|-uWrP5`#%f((=ULcLuP%dSu+&enP&;%F`k53O&A|VfyL!n*t9!<)> z4;`N|L`J^TkR%d6(q^8rEJN3&rStT>2%7cCp9nf-Sb_(HKeNBVu`R3$a4+#bjouPx zX{}tKMrV#tn9)G1kfbi{k*}<9&CB_V46*B!kId1@DX)BTR-DDKzP%v3_I! zp*>8w#xaL*A0xS_(A3K~V%}!vFI@g^&p#EGbYumRJ+t2?oW*cwA$Ih78c&*=;?zOL zqH;js$_Cx}HbTsoGf49xzyWB*rNSMW!C2>)mfM@Jze-QW{E61xjovxZWuALbxj@TH zqZkC!+s-3I&tnOrK%S#jzre8R-0>#I*s#W?ki+dZEXQb%RZ2W%LblBE#n*1JM$7j_ z$gx1Lw#`z+)I3J)uCH<(=HZd^EA3+$%EBz-s64Kd-UHBXa|F$Tix|wIy{JqEkl9HD zcMc5{<*uQx!{rJRjs;g}R3jOlB?7)Bn1XF&KkEYed7JhU0&_7r*gBT+x+4Cpa^ z<^3h`X6Dad`mX*~P{k)=5aT;eATLRYpD@p4;+JVFAU2=e9fGzHKjw~UWs;HU24{A@ z=BND9whISdASpSKzw!JdioM|y0`>MAZ!o81O^+QMM;y0#I=93(!!%@K20@x(j&wpL zauEVOK3lwivq0;sMac{dvdp@k@hrUaLWj`8_H>1ZoRsh?7gBv2>zYMi*jsAexCN9G z^30t!^y)`yFJ1bfHMci8pw`{Za^Ov+R500#I-K1$4Ij=ept_ywr3vZ=| z^eRq!ygd*RZff$7p*6@QAFR`knU^{o*CcdhhB>M_D)Kp`&eqsc=h3Ht2p@QQc^Ud^mF5;nIdWie-UKCA`d7Gh{@2HFUE=VOSiQR)7Ir|+Qj7v*y>yr8RF?Ji+g%4*@kWIpk-fCN)+r|{PP2LZbaN|0VecqV3nhVd!QaGD z7~l)j^+FGrWoRh#IM0*iuOUpYz4bOd=nA_%m%MCkw`-G|(+_F+Pv{vB+$0WYJKqS1A4Qbl zBQ2a;+k0jsfz=KUGPlCzeAcjC6e!sc0OM&O@BGm-np8?<*AM9wO5vq`psH1X*H9Cc zzYWG{h>YOJi4y5d4`uSS(NWQ5;oUkXF8k!==JhA5|4&)x*4xOD9pDmgB1KXpMO{Xk znH@W0XV*61tbuIoV1Wb(ke2{KHV;9NhXDC0`_BU8KP1R{FYwM}JTtnCB;F)ayyW{% zbxT^iM;tc0tGcVsy-uAvMII<9|M(-;w!%!#A@2}2s)m*DWnLqRT)1y^U4yzV$RBf0 z;aRQTfM=I6>X*=L<-Op+X|LG9f#Hfl^E)^K8=AxRi}qC50DKDvB9SvhKJBM53+fj) zV=SC3aKD@Fr02LNx~u{MSkyz~ZnYVM^E_~V*sY|){x(VgPcj0z$vF48cE>V6R`alm z>#+L~@zYEZy+4I{rL%%vflsE@2UvPf;NH{seF;titWhb>Ju7kJw}<3bK;OF}Ag=o# zoSRL#qua>q-i$R;CYDT6Tr zA!mf@BznpC@Xvp8oT|NNX;dYLExyJ3C}o>J&9n5g-nF`tKM+G zof-}oH&MPIvg(xmZm`^#f?BfI&LXs}y|#^}kBJNt5O=|k0UwcWO3 zR)KNsOT=`~X4@#K?2uccNu%A)chgq!)}ZgMXRhImo;#MMJz6`ayQvTfR|A|kD=eZ` z*QY>#EW*bN{fZ!rr%cmz(Bv}&+>|l528Wi=Ych}QG+E_Fw#ETDtjEkktch9Z;k;@y zL(r-+7r%Xw_K0jcJwH$H`N+J&Ab=!Y31{FUyq$C2&_1M9^et%IjzqcQyWmB?N;;$} z_{i|}JReahPBD)Mzb-EdP~In8{^Ss;Hl#crPV=?L_j02tNugLA5DaVmxRn3%1pqfc zhjRXlF;rPDdqg*kH~S1)z9jbdAHLX2fAQ(_bh~qiiP}u#i_>&7zQ*xHLNexLZG(;n zO-gg-8Zzi+VPTG{d2U#(#CxHYAQo4Lv`BqwS0y@Lk7rmyCr(up31a>7*?=6qS))1m zzkq4Sm0i};5(EH|x@oeVaK4w#^9W?m!4U%A1Z^$K&)H>pO}^uO#!X_@OnZ%pzyhJP zN5)ih>wu~evul7t)K?Sm4nf6BqCHL<#rr~-$zZ$(V%22U@uVf98S2=U)9gl|etv|w z5()A5fMf6T5}A{~IQ%ZnWpNbaX)E#pLzFK}Z2Jz)$b*{2ug+%a1M6Dd&-UQtDcz<2 zz*7NZu!ZZ0g}>GB&}K^(21Fm}kHIV(0$(b?v`e~E+z@!Bu}YwmHP5-i3mMSU1pojR zS4l)cRE3VFT~y|?1AH9M8q%sSL8cAZfJo<%W!^R3Xcu>E%^zN?h>o(Tf55gH*Du(Nc*aA6QzLq_^oMo}aH?^qF#LrU5&< zT@rrwKR1K)ehQ&Zi2@-sZA1D}5ol@?+XAk583ck;gJ^RI7|DsJ*NJ|mI+U5;Lt(wV zGK|a^vCI(Hig3GNVD6jA&t&A9Q4(zn6RFYIg)n6br*9ZT(?2(#5?wb?`VJ<3#SU;D zOh_~iBLspimg!`$O2{6x5RpR&f&q;+AVZI`8bS}XjiZUt50iUKWUlB@>e_zIV4XSE z%Z!aP1u4FO81%}@Oors?#g&L0W6OYE^Phk5jsP`J;JGDp#@LtwdAf&21fx2R z3dR=DfqP2i-R3bRfycYi8cPBp1N?`F2jB#!8=YaW!}}eZGklPNu?Sq5WIkmD1&_nr zn#GL&bHo_g3Q4ECMTSNDUZ-rGzShXHxD=;$4NENJm-Yc$haHA{ zuF=ACqNujQ;Yaz|3a5~#g1DqfI8pA49bqS{pnpgUCJbZR#pvWp6AG#$c`w+A6 zKp#PedHL4%0@J&Z+Lawl=cn91U>WCmXkRxEk6~QzAbt9`-=uGTdW=N7PXF1uOW*$2 zw+!EOv+=UWhaLFQkxK>21d@I9KaS9}UdtHu`-eyQz=s!QDN!Qt;rb72#_zIvanqPx z4^1wQ_y*BRP|c%Hy6g0-&v( zxBxC^^iUQ4<-!1!n#WOKBuJJ>sxjoAsF>AZEU#idQI4<29^zi1(B zu|^^r2BvWXjZ0k z=5bjne~YAy-17PZ_xaNMCl&839~gIXfhd!u0~N})S)e$)2l>YQ+Q;ox`sdI4>0iF; zrO$}j)to&YZPK@B*Y8$)LHH~y!rUa)B!SJWF^Nq6WkAH?!9hA0 zKEW0vNgC#tW4tMjs}YW|)$EEmCXV#kG z46Sa-yrAR5sf#<1t2wp8JT~?>sGh#bU@nb*U|M%!H-$_H z4PmND7fhH5Hl|kSoz5GzrZAN0Wh~>QQL-?PTE(o5<}S|Wy;Br2mCO}*De(bwXC}=} z4aXN{cw8YABwU3+)QonOxbo#O-lYvPtIwxJF@J<0|YX6|^&P(07>P_Gcmt!DSm9I_`DqPhBV- zD0uWC%;t`7Nn=ZTnE^PIx0eVJmjm`l4(G@Bp@T7*(agg^B=;Nr@%!}ZzkieZ1a18F zS6`$ze|VoR-jm(JHWf$a4}O$5DzD%FrbKk1tb?zkto*}oX(*)YedR|bWWZqDhikw+ zUsTnX=Uj=?C}xFFK~x3lR4|9pL32rVy>9yZPj}N^n+=y)P=C()$2BIDi>BYK$i2i| z=mHa`x}M^Kck4u^dB~Pw?p&W0jtQiB10X`@{0I)Tvsz9KB|+}>F{NN+rL7WHtk!FyK+5s+LEn7&*Sr%We9ukbt>EvI?m z9ZU_aG3R=OR=y+s39;#TkMU?@8PqW)>-8IcWA5=hrMz0qHru`ocAlrEz|5-ySA!M= zs2bQj)#ix`hGX?nWYdH()B;`;)4{ep$f1#k!bCz;pjgiFf8JYG?U5NaYkG-1moEj1 zaLSL7VY7y(N(c@a)^YUl6Bhn6CclPxj0vvtKIf(tA`xb3TOc{*n?8X$0V?4<#0eLX zRlK1YyAIfI!#;Tdf10e0QRl$MX|olZQrpD^WvV!mCV0*}z)>@Gf+miQ$%K*rXsi8Q zQ2z!v&zRF)V^(jNYhRMgP&0l+-mfKVtA_+ywBZPGA$^7#=Bo}v&zT;2_u-bHj1-9! zYCCu7{2YfFc)=b_PtnFL1nW7^nc97FupVCWC&LMeIz& zj2ZY(Cwfg;krOFhC1lwVWa?=|irf5|-al9NK*f%q;HWTb1)S!YPL#*36#*7AEYe)i z?ujGPeU{%bhNE{9zF=7ptGZ@70hS3z1}qatBV(TGTxB4Xd09D1Kgfh zNbaVHjyOmHFEGFL(1dc?;p> z8PBvmGQS^9Rhcu!ix8-NmPoDR;n;W;;f47bGOSGEGZZSkfwDN?(Ck+T9|IM-1km(| z-#zE9?!8irH+0S+qyV>xJm?oS;|Iw7JOz$|Rr*=s2|r~$?D4RkPFVW+mIe3DHdI(Wv|qb~u331` zSojLSjQc zh+PuBU@VkZ_oxN^5Jdx!MUI5(M<1BRFZrv#nJ>JtvB-78;{+P$t#ayFkEM|9{>2(?ral!@=RJh<4mcm1CF$23`bRmZ!NjL>IYDVM;M2Mi_wgepGL>aP@Ia$;k zZ{xt}Bq`3_mt7+b#8Hl+_nu-?V7vXg>vyeO4$MJhP(1jFwqwz$`J^*x86|W!Vz= zoS+Y~UQr8kd8+#nQ@&Rt3n#S4vjai+R`f6*q4lS@r)|%M`QkRyMbAlcKg3k7I@JXI zx+P1%NO1TD{v;c}<7dFnAO7}!{=l?(%BR3JcO}!}cvLVdl-GasO^xDh0^zfEG%V{@f{{A@^80_M#aSGWyDwFQI14Cf2YVLZfJn`?z0CUmO$FGl4mf(1-#gF>|hFJ=k=aBi0}i$&Q(jn6v@PqOXk5ArcXw_tuv= zf4X4W2fr8XMKCdfY8FfN+)&vRqegIfDW-I2gMC&Kdqn=IAT)nvOCU5Azk(Uj4BhYSl-_yo+RYZyfQPA!14hq9^pH39SIW%L2z!&MGEUkXg?-5 z^DZe|dPE=1Fn4b$uke&X&a)i)xUy)w^~+u z#WkDS0 zjB(I4%iPZ9Fe+&=$4#=?%~-b8AQuoJhwhK+(ZTF;3r2SBV zNmy;7Zu)HcBoWS8rJ*Fd1z-ytRHp3=2Y_yUOz0No`8I(g_t~L=Y)c3G=Lt)g0L> zh%ullhXbiGMw@7or~&SN~FGa{NUn3Hu( z_=1S9Enq)_al#1}?~>sZI`qR%p~+B<2skMNk2xl7jdG4$o!+T|-`4yx^;nYEA#||C z?(k}2Gf(t;%=c2K!PMUlK^i7Rx1T5+1U~Cy(X?i4+}iHYK^$_7Rc)_=ugz550EgnB zhruQ10cdu!@}C|d_*{Hr7Mar;3}idn8P)< zSeGG>I~yDx9;K_Rt2Da)fOf@s1+(keTJnrF775^>iR82*y!9Dz07DWMI>a${UYvzb z#CC`iK<0mf4>08m{HWGDy#`Is19%%7`8GrgO&)2nGKy@u{wpeuvS;oOBjiGkzlxF6-M0( zGZ5xy5dkEJ5P?x)+!$X%EzaG>+%fM8{7_d$ESbue#=Is_cB)5n(7L&=N_+?-#=C0x z!i=?kd}s|1Ed^Z0BHD*KkO}40zG~QvYZ*2RIBtRa=#Te!h^FrxL3phr5KZ&5mxp*v zcSD%z7<8(|=h+MYIpp9nu#i|8Q*+168!kq7fq?de(_U&jP2)QL4Yi;js=!!{bhytx zNpH_$HO?GiE&lbCUxT<2n^QY0pp~K)+itd*6`1l~ZJ~q{uosI4`hB9D`efl`Pg+-b z>Buv#*r}sUs~;8>16J@Q^=1_NP09nzc7rY0Nf6CD*E$MknHtv?;yGvKP```$%T^_C z-d@GfO<_hA2WP>&O_+Cy)paNHid!Jz(nR+>diMMf!9r{^v==F!Y(oLGU_Dxt-vmUt z0L%g<#l>ht{AYvn2^BFxt_dzi#A2Z>3uz-gd49Aie?O$Wk! z<6LJ(j1|LtV+@33H0$CT=Fka|1qtIQ&Z}reTpEl>yh{6R2SAb$T<6+s?~xBmGcz-g zOrjK4@5Lcdg{Ff_fy+mea|?uSF(si=aU2E@MS^ zgqr!{2tmrU6e0xcMDq?u5#?Nh=Y5IWalabLCfZJ)afbuvj(o*Odu_A}W+qG;Rs(&u zzALNjgF4Wx84V)n4iBCad@)Y1UcG_=#xT)4d?q((aD%6s7nyjjfq*1@CWa<=2#(eD zDcrf6s@+4p$oyt%(KI%i%ynp42N@WpjyoMNW~cxApr-|s>*Q)<6|f?kJm4ezA!I-? z)Qa*_jD0~huoux4_@1Y?t05_I+5~h!ebDC}K^pdLuz!;Fj|S=N{dM~7Z(pYm7x05f z<_L`&C!B*TNsONwaT!qF^9V%c(;!e8HzS`8bN={{P`oGNk66+2%by*jpFeBizDHt` ztol2=a&N|jgrWU9bTUjgpMhCX0oaQfZhAVA$4@Yu2x0NMt6C6ZXjse{S}x(c#t}#a z0%S6S@7+4m{7OnUwSdl~1w`x^CB}P{*F?(^FOZ@pGSc3y6h)uFjdGm1pj)9~dt{L4 zH`ik|3(pWrQH5)ukFrcTa6#la?r@iO3%Fd62H^3ujntBe0eHX1R0FG79P4TgxUY>) zuuFp0TMZZ0TR*=yoKDnHo91^aSdiGj*=rH7&kg?t;{ms9;s!$KC%C1#wmNH79?c=X zcx=X5^c`Wq^%(PmA9NwOpq5SHBW%3q+@%2$Q?%@7n06oSchdJb>n_=*b1uAfI8ja} z)Tw4#K-*x-nLCL zs?%y)wiWogWY0q%2w@(PHpupiyS(clo?`a(QCpe|tPayI@#JpEY@xHs_pfi6w_l(@ znq*AeJt8P(lp2e12ok|4Ai|2`$Ap|?YlQkiSQ^yOfI4+{_=xVyVM!QGuCz~JsdLLj)iyW8M1gF|o+Zi56(kO2k_zWHr! zZS9|XUsu~kdOfYn|}}B^%Q{r!N=O^3jhg#3IG7a z{}r79aAj@WtbhQU{(mJ%uP{J303Gdr>|c)l-y5_y|6yUEqhnyIu14tDIOjvIWYk-`Two>|9yGw2jF8M(I9<8 zLBaKt@7F zMMXh-^Dm*I{P%AxQ1DR!v;=6pbno8C>hOK`2qi>MDUs8qCu+9x{1KMQ&oEcoB2SEQ zu`?imD0{DG?e)F&!y=>LKOD$NC`hRPp&}#y``14JNchMow7dkU7qUA4dPynyLC5zF zt$A(-AxEf7ZzQ8n1AX3@4FaDq>vt52-c(jv1nl|Ib+Z4IS0%F!P)?j@yK53}bXo;H z)_KEne_Hpj(H1&D=OnVw5_`Q7E3Jtp1@ zKD3X_jcPe+}wj&p%zav$B5E2D}#Q*dm4iX}y2D5S7EuQrHR zeN9~6sQu{b#UnHFZEVSnT0$sCJpP{NkKu!-a+ral_CPuQ;C2GVT!BG6QUNYU$*(v~ zk{Vh#R?JvbC*Zfa3Y(U8ynYrspk~TyKiQ+ywhk$VQDw>0gHWV$fTDOlxZ6VkgRN~S zb(rlQxEHP6WQ;xjlh9Paa#BOc$B;(3xMjKfzn|P+MRUxJkM`P1mq}`K?RLKP`T=Uy z1YQB{^f(=4r^vtuy~>RuZ)s+dS?*& z!q@$f&IAd?Ncwn}olJ9e3(a{%nrh)NnMMa{y3%h8umt3pT|D)a^0B!wws;n$w>#lC zl#=|`{%4ht%@A3`nFbw;BByi<1;H>CI-xxZDCZSNUu6P)A1g-${!DW2*Fh!Hy6J-= zgzx2<;opa6`GObYz|m6^-tHN%QNUKk_wxo+)%AQ@ih=s~vx?RRZ;XHW^jK8EtFDBr zyxf;3aEu)o-+81;PIle0GAwFLDKuDD`@~h&BZ_Ig3p9?I2e&6E6;1c+>H=1hdS2#s zy{Uu~6S#>TSO5TPaAa&PET(O3#`WPfv8U7boD02K4 zZVwUDuzQiBZVhbQ@Y3Ky7OO3lQ%Vd))~7Kd{1on6dBIrZ>Qx;IkMCetqJQ(Ux1~JMj~TGn)-&GYn49 zfA3U0I6Nin!%>QkicV{aIcfuhO8r)>Qj%}CZC2Og;FHG!DWmNiegrUM3yUG{s@^Ou zRs9dC;l-Ny9*3O?$cvv%_^Q7$Whm}6Qa*kQ(PP+f6Bdu-?0g8Gg!AYBoE2` zo{zM1%~Gq@%UZAyL~FW?qV8+!TI~9 zxi(Uoa)8HUv7TaU0Tv7(dL6~7wAzy9Jj(SwHi0{}JO9AP1Xi_4YAvZhNpVG_Ula66 z>*IG4Q|s;(g_p*sdz4$2!y}a>R1kw{1hc~=SN{SH5lQYFfHN@MSL`$craiN~zIDz`2XTvs;F@}7L6nf#EWXikRgW5 zx5R+T$hg$U$OHw`kq2~Cul$cipFE=Abzs3nQF7S$;Fe%ckv=<)uTx)pQNloyRdKjk zfq>u>;(3NO(J*MS0_5aB7wDY&GBV$K&R>@*{gB0uMwYN>Ac{+zVvB*IJ&_-T7=Ma~ z(RCu01uvA3t?G9z4MN_j%Qz&-Mv)tca>l;N_Hh9e3{u8Qnk(4Ictheij@)n0Y!=Ep z)yHMY9Tn#5iz6BSO+3-}gTCpzS&jE2Fu$6dZJ(VM3ecgnJ6UrIE_mLD>=0LiF!_<1@(Eox>DbZdjA!fo zq2|rW=P$6wpKd=LysnUqI-J^4jSpm+TWUG*K>`)5jqlAJY0t%hD{+$N;+dW@|=!$z{By~gibf*+gH%~@3cB$(9w_$EC{hUmd{RfWLl zR8w{1kbm#8MzNFr0)9^Ry#6ML5D~z#wk7DT?w^U1H#gB0B?dR7n-Amq4B1x>EE{uL%W zh63q~Rx1Ap%3-|g51ft~yjWH$H3!$bc)A2{=xIui6|JSQF_g6hN!^EwoxikO<*+zf z6v#t|f<8D(oqAsNW&fEQR#QQ#(t5Y|T}{>pQkimHT4lSDy~w%&QVi6zT2vyI> zw-xEC;intnmP83PUJp^>XqvC=473jxZu5VB3?60za}z&KOHZh;+Um zlqf~wmWzie;BAp~hABYho&<@0KIxA#M|`>5snxkYKiR!SoZrZ-d2~!p+E@Am1MMr@ z!+By=SjZ$ly}^7iZm6}>6j@3&1Nl$g!_VLk6Q<01$>M7ERh4wUI*UfM!^%WIIpqgB zKI<-Xvg1?hOC9X=Mfrtx%jd3JDj7DAoD3S9tolg?^@{Sc z2p5^+!+F+nVoT);kpa#1Ki-`Zjw^>n3%fyk;%4tkDb>jKYaL{7KWAj}R+Abe(aaN> z{>9uCnAD){r`4Hc4~*V>^3cevmBbuS=F`+SEJv$(5yAVF-n_cPYReJiSZuVZ1s^zF zJ@uItZYy_VE0uEoA^Vl|Akj~Mrbu}wcht}2>^Z4fE%*zn;e|{SY}Be%4Q-UI-mV&X zl$^uSIFiE0fvMc;A;Jx+2I+a;rY1Ua^27YY)wedOV8rdaBY3=7lh9pZAMbhZVlXi> z9cAUEYZqlZE7cU{38jW`((TXuH0#a`67|_mXN*T% zY(1Gkbev$2%HBmKU9)Ccaw@J4&n}=ugp^(x>I-^4INbDW(!3-xBd_)i40cmKKdXrO z!onb6f0OG_S#szUqwJ>5aWh+WCpGP3T^iWALkpgPP@W}&ZfHJZVb|^m4uez z6(pItqt-8VI9#*reMMybA%Bc ziAn(PI70?|njV z#!&q7t~O;+A8g{tN5~*tgdY!f^gF*K%Zv-eKlf% z{AW}Dpv|7pPi4W1Uk@%r?h%(4SC`+JIyo4vnj;rdO7|?$Sq2xYTa{zs-lNJ1GJiuT z?9*+3t#0LkY|!Kf6IkO8Lmv$ZT^e&(7HStCd<3O8(ed|dynm$pN#e}kU)(^O{NWQq zDFQA)`I`9qtEKW7)1C?Sq!xVW%Sx!vANa2Ndcz^?j^vzktF`}#?~QhU3y+wIX-R0M zf5zCi-ZydRAufKNu*x0pQ|f^AvQ4ftDiIsIE`T{J--o&~eO1I?JA;J^oy5~GxV`~S zRepo=tR{j93>Skrh3JIulKb(zDn;ByB9y=5eb)B3BF@f8l0aZNf5b*$elPx>vKOt7 z#n%5_HP-!d7@ORO&GWjDBwVmz4gha@H+r!13ixK7ljr3k#KEXSYQrPp{a^0(Lc4Vm z6n`&PsNaogp>PX~CjOMzXoNq2dR zDjlcdg25;7On8IkgOwg-YLtUVZ{cR_hMi-9E2ml%0aecgF{&V$=q4%Kf76&pfBoHm zR5Z3hG->eV2T}h!e%l&Ji!11mQZhsfFlpU3+vrY=L}JBP5|7?LMzio#NOlpOC5r(0 z?=cAiU94XLw$IZ+-d8w1$Rx{vuo)cW?}!m6;P?bl@GGESg*Jw;4bjTaB4?(|MpP>> zSIfTlJKlFvt0y!y$^w697Z0pZw`>;s*u^!7uAsG@T1;< zTQFer&B@Z~2kQ4N%S{;|;Q-&F?QvJ}_g|xG=!xt(QWIj}l(zh%JF8fOe;hB0?2$*}yS|)?U4;f<3|l{bH==@Hw_k6N zaAclD&ke2If{@`$t~IfZ_k$*%(~6Lh%JsmsrE;aTfUA5K<7d5^4{TGTH@F~Ed-NKI z+T4uH#P;-`OQ#EN)Y5-`Cb5O2LK1?SkN$boF5Ne0A)0GK`^aPcvCX1Q^kh5?we}~! z^X_GBWK|cNsfY$|L}wF^zj3MT4CN!-OYE>W}QsLzs`B>NWg> zAvG+W=unj6KTm$X*#`KdUWkNlbgR)Gd;;l8tZQ@ud2r%rj?G?Zo zAECgW(qOU!Z4g^tmph*&TwPwG;9dWB{imS3?v#U@h_+v_3F9XAW!Q<~B>T57HzLI? zMkzQHQQ*r_aGA!8Eov_y1u%6zH^XMHIeF7PuJA{fxQ0ou4^)xSG^Fb|tksw|8>iNz z*bi>+q!)!DL&>ZObI3#U;RPSQ?;M2sDx~DeE5|Lf9mMDu5Ygb-W7O;=2D?m5#=QcV zw?Dq=;SRB1Vil&Y(S(1~e0Dz*=#D#LHUR7vKNs1Gitu(F7zNPREIA}%t&RmYA30!F zk>)Rhi)qq!_Cg=8*94UD->DSeC}uAHLb;cxWZrEX)?+L)!2Q_beTQaKCA+>e4-ivq ziEi`k9vWerrZz3c3)aAU*|3JF`=4A1zXD*t_zgw*2KhexV34lHWObkwD-(+>nNU9X zrGU%MNs!3e6XXnm|03d8*zQ{BP&z+(o}YARHw&B$bjoR)Uk+w#eAkYRZ=8BsE2--L z=pFCHcYXHw2b#11Jx%;6Z>rknvi&%E^i(8tMlkIm5yzn1Fw$U6s2g-SSXZjNVmC80 z#52s|?R0B(%?j~x-6+6%yB)WS()tdow}lipt*DjAo>YG{CzOWmm$s(DAH|^b@4FuA zY(2;@u>r0UhqM}i&#|J4g_9+4u_HF#`WUxS-b65m)Aa6WXu&k9t)*pSOBwD%s?8Do zU6fKjnamKdGMcAn7qPju$`~whbISrhO8n;cbu+uWN4{cZ!z+_Ee`MvfZ(3UEfiA&bF{Bn*DfXu7;qn;%aCL8>T zGxh`D$nWgnzkvp?31R{Jf zjoqrnthk{oltJLffSaE>QuF-b^Rv_;71-WR@+Ydid*T^!{9vF9_Ai;Ku~dhniSl+M zT$_Y~^mwoyrRu4NGx$xpqduP3Jjk8KeEU!OV>-1z*jrl5^B#& z)2r*AOCguteT*TxfS1g8^G-gKOMuNsaArNU48}|)>SEqE9x;qiAkD3nog7{I$W29r7<}l4RI;0ZKZ>3gDRuP$+ePMG=(~c5 zFJPfAFJ=-gxuPD6xr(Xsx>}?p2Rr@1$X$?=e$Dl zhy$&-y*}5?VbG6R@b!CyX;Y>XK5D9pAG=xgvCo_A2aj4wIVdHqgZhoNl-R*zoY4ry zPijh``bXLz&EG*b0@jM+M#WJgQGz4BTA zK#%c*BsjuXwdFu{du8~z_tY!kR7dJ7 zl^Lb%xn%YVV2WLt-Ml>H zJRNo8ABAit(5+I>kr`Ga+r}I}Nu6?ElJ+hby#mCZEQ_rxIMo6Z0)t_F9vz!d;@n-~ zHTbfbKj(o=%NQ-+pS0^Og()u8wS9-*tyrLXIJhqqXIS?vq{GE-!4;S?;egu8vXp$Q zy@K1|bGwwccmCHG!9UW(XLh}3*Ac8}sXIovSW-$f&zkw=cZ3~iCuD8-ay$0lA#rcL zMI;Ps|M}8v2~%oP0(O};y=4g$AG=mX4JF~rcm?p1t9-I&l@kqRR^_@h1cS1CU&I1? zrK^k(t7(RvUv(N2Unq+jf17};BHcfCszvK3R~UY198i#t71TLpu0&$64QeWPc9DV6 z3zvSMX%}Fc=CyBSmB84EJ~r(bHA~5N#1OSwk_C_I{-@}sp)?-)g1Y18F)8O0lVO9Y ztYf-B`Vdh9Eg#2T{c6Sfl3UUEa>#O?%y?qj#-nJKp3aprwl4}?CTwoZu!kx(A%d5; zyA1aB4&Y2!^+m~#8Rup5X0B3HaDl6_hN;x4)2Z0EJJy37RBWBqZ>x``4qzYNCm`AK zD;Dd1J7HK!5TO4&PRi-OxTE}IVQI?W>K9|?TW%R!6>IgZ2(3PR{j8tptnc?f1z*2R zeMHXmik%v&_x%;oc+5{07#mC7hgzL1bZpU<6mlU&+tuN}Jiq!M)rVLX+C(K^KAsPR zUrQKs!VonKdf4j2xx{CZJzaqUQWJ2g>vd=#*KTlZ;E5Uv$2QBA8SIAnNWg>MXJ4)a zXcHWuOj!M6r%jaaMz~J1wZ^db!EBox+*{&+u|aIoP|_?+oYLOb0c82l0ic^2BWlvE zmBCxtBbS{Fm0dm!3`HjA(RL=o%I&Vt-6i-lD?))a08B+47+|6M>c z&pnRow3M#;(ZsktA5tTvZ0ZVYcdz1gV|dSe8m7WCBe$ex>Eq6akSYJ#u3dO5jWL8a?lXB_aVLJ~Jqt}xBy4*e9= za3Bj=*4jU&*pPyI)zmfW2F|=av1?aTGhTN%B(uQBBiPU2JVLHQx+5I@Lgby=HltE9 zwUhtN5hn_~Iu16i5w!mmDUqN+Aovlxn|Jb{PRtNTHyt6uN;vJ-WTfS=8hdDZ5M3>r zm!q-+7ddhFS+LID$mCeHad`6JsOm2Fj4wtiI%G~{ki@sbACJ?m6A>sToTA3pMdJZ$ zWlD6%x#?xS6@hZ1Dyobf#AhH^U8M#~dDmPv;4Y9!2Kqh=qVXnFY}Rv5g$|NfJ4&*# zb+buBr6n#z;9_@jxI@Ffz#6%#vH3n9_dmo%$@MyRN!WV2vJnXh<9TM`M-fLl0{R3# zMw*?!wG1S)F`=Bqqfa~o;YXAF-vVgL6Hz67n6ypg(Gu%2{1-Sasr-MCyui&MH6prQ zcZo0OcpHe-`dfFjDo3JfS^48hBMJ}c_l{j$AB~QE`dAr7oRoXjI@wn4}FpM*6GBU=WjN`&QYp=Cy@$5?qD z+8#3v?47Uz{{}%ze#)N1!;@(-SUTkP+xz*3fiuhPjlK3QPU0!#Q}xtg@g^JgR{(7V z(M#ndsaS6vu5s-{QPn&pCt+KbxgYZ>XO4B1Xn4WT6Z8p)gKb%Nc=C^&T*Ue5+C@EmN#9Y zlETm3reh`6+nbh^6(A(S3&4MwQB%8HVo=QWs8_(XG~(_Rfd2kvGb}@A!)0EZt#U}e zXn6XH+V-O>S*M2IT?j)+S_Z{GCi{bU;SXwSefjzD2Yv||$;1NHvPJ1dh(bFxH#YD` z>>AtF?D}mG`t~IzNMutBar6?k^0LK1)w_#ICD2kr_2Z>@1;>K(?_{Xfr7F;9%Sc$d zYW5bsak1fwtR^mOe9&XB%4k9?U*iMG{ZHa2=HFSX>9FNmpCzZW*<2HE=fv%f&t#PqP& zz~rFQZSQg7mWD#I$iDL?1Pu1^_&xSOto2Z>qaEN!Pq! zB>F^ZheJ29NTIG+Nso+(49QZb!ovh)bG*_xYpwu+!=PWrANRTzJkA11pzK-1DVCIN z<-{&?)`5h+muOBlZ^;Hm)%d~s$MZuVA+CTy{t62L442@84L=C0L|(B86SRkRz)U2j z)CYg!7i{p4N;0C%ZV;n^1G19(3Rpt7Q*C$JQiXMOb>6uic7{zHX#V(A$2loxdZN(P z_727ZxFL!3xv)gn9)dU-924;x+@WT5iYK|k+#2p!U2Fr5)#oNlK1DW1+8WxW3j9{6 zJqFH{cJmlt33vGYFxWuvdC1sEb|L4S&NuXOHESbJPYbEiOziqkjwOb1!v!|4Sptf= z@x+#dlq;c57P${FhHW+wkzUV#`jXiJlpivpm`bo9>-_q6wffB6jA^p-Ywn*U*Iu9n zGn1Kvz!6pvg`pTL{FnXV9yI2%`&(O*{IsRWkvcu9no#0e!h#bR#BwoA7klxRESV2w za4iw&XL~Y$gK^xCfBUH+LIvb20Tj0y;`<}r;Be}&PxBW6@>m|~)%Us-{d`eO7OMJd zUWUq*gq%~EFH9acd(9vMIa5vs<=(NEYD0qtazpQW#BYpGz%gSJPBOvoC~0s>SLh>( z%gljOo9n{qeaH7_JuuOwM;_=nh3Yt~sIH*e(yVRoLlzsT;NF`>P(fgBC9nr|le#pS zWU^LfZpQFL@fJYkQ~)WJh9WgG6?hVo#)Xn$sd zr7^k4hc{=8yG>n%#U!9AA9th7SgkiE=uxpn9;0H6C%CnZeQqoX!964SrfZD5U9PG%)8A(bBlQAH9$3c>BYiCF1 z^7`4yC*c)CVPYc?Z{+bT#!jOBfI_88cs&(=H79FzF8;*HW$heni62tyojBQN?WZ;Y zRkPV!JHqf4f(!j4+K|AC4aY3c5|g$7tP+-C_5!x{9}C_`Ya@Sp8@gSvK4sqfLh{V& zmsj3*~cf;?BQ9_RqY4$dJ0fJWr$NoXnU&osp zPfxSx6>XyolNlRXdW!DqaQBh?0tMo*N%mMrt?W#H^73BR{|>pVQx?4^PIGdFha~x5uAilv4@<1@ zjKms`V{4~+6N%}oo_QIHLJ~j!H6px#=6qDB?EE5i_Y1kjzzOPR68kK>3fb9s44)fk zc-%aBn;vN=j6)tCro>|Ro{T2Oi7O*1(KPHEvg0<{`hz5p`N-^#bA?3>U1x=rtsv4J{7C>w25=qU7lZ zF5MF}n(4dzivwPPE!N)N#6$AXJ)AGple%4J*Euq)n>Cx!I`Qa~PrX~kH}X?DzF_g7 ztx#fLwk*8@MndG@h-pYNe(bC*nCx66!U~iyRdXtdm8a5>{isPE2{NFwYw>kKx^X0G zFlNaAw*EanULSvS5W4{FE*E{SGV28bZUKn9dVrc^CZgBqI-yK^l)>Kt6p;efEY5>~vF9ERDCHAjwk{{mIFJ3n!BW00Xt&Y1-3F8Ib%BQt^a+(XN) zfZi{*}N0g8s8PJ8J7}t>PJH4d@p9oOTpm3kq#Tpta zzZJ6N+vqle>M<(tK1QNpg_8fjo_t0bz8TNOH9o{-rKEVBt4nrf{D$zAv6TRuv3?l; z#pF(+s{*Emq*SUN^gz1#tiRglx#cN?Ub|{iWk)`2=x_|UNPplZTk}+_DdyS|FWAQK z4X=KGI~Esk{cY)Kk*sg4L6jt685`poOb#DBji5J!XRmWc{-BZ<{!?Q@zNpixy2#4F z3P{UAF#JwT#H^iTY>s1#l=ri=}8ANlEaUSk$)ZK8I_lpxpIzxxx0jH@WCyKNG|Z$>z3HmnfH7 zmM7HAr0eJoePdIuo+vk*tzRi~L&b5euW%N3+aR}C_i>?e%JxAc_~GKgVUzrpz1+E; ze;0?xr!x=@1(MhH-os~}ynUC$Ky0~n=#(&nbHUq$&=D0!m3_!FgJ}uu)SnbtxOpmvfKVU0P1Tl^JQT0Ot(f z1R$oMdS$1%MDA?{o=yj_INC3Lf#-hjlv)b9TFxLVW_DfoO$*7Bpi=*oolNyLRC!JCA=*I32zC#j z;|Ajz%jS^38}(a4XzU)UxYZdTYcAZLCeB=O$?+gf0l`Y)1tIbFI<49LvE$`^jhPY3 z<&izt&KhXDJVk9~Zb}PXnZ$GU=pu6_r0gqo{S*49F~ph)z`Ch>t&BL&?5QGNv3Zby zDVg0u;>WbqlZShEHrEXtw(PApni4eevGFf^&*o+k1UA%_?rWAYsLb5f)D>ZiwD$?M zIaC9UJuNqPU$Q_dbOMaCVaDG}s0ZU)if-8 zC!)Sx{?##4A`>Q_hvA#yH8%L1gI0-u$;Sg#XU zH@O*7IPWaKH&cqd=$V$r+SLp=E_OWrhnOVi1sa;2z{V`sdmd=LSQXT_o$C2{$La zvDa~S0?79tT9GNqo*X!a!8K>%J57I9y=v2Z-iaj4zN||}k9G2byP)UG1}1E|r-kno zASy5$)ypwopomxO?LguE&7LW;C~D~7=yKn{#n5)f=jyEB<}Ih=-^sHJBD)^HjMGc` zU>K;SANl$WZ4CUazCO(ardYX(KKd<@PA{vK6ZmEu#YQXQ~bA8aJtEnC&CUuG;xfxwh zL8+fKcbc|N7fUyUwka3A%rqF-rs);<1a#S;a_(TG8(flZEcglPxT_-)Oxg)*yT0lA z2;2~Mk!-_p#sm+|YC?I%)aSb_YCpzCLeycdt9ZmhpP7l<}DR=>555HT4}3!wYypPF35tPaY#i z4vKphjA;DrRrtQQR~$K|E?EazS0R;Y9MZU=)^5RXGnO{#Qx+G$X(&iTIlMD~+2m4T zaQ`FdC_xIddWJ4o&_GNd^U#xIZ`XBX#ge7Q@BP=!$w02{rj{QRZx;gRW7zqbH0E#` zB#CM>fnx0qMS4Dn>lQGNnFJ~%r1p6na>{#3unrHb2)Fu>;s(;}D9r%3Owsz6*ox)p zd3V%e8Gwmbo7g~8NgSEv>;-vU+#JxE&5rN3(AYbdxK5$EQhH;WV_sS&0+}?Jd6O}! z3wfBg=V(>dn?TX$z^`3o&40_>!{+D5NjZMSu8ZPAjdiTis^9Biz6sEO8;Sc0U}5L@3k{Ptas_#h2iVrH%Zjk2z(C(NPdp zH)iyWM3X|OL2V6gpQQV|FTQ`E-?#Uix}QR?X!j|$v?7m2@~=;0rzS~T`LsG^!K#^)trjUJr@_`5-P0Vj;(LjMM()ZyeTw%W0|8h6HMT3wTvs>wL=@ z9Ib<}JR^tObTilWJ3Ql4vGd|IlT^o1dh?0;Ve^*Bnz{auYItoES%shljpf=Pqok~) zWTFkbW6oL4sqO+x<(;hJ@^G=}O#4Olnlcu#>LpTK5hdddLr$W=BmSx`DCk*usV$pp zlLZE4>`bb96Lh8UplOfUP_+mid&ud&$lK4Ag~U?i!JcK|_WRyN?$B@+MpF~(ByEGm zb)S|ClAo59x4k2N^Ck>3XN7;;nxTo44#;-_eVCcekrphrV)QBF0yk7Tf4c{+tc61I zEVt`JRr`AvPn)5teg#LpegSz>bp&HMml;Itb?fc0Bnps`D-3XU`*zc1Q_-xVr6u)@ zPENy95HDWl6S*3nBW5@IpoD#umU%$!~2H=ukB zn?k!7(v2fU{x;_lgZnrKq7oAJ{fXrzIQ37z90OIKRKpaB36Kj#ba@&_%DV!uIZ)6j z{wN0M%fNlG({(y|8|Ug%wV1l0RmOe{Np!k6CR?OVz5ZkH7Rfqd&G6l|UjW)t4S(Y9 zd0^qjCbe^(pA&I-oqBu`&(IhO?iXE09J)@Uh~P3(L-RU?#wOS;qUXbUus+O`r}d7q zBey~&8K&5w<5*t$J*P2ikVwt8!2A)VmqVxY6>tNCMr&^O-Z~J_`NgrXG5IFg-goJ7 zd}?CQWs)b;=Tt)o)Lh3~6l56o_8Dg+C@VQ`-8z{t3JJr!TP>C6H}C_oTe@1`WiU8@ z=PeDD-BD+!vQzhZ>iJkMt_zK5$g{`S+=W=zTbPYCDq8TBEQ%P|f2lhifIC@CWnLH} z1#T^@KTms5bXDdwy7nX90Vu;P5snoSj+Cl~m;u()2)@lU4Qb?1KPp#(r{41Wfv5)lU)ek^C7G~upsc0Xe9TRzi53U>~p zth3wii{`+Q2VlFtaqKGNt9tV<6 zQxnaS@<~0BSTg^IL~`dg)4R}HF3iq{ZCEqkumi+3%S%MJSP9i3kBDAxil6WT{qMY< z7$k@Y{PVUx2E92uwq?4gJ`Y{Wz+?0d?*^E*A4&u+g!oKG{S}ITVP<< zj)y~+dUNG@d%Y7XA_H%vSreg|o{=La`~B$H5x8fXS?zI`&^tT{xr;N|DICw(O&wJw zc)%3PuOnCd*?+$<2vZ60k~@C@J4BmYSW6p@iPE0NUxS>T3!mbLXhXxG_RD_du=sJryJ-pml1mjzoG4EH5{IJ zWFPEsXskdwY54mNk-89w0O&^b&J6lzqRTwrncIC60&h~}ZXiGrYRCnv@t8W&8rI7# z>MyYUj@&vDw0!(JkK?#C00B)*`olm_jZT}6=I#Vwn0>Sj@bfQ4oGWqXk1Bfw=V#T8ucgknlvx~CF66f-JYi>- zL|CYu*1^30;oW>r{G$Fj8Z}}ue~B!^&W&=!knI^wXdy^*>s&I@Vn)q!C(61#kzaZ` zZ_!RqM3G^CUMMgb0 zS!9J9RQ}yKD>Omj$C8({!S_s^V@%>Wy-^hsSR|z3Mu%FkYOt41_$q1k_kP>m1NzoF zmrX7UDPyLnfvH%eeJo#>zmo=v$_34*R@3y(S{2LlLI0Q)n>~k1w$@Ds+`Y3nu9ar; z$^*1P_@{ZZJR6e$2>>@$Mfx!_S1uHiUHjWeb?#EK=qz8>XK16MlfPkaTPn#9d{YVc z*l2-2LN+Vv&*;857hq%0vts$6&;U71I|YNk6EC-R!M7q954sC zP2grm{K{=i=8M=dT+IKxh?Rhl2%-@6;* zSWlad*TUrOdpA4857SUCW%A%&_&ibOge0eu-ZDEW%V0X$d!psKCke;*>a4t3J=s)} zBb#=q4kq+TDxc*S=T#~doXl$_5M^{Kq{lGwu8rHXlsCU5!aYZbu3l~n=;6bLJ7(nM z-@m{()=UTf+t2*2N6A)rP_p!L#x}(^%M5iB&eEKwkRur@wzV=BZthUpjHvmqbOmZL z8PMf(I@bmR^%pcU`xu{GF8uj87cNhn4KGV9X}{1Vr3O)hF$+9UoRFu?kVy zbqnW0A);@Im4Wa4VZR~p$3M4VyWaMVc>{b(bQK8M7{^6~SjG&<1Y%XM;Mi_pSazP4 zW|F}Fr(e=BjvI+)KxFfqVs@CJO_o&lP_7jEi@hdI=>afW5KuB-O2ej6M9kr z37&!9&g$8jteWJI9NVKf#B4O_lFrAkVgE$xceAy6d~XL&{uAZ3o;z|I%lKYe6g6}a z=YtYR$1sYm=EV`>Kkl5AMJQ>lGj{Vbw`Mps`Gsf-FSL>uVhPg_14+7I51Qd9MkFRL ziur%eU!ToqyJ0snqRb;M13n(nR>!$>RO{Wh2~+$1yH*>CP^BRyb}%{!#UC^0)>9U< zE@5n~l#N9BWqEdq${*qkxxwA$7Vi2=C_H&z_~uw zg>mE0v_X-~LId?ipTa+y^_Iiuy&D=_&L|bg#9<#4(fPB#AB>ItF93ExiND~|v~Df% z@F$=9i|+^!_4fU$;F@VM$8iuGYg*4Gd@xDbZUa^BAkF^(aozhSu?42j03($Ay~{W> z9wz5W&)I^KLIg?m>*`}wzj3#SoWD-e5-Cze1FPrZg|yQdRaPr@e)Rf*wQXI+CBEms zUPjk;ay`AEYiJ=(lgFc=nzcyHCbmD7SBl_Lec{?WfZJzi_h#^=w*9!=#LLa9;?^}O zBKY;CkCm0a%v+&K$_rO%_P>92w)JhkZPr_AZZ>Nm5MJHGGSXPuGBn!E?^-d^SN;=R z6?6jf%Y^%n)qBm-*K^%3HpNY|T(&kA;OJYXic4cG@-!^pn<|~}8~Q`8 zI*-Y`Zr1km%NE;gyN%VnD|hno#S+hOiso75Dk3aol2+n)Q!3WDvdcDOX&_d~aeW8R zN!)vCuHR}WlwZ?xMiPSHftp!GNpAZGPZ+dTc0_Y5Z6>N1$) zbLbmWJdw9nB>+m{YW38p#A>B7sTqIZ4uw}L!%3;{etv^4NKZlQhbap)(Ll^G1%Rgl zY2Yz^Lw{|x9=%XBQsB-$uI_DZ+t4b-)rwisSkCO0)2WT$LiA*f`n63UW2kt?prpITk*cja1CiN})u$C^TkPrkbiwTB=tIR$nu=mX8SU-D(^A znL624Um{YQ^z92+^Y8D7OCrCLHfEuru1Ct4ZW^fAZ&!GFuwzFXvHf}z#+5kPR?dC6 zjWR-5ugx%_;3L)U{A+@i1XSjow^{j_ZW3}H~$Kg7lV0O7^9`-+7E?4h%|+%4?l zSqyg<5yoYbLCYI34D1p%pitV5M=IdmTSj|XBiG_NE<)RF?Es5P1(30MG$O>tJdJvC<>J@_|JD`3{l(XFYHS$e87VpdGP z1$Cc-*WZc6r(;F!l~uReZP%XW3!UL6kVZh4UrnRaB?6HcEUH$3smKjknnE3oBWS&X zJ*+jvi4;*JVWg3kIKg75qf-z$nW!piPqzrAr4Dt5S$5lqr-nC;s*bf^Dq;9knX}cV zN8ER7NMM}2npOpsrmC{(NK~|pa^Mtk@$ka$k~S+IHo%hI+THDNU0X=6Z!@b$ENmV( z1xZ~jGGN0xt`x7x)5aJxakhqd#BWDdT_&K=Y83FH`!MS0iQXLUcLG!@Lsyt*T)aNq z1>elY;rdMTzHXWx)!wJ-G-a%&jFdV_GO3SFp~F_HV)|{%%zXa<+ZRUHxHkL3vXfY5T$>+8?_`x!g^-&aq1^h2(2JyEMf?g>|6x zM<%b-XI@<^kr?MJ)qO-Z=eKWB(*FQj%ctiHkH3FOhHjUASK-*;>Dj(xLri)>hK zCAhe^vACF(SXq`e2%@HT3mWJ6wQv|24h=5XVBdFVHu3g|Rzf9Sw%lwMhk~qwDFHn{X*fjSL14puwF!r{^x(+ zq;-aU<8qO}RtY8b%Mb@L(ON_0hNuCyM0xIJki4@=JCV%txpqGmB;agXQa3D>QBgsN ztt51ThI5%5FiF86L28VSD$Ywtyms%^p6cywlHa#>50kcrDP);uHvLKU%tJ9IMtJy) zxiBPXug_(=&H9qqT}VN%Q{~@HTQWfk7e8hx*tJqhp6lCHOfqi!bUby!*H-ZyhvGu1 zibQOFKKAa2SVX(E%S##NNo4D>{-G|vELucYfQo%b?xo`_7cNL7%LNajMSfBXM0~ zNb}E^oGu075AMmmjh}k9+!2p=J+|8(U$-r_mZ}3^^X5q;&w<6!*1s<_pQ=SWJ8Q;$ zzW`@;-R+i}ZT08M34sY$GbYl(z|M-Tk56JEbm^ zOzH3U_V9z|tLP%OmRMcPHvyfIY{urcABAq%P55w-ncPV2-k8OJH8|9-1g($#%fUYMfD~Rc`yQn74o+g3w9?b9@ zhQtpsuF!xq|5F$IDhSXTaIFr_7TyW!&ED zvKr5Dxk}sKDY1$-h;y z;OLSUk+%emu4p2>hI_llF-0Po`Qi|mW5i3I=($YkXl+OTZvwz zDbNCu!nCC|&ONnQhTPLHx+RM^QPEKeItctn#c z$Q|WYxOZIJ^&Q%^9L0`pwRN?Ms)!@pbPe6Tt>a7ju)$bOy!0{7#15mXX+{fHb!H5! zn^Ox-kLBPW?xsCtB<2@~vklDQnehE4Vf7Y5y`8($H885b{{WdHAmd!MFvP(1FD-O1 zJ++}p^~fsfju4&|WGi2VYCMi(JZ`HqG-!5~KQY)cGP5%qJKKZ?gd@PyFg!eZd_Lc2 zE#Z!!sH@y8UAX$subms@g$J{37> z*Pjx$M9y|Rtx_3u?SHLz8=d?cS-oqk`^j`g6xQ@As=D33N+@GtrCTnhtgvp@NCv3h zh}o_0C6NS-QI)t;A?%^4!m;%Zbq4s7*z}u`iYVsjG^;2y5i*vj6{k6jd-LO#4fQE0 zjr(~Nu}gq{>)u;N(KB1jvrjrGW=IuD8i8P^TOg?v`$inHrVra?cX0ZJ+k2EBExGpn z<>VoQtXEN3%^tKTLr~mEE97ZWiuZ+KihT4ZT9t;XFtk7;s|4^+() z^u;_2(@lOjk3uIQ&63*Mdq{1Klvh_1O-P?jCufp~v8;%eITt)gEs&+U>Kd`*z+%CJW9_cb?VFC>+U_ql$SyDEwM&Vv3pryX z>#r~+ktl(J==9~xx~qV*avf?Dle9ZU<0&S~XaO8zR)K-@)dNIH_tzBMg~kE1{W$GO zd7y%8R&NOAXH)Q(sZWJ4RZJK+iT1lxj5)cvW2ZHQMtrm9MgpGGjCa9}IoH+N2Gc#> zkiBFRvhDWL8;Li~#xbRWLS(mwm0cy1L@6n13h}|aRyVh}9(Hf(UjG2xa=~_6TS=j? zGTgo6hq$?r$kI(C04maHr=(YuY4P)_zU3WTuWxY0TdOJ351sq^!u$8@i#v;JCYhx& zG-f#M?$=U=3dSy$q|!NCn!HIQl39&>sj>I63kNz&1I22s z{{Rud=r*<=w>wd)%-4u$C}K$E9^O`Xf8xOCG0x&*lG;Y;yw3z8ptDDp*o^1yz}Z1` zX|eSd5e7?Z@t~S=$1td_L57S{PIVc0jUVbdixQ}nFZqUGDom_CAuE6hR#7tAR-Q$g zOX(+(PLz3?Sr}Ky$}3C`OY5lKp9Gq8Hn(>lOePC#l1FX43zuh|OUDU;U|C~ME2>s2 zrKm`&3WHo{R-6`xwUd9MG`>IXrN@lZiu3;fDW2Ybd8OrqEbPrGN3WnIN%+)Y@jaM5 zQia738rsoKAE@;?@{0ce#fN>v9dMnJ-pJ(`kx+2WLHN`*EoCLJ!yD*C^j?ixnN-x* z;1<7TDdd2@$s#yj4YKLvwhOPN4XpZDkt0YXeAu#)(~Xxg_<^oeW>+ALxL{(E%M(KK zqCAJ_FmBy{e-)X<+3%h z$dOtyN;DM;=1D(Ex}n1b(s_Jo;X%G8g;yeIA-Z2k*PBdm(V*OJHx5{pqY9?wN(bvT zK$1DeK%9j#W2XeMKHa-{i&+%82y8d;-)whx3S>bdt3?}1uCa|r@W#@jGtk#R4{^n1 zq*}hRsbH?aBDLA9=Sl^U?=?xNnloSBNb8@I$k=MEqN`|ncF_pFcajd_B!UvhbV#Wq zN3W7ZKtfzbt`4DJMJO7nOe+mlc~`e@P4`@Z zMW|e6qWPz{r{nP*-R&&B(_fkZeo$$K=3149=`#i+ndLKR<0S%oPVC z@jiW1&Is^0)nx)r53xjBg^k9~EXx;GS-O=f2!>byr~{d_^q(T9!;N&~7g1NuSka3A z04zk8&8xMxJ*TzLZO^?&yFq&GcXK3m6F?DVjUi~H63MCR6qTS>UNqrWZ5WV z&)VIm+uQY)@g3B0C7g{cq%G8qq2qJ|<>_$k%EW0Ne~T<5?>ji|8C-Crw%Ye^)UM_3 zHnZ4MZ2thK)vg-K?)z1vT|@~ntnoKe>0MQnQ6i5p#j@oZb7FQAjn=xfUnjA=6QKRE zu0*AyySuiWTWS%+?ysR{jfiQKN2RJpph=)786Qad*YoUa-W$uN{lB7v+%KZ&CrVp% z5yhe#hi9D=q`CyHBRlKULfV*`@i-=$WUZtX1>{!8sVGKX6naw^1=J65x^1z@!R`@% zl|xBSA4&&mU|uN=-U(Y)!PRHM%SI ze?$AK=4U$xbL`h}1l01`Tuaq*1!&Pmr3f#VG_=DwmB>T=B>JncvE3uOv5pmNq}q0Sy~YJblG-xrtO-)w05qqG#8nFKK@D}fSl?M%?oeBWytSHk zlG0~%62MV(AeJ~>ipY)UM)nhmwVNx7jTGUcUI;pG7(*t;4Pw($!ubUxFcihs#V~Q&nuN>AV zm0Y5XtE?=CI#g5>?mP`Vw|YVvu;WfsaofnE{afwZt=DF^+%BY!UDCu!rUVLEp)_fl zHxaCbQh<=zaP*&0WX)=&$kT&`n^jtN^Y8d~_k8&wXeDS;d80)+3Y7r+fx{J~XA$+f zx{U&xs9rZUWOhPp?rv|h5u$QKu*ZKG1uUVRB{b?ZDpWB1CDbSf-f?`iEA77#*(SL$ z2)}ByLI#$SC}vVf%TsjZRQmw_I0l9(kKk_BYj~84jkzuh3W6oMh>!kh`|&MZNslB} z0v5M(iA0cEBr&~c7pTAkl{GUEPBQ1BY7Un}men*1S!O+=(MdUh#rs?cWN4#^#$k>k zYFT7=5=Tur`?46ol^r9N&uH=fj;2g^u6Ex15}0kRW;_zr>+kL;n~O}B4IGOCviL|@ z582dOXN|b)rpGGxYj_(MNhL&f*6}4Y)uihZa^?oK7-Gg{=N;oL>R7Cj#3@iniCh!$ zT>k)u7^n@hrXpMU& zdqtk(dvk3)y~MED!!+|v11_ZzykS|DhMhE$j0-RIaicD@Gbt}!wQnZdw#bUJ2rZ;S z(oW2}`kh?CaU~SU{A=nzXjw}xk51ksJLs9Nc59%n%Vx4oAwUagouQLUwJXWd{9b2) zt{Z&Nkh)`;32c`45bc*%@Y>Bjk@{&AGF&lDRsA$WGv;Z;du!q|JSprlxfz0F~H1z zqBT1+dk#4yg55#O)$`WGara{IRvCFMDEazmjl8;QvJi4)H3u7{%v?U68(U80x4plW zZSA3gXOZ85423B|GxgSe=lsA zVWUdeFE&)Q+w88a?WMWAL`ImxHA$#s?;uVzJf!?F!>!#acWpMbmYWTew-QP2HpLLW zriN#|ecXj%8w_fh?vt90Pw@W;-F_j(^anlNtRudxz z)XXa=HClRjD%5ideIsjR>f4P9rs4F2S=`%2d)*dD*Rymj7Vg`nBRMq@#HQqRovGFU#_zlM~^Wst({ zUO_9t%Q&>l1WQ6XiCpPHs8r$nVv;JZY*s#?$Q!$FsB1*E8CHUpu?;!?Ur)ms=>Adg zS4lc+;&BlMnvA-5V#_-b2sN6Ok2a=c;)u_qc2LQ)*hI<^qPB)27PPEr7suU#da!F< z0KZTk-8*XM=A6X@jFCwoQ2Kg~61q)m>ct?Q#Jg_g+`ET;G49q@mb-*IY_}6E0%etq z*AUWDt_p;XFtJmpSxHqsnqgMPi>rf|KU@3KWzwT=wdykjdwW?LK!%_X)}BPK4D|dl zQLK?;s3Py{7kWET?~QAuFlC0;&q?;Nk_iY4>?0A;y$edIg2#V%MeK~@xOX{749t*E zBvdSQQ>O`x4^N2m_Ts*f(&elk+BXjT+ywGj*l&B~z0D(coL^o?QJK>#^)$AF>i(Tt zxM6q1t`^=)jKSmmQ!M0x6~5r!oNJ2o<~!FMdjnffXzvo-#RPjf>rUsOvR~UUsI6DOjt--yHg;YiPOlM+&mLOU*4Eqnbl2DKI#ug% z+1G+`X?C?%lA<|js@0`CHoTdiTx}Q923tEATu^G)Du5__!isG=!K>QK(41r^o9-G;PgZH!(?1=*fwSkmE^lx7WHl8q=tdKq-v{a(M$`*2QyH#?hTzFE4fA`w54Qp(O#pXC zWWDV}^3Z?%@9oU5byAw0APnUrQW2Y4>hySiTx90nkLot`m}cAjzqwI^y!Y*y#46C- zTa^l$kSo_b*|;Ap4qT5PncC^U9b2huUfyN0%j%SDK38$KXte1Lv9OVKGJ~j8CRyU~ zbt#zopToL6JA1}Qc*lP&#ir2&a?2{pUO1FOy))`*q&$V2QRNkL<(lJ9DLFYRhQwQ} zyLUmi2%uM%Nu;$)cor_8RbHE1OZuiy(H#2JSC>r&FYU;?N2EWuY_eb6Y+J?Xp5EZw z?Nu&{gWL`v z4#zOvcf>{%j??9A+IW;L8KzfFEI%u*tz5oeo0|`5@_|wEmR+tP9^V3?8Vq5OFAXis zxp-u8>@g?A@SRoD$k`@&7+W$i;wWj1{c4O)w|S@WjXdkOainYkTW#4Yj^;ZnV$Vv5 z^g^1+@*1(@-c2C)JVRD{7Qk&SK1SZl<+kZwO}T8*V7GSDUIT_39JUe29W3%IQl_BbXO7`jau%?e0Fq--#4Z(Im~EU6 z8ZRirI6#MKB-sF$P?RTFb+5436D$7!tacsBcY8;xexBf(n~e<3=1~?u{TRoIe5R{z z1rZd7Q;fVYNEf*o{>&U`S`jfTZy^~SB#L-oQC2>llGs_xC8~O0c26%!rXWd`{_TbC ztgWpgQ1jnjL1hwh=OxSON|{xo39Hmvm^$bMszlp%d(ESMxVGF-t>vw%#3o>xBz9DsL~Z{hgzErztYadw3#Iv6f=m|Nb5~B;kbI?m6{`88vg(Tfp5hN z=KM+1?H^8icHLgn^VQRzVjZR{I}p4{u~H zE_y1=VzjpDEmV}yrgLu@Jh@QPlr^Cky4tB|i0Nm$EBmL{n@HpXYud@TDl2h)aImm% zWHGGD!-xFU{{U>Y{0pux5~scWYufR=k&lYdyoGWj^8J`yro%DC z9F2DDdtU9e9n#i4sy*Cp4IBLG$8ByceqBsq0>pXRP|B)Cbtf6G^7ifKY{z$Z8<4k- z^4hy{@$AyYrrhC<4Q(1l3C3LXOlQZn*GLY}gywq_7TVDSC`u%pTzSe6) zG&9K@linn zT(im}k1}-BYDP#r~G05LC+H2G52ydtzNhAZOH5i?J6HPbUPu$V? zTiIF?)4#{P{{ZVN*4i~B&utN$mOVtW9k4dgXihbC&fL1+hmY18GNx9vw)A};W4fV^I zktK}@iCKcVDI==H^ip)Or!_Ej0_4m7>)UR2xnoJpTZV^-+bT%%L|g?8tS?3Mvwjym zJg_vxkXbTl5VG77O{!xRwakFp&1?n2!EaM{3&T_$X=KPbIj1gFz+}L;AJXKV9QQwC zlCO04*Qi_NmIsYPfMqOAGCE4efFB>Wn9Sna9p#@Td$`8mH2fr8&5Z4LG*ex5bm#!0 zfE+S|n=4m`FCYB!xS7=n|r^R?d|pU<9)T*!n<{Kw3-)9Sl84OK+UxPczJuU z{0&*0FUU7d7jE7{t@j@5u-xO(ZzPSSSXjR*w^u<7jpFoWJhdf$(a_1otxhFXyNwsJ zcOR&+_VtSG`<}}Cd=OiUX!iuTcllEpW2>UEjcN*j8c#4v(-+Uw2NcmSblm&?=S9Zn zx7jQ)tMw#U?+J0+kB@$avrh_kkutcnoO8fLAR zA3OzY9h{X-!(-fTcLIwtI=5DR60IYQy9zBA@aI*_DtmLgT0pH}Yhj7@jsEv^+@+4y zEi4k+TZLy9z}`xU3~bU0jcXi@??OkWxYFtZYBekty+({C*K(7@iW~F_O+?_NfmT&M z+GBG3t+1n+f5YR<x!WpAhk3uepv@*V>eSF%MgaP=0)5GC5)g8~f#c1^mL))9I zJ-IH^+Z&*;xfj2wc`z3SNCEvsR*1wSayfPAsHXvw4K!`7JWST&tf#;OXAjhiITp^% zZmxFZCMhm8K~Tl2#oG9#!>ayd4dk_^6^Y+dR%eeyT<4xwo*N;#NoPH*(a&`dg(PWU z2Pk!r%8KTJ+cu{SH4TETT~@o9TsJ9bV*dbCn91F4;8dhB$5G;>8A1O5=HqS;pB$q$ zZ7x+r#UO1Qcz`jYEGAGWG>swB0djH&h5@D$XqrU;0;ZV?;gzbY1CwV6JWtz!tJIKB z*`qIi3%ZkS8|t1)(NR=a)5QK9@Vc(%(HgGi-qPoyd-Q;L<|XCq$QTHf2d z7jUJ;w1b*CQDj3;HDFZyaH}=Ab(GJPeyr{5vv;NBHjEQ*xr*Lvc$AtgAy}lXd`$5< z_~NUPGfKwbr8BhwcF1Lmkd+|p43e=BFqDs2Q&P12-dMMwDlBOIk7UEQ_kWv?c%_DG zNux=kaJmOw1IAnMv7UtR%ldGfxcM4-YpSsB+qT(m`Suc@`EP^>iYuQ2%F zs^l1fh3ziqzPXj{Zf3EAQE_mp?5MP&BP(>`LE&9oJUri8fo8*9^(dMxi)%OOecTpe z{?_u|6BI1=@mrsiplq0b(p|q$C{$HuGa7zs z)fSY5EO!X*qpF<}A<;BviZ_wPS+*7{LnKiZ!uGQKO+|0S zaGZSvJye&~es0+-8*8tc@Ywdx2I%v^)5X+%zS=aF>7+6|mrjFO498hI4yV=ln0aG$ z@Y$n{Tcq_Yy{`&()uGzqW_u32lE5U=rX-h1=z^eCQLCrliI+%md$Z= z_IrmlWLD8oUzK$@bXtlE3KQ?eW>w>>)T(n&MJNn+7rfDNe++Wm$r2%HktC8nUUgYl znuUq3mZybC#RpV@+k))2^WR?GL2zvvGdl}<$Y6rr0AD9`K($FECZd{;kgF*~!L{I* z4X4q);RU7SN(IcxanjtL;6!c$EK!zOovrmMfQqNvLCk8(rB$elCd*FYw)TeAYP+qy zzTImr&|7$~;({)uiX#fKiDO2Q$4U^i70G;t5!I!_NAzjC#1u~HySJHb+9!rZm#NhZ z6`zO;%0cDz*Bne4I*RS^JBtn(e8v;IE+CTHdF^d?8SP_|C}o)n%@WL_IeJd4p&8La z>eF66XB)6*>KoM=dXAT;X+wRzI)rvFs5>Ubw0RAQd zlrFPYsgc#!$i_SJ39oye?Ayfhr6CB>uA%-)YV?oITo+B3hB%(m9$XIB0zL9?VFreQM@xx6mvy!ECm%yC!jQmAx$}{img*hWscq)ysDYV&4-Hu zjk-IVQ)OY?#!E*a{G`&fXwCVIC6Tq7{{Z${v2B2=37fWeC?yP4ZWmAmBWf{I=crI0 zb#M;p2CBlH*V{WSp3z~vcI?*!Q&TU-u*`D!!93Y8 zgcI$hS^JA;jg1uUHhPI12VF5}s6L5~HB|~lbmfW;qGq(VmWEi>=xVhUrYnRL^&uE7 zY^0u8W9hxi^(SQ%F4ZSfpA%L*aV7$-0A0KF8@cubcxOKdzsbKXp3i&R?;JyKyttYd*9!%azfI;)->&lMV5~HeObT$uCo5gbTvrPFyY2Ro z?3*3Uh-PcQkhL&tB~jO`w%0~RZmH6_#?(KDJRapt6K=OH!tT|g+&hrBOwh#?GCCb6 zLX=b%tLYRPbnsg8;1xr57>~K;+L}P}-S;F{QKhu7OLZl)F-cN%lT?d!N_5#Yny)^u z!VU|FJmX?T_iJ~l~9X92^kSg_+nY=TZWD4+-EXvpR(>3`&o7p+1*cXG>Go1&lHS=iV^u~i=9eU z&_&-)ODSNvSG#VT2!yjuO?oJYDHLS%Y8<{8upq43E$VGiD@MVV_Dw;Qth!ku(nzQI zbyJ@V6GbL%{{Tt*muuNAw@9~kXclN~QWcICkT*n2sYyV<5*&J{F;#K7cTL0*d){H) zJLR_Hvq2)+#kX7ABFY4BsTxU2n&rvW)9NClEVDNyD!?iomhoxxaU?+1OGx!ap!G3_ zTWS8B@Qjp@)<_<*+P~BM&Xl)X=X=J4wz4|(c+@oqD2G5$JTuIbfOQ+l?Z(?>rKIZz zxr#oT#i>ALRxT2x627CP_!H$&O0{eT#M-9ODjA}Jg$|YyYt#zorPaA>L-=YvrvzJL zx}l*lM@;eykDom__XDRqq8$djHm%_N5 zxfR9`cene)c#0*qOV!-kDh`aXt2nJFtJ3MaQ-ZTRyJSEX^~XKw%Blj6VUXYf+sETvjCe$n3U6tB9qOYuuxAsZQ6ptn3DQTFPKZ zT^wI_(>dCU&5)C3neL2%I_0Pc4gEPl3LC-r#7u*(>wtKzy z-?ZJ_wY81Q+VyUNGnawPa{mC95lZV;1C(LZPz$@Vk_2(@1od9;Y-?(TRl1#Rr6km7 zK*BVCJc~9i>PoE1YTGvnEa3S&`!|N--lBO?`q5;^GF25*-BZJkIGs}B&gV&!Ldym1 zr0{g&xj?EplrT~?WHkdyV=BG;4J#EB(61Ydc;nOsP*4FKICxXT3Cga=JESo->VQ!e zfPQW`EPW#tZlxkL+pu+_)KkmH8)?usWy#m2u^#@{b~cH3G%p>L>#8}1O3UULd+^LX zGE7c>rQ1L{>^n1Cy}Z`R(vn4)hNsu9E2s5!b#m0-i^aUndiAl)bd!+w{=oyZk=R7e z#-O=r0QUzhF=!S@lcx~r!itIpK|latEl}%W0>w8@*uB}(I3t?paBHW#2&Q$UFvhW0 zDK!3@HytNe^5M0xmWKan9mgGzM`7LDF@QZ$}{&9gX9ZDU6XK&Y2Iu%Ns!AO zmDCqZfC`?Bf=UYIUR+u#B-kxJsP>K8>XK`V230KL?gB|ZOY+9zzZzoXB*Rv{&9H6+ zEvDmbD|jH4RnPYrhWoKhxg<D>n z7h7dRg<9b%i<;i@#gs=`l0aEPg2s#afZ@j#TOwH!HqFhf>k0&c9!{&o@vM-%hl@!Y zDvl;RacWG-AdoJ0+f7Hz8fDg=z& zs~$rDE=c~6HGS9)K`lsHg2*+3q#VH%BL=KfpzYAM7c{b+uA(tpMy1u(MQGrS9UoZD zmDG+#Wv2!>C6KFPfwkprAxS>mse+jWA^5&xNp!*LUSb!B9%h-0Om~aQCF#i)J==5c zZtJ>&<8_S7J)N>ZlL!PM(OHNuAsVD}sGc+&KwUvmv`lGt0d*4=3m%F+Z0ATkCTTAJAmZHq(#L%jDz(`&d1 z3Z%%!zvE9g+c2V# zjgg64s4A&Jo^`-vqhza0bH2M7=#-0h@#JEzAb$>gLvICF-jc%O6v$8}Qj>@Kw>0(O8%36aufkj@>XvJA2-}eGVcOr9O?j@0DMHfa$Dao!@wsv$B&sI08OG=)VOwIV1|()#D5(Kl{{@y!uBD74fD8;h^Q7#J$qS3rm)c)<%{r zs9aF+&lhYer7X>m85u(`8nl8y^QI3S(iL>sq4%F~4$6zS_Dg<^-Nbed*Os$-m?udf zc`2=zTSq7a)MRor!d@s!5bESsc9)|41ZV}fHy7?Gz+OG+HPIdwKl zxQ>sw7fUZePv84%yt{3hTdQMnVXBF9Z~C6#%g0~xt#I0lvVyw1#LrP&%|X?X2L#Y4 zUfsQ3SlpMWp4HqtTee)_Q&P5Ma35gBL16vxPr0myBr%C(GI6d%lbN8w)dOOyF5hz* zYPxdqqYvO{aL5-VR*jU>qb@k0T z#9#sX`gBB1XMOtPT9i}r?bdJSP_=6PidT7J&DfnS1b4Ysj8+NO^H^Ay^jI;%Y0)<$>h|s(}*Ua)8f0vq&Jd zl0=$4bWT%7GDaIsYLw4Ew*;<$wP2fNx~T)JRC0Z5!|cVuYe1}4wj0b%6p+SdPfg5x zt`|Ir86G}3r;N36`1?m}RtAzWT)-?x{5o(|s2rOKL)%s_%q-(_zm3sZhq;I?gJ_Tt zi)S%`9C56148n|;>=+h{L+fP7X+Ud&ss*Aw*4`rA_Saj5L317Kb4S6n#IVY_{#=^A z&@mAjJ#^c3s95hVmIitR*5WY4@Bp<2C7cl;70+^Z^{aHv$aym*Rb9AyX{YbSNg#ti zdu^)13wv9MT*h2G1dR{Gv3h66MkJIL+v1OKE^b&PdI2J|{pQZVYt*GS8@U06SHKG%!Qng|}KLQsnjL_hIm4e5t z$iZe^(&gf_FJpOgw%^>W7WWa`+!&>u%P-4ADG;QNts7;=qsq7&QN4;zG-R2!+nuX) z-1d#$%1ce!0|~i_6ZM+FxX(#!5S3YW-hH#Q;-$b{4zi-)W{{V4#Y~$RoF76WQc~S&xqL%ieB-cb{ zewj2z)KB5{VC#6&1zxkSdA1R6cNGXp;BNy*LqlI^G$RHtR0`fUeVsO|*%r>$2aXA( zS%fiyj3Z&Lx_k%SioYjVMbWNFzY{%XR7>DMp|p`kd~)Sc{cT0~<2|bDNM@R3s=UFR zy-A_V4R$KE10lUxnP-SISC(NWS~;q{9T_p?I=Y@?NvJC-q^8Z6=$bS3{^0W}CY|a) zid34?%GlQ9*PK)D%-_v_Xt$3 zyL8hm%KCt4i&2$Zz|dh%@sdpyqqa7OcadXzx!qjt77Mm*=uky!E}HU`MFouP&#PMk zN7=@>$;xh^>Hh$%-V0g1Yx{%A3z5e`*yAde&cH!)d2v)|o-1~wq;jxn6CiLL&|E`% zz*M?u(u@+BgX|Ui@D@_TPeZ+}WS70mw^}ix&2XfO`jsmrgZv0^cBG(Hqf_5hcO~3A z`tILHF4YaaiW;d8$@472oH?#a5PvQ@KaZr|QG=?yV=7lxYjHALh?}Q5V<}qcq;a&Y z2_r~t5w?(hRK@;J4uO6*KBbFf?>^<(tyCiZ{jvo`JAL-GxI~h=kr}$2v1toVVN;Un zsmGpoNuBpe+i>N#YW7dnSf;qyBAa2}Y|4Dv7};Jfp=~;|2_nhTV851!m2m$6xZcJq zSA*2a`?F)a?B3$M?fGt!@=FsCX`@{#;#iVl$V(L?sZKa*@D}w_hAsCPhYg*DJzDH8SSgJ-0s!wm8Dy0 zVmA@gh0^U+CpD!?dH7=aKrXUsxAyC_Sq>@TI z^68BUA#1}*i~6KzhHK8#`9rVV^M1j?hg*-8GNek@}Gwo8f3YE9!)SZ!BhyaN6!=wZ4osB)5^Fl|v~EQb^eyPLh2lxLXM#)3x_5+V&B|@(5ZnpeB|% z_)%4V^1xXPp>Km@-sMQqv_oq;i4mlbIy`!rYMm~p;$IqLHbtmZhUBd7*WV$y$+pR_ zUA(Ua;EV=i8j~E+#tk{xR4qvV0As6IEwf15?t4mjq`%+ZNH;jG#CsOqw?!yTmfbY8 zOKCiaw&wNND@!AKh|!#CYIe+p*A1sn?9Ml7cPrlL+;8d5PVS=GqQ)SCI%8mQ$=>}Afl+6vy^^t$fXxNUYT zUDn3$cap_7u>{auSs}GDh(RQ+$3CIy&xT@$3(bypWBNX!V{>IayLM&%>fZkVwxhI$ z>u|AszGBhaS_t7upQjOd`aLLvyA3oVt(ID~5sY-FQCmc=KB-4vFJIB;tF$!yX4YQG zetRwd0LBKdP{y~gvW6W4T?Vyfb)W}_9BiezS}btf(^QP^P%7R>4ZUC*+DH&tTwPq< z8*Zh`$DlMyDzQ{@>(VNryxR-q1-hFT(WG_ltHA0j`=p@h^s@_iIjiHA2K<`P8>Ec3 z^^L~!xXz3UOM7uDQ-D{Mm{ZxFG+LU2&Hx+GwYl6Pwz-tSZFXUTN!p?)S*BV`BdB7L zpv|(vDUX&&+}CcQR~z#=+&fji$-P}|)_3vlme9H)UBIOg{{T)oN|mdC$<+YSkHat& z@uhM90Cl`g1KfFKjZGZn%a37oA`_d5HLHLkL<#~Wd9_eflG)V%0J9nGxb>6n&)0Cxb46ybfUzesjWN)t*jYn>e5;D(^9#4q%-?ppmixJP z4aJqBj`s5A>NSx9IEranNQ13CLN`!<@Nw7tb4CxySG&mZFDp#T16?Et7DZkJHLC}ix*k1 zrx7z`dNH|<5lWsBs*E2gaxvBtMSPzhJQ`ryrwJg|zMNZNqTT;?o7xQeWJ! z2jS)`i%h16*L0q`nupISVAB@EQ~g9e0c~fsq+|us(_A!(qydqMBZ-HP{LEL|L4DRj zlfV0tq}!WoRs%}(ZIV?3&X<2PM)zXYG!Hfny6%qXnT(R|m#PT_tWvZQvG&CePCp;Z zf+!KpU8N~m7}cdBu50Gpab-d+-J5%^8El-5NP3ZZm9ICxhM1%hEZx7i+gR?GQb1q* zdL)e>fRkI70Qh=_k>!E12^wd1t4-P4$YH6!YrM$tKk45(efS4eRu<^_hi-@FZlbh? zHo3T!#BswKmv?i)RYqmKm~)5}lVO?rdwQ`(y13f!VU92z#MaYBRSuqZS5Q@XhGzjJ zb$gN_nt70$cwR+~L1v9h1qPY523&AbQE=^@zWZ(7cI((WH;>h^GV~;Vnyu@sF9hz2 z3Oe<4H92a1Txhn2q`;DFm*PaVpHLCT@XYyuWL`%jA^6n$u?W!u&uD@-m9C_XrGdhW zM{uH4V9I2#8p7IJO*xjlz<{n|(hM(i-leM9%<(O>arD+Tg~6gwN2Ii#jRQ{#jy}u- z4Hq8Yk!0E~3XsaS&It!B#;@$a)oK-(4{#t@%1A5*j#c#zF;Ent`z^xO7Lt3S-C(Vx(@-x% z{7zNEl|!?Toc+UfZ46LuakMjCMIF_rMr$&oiHnG27*1t64nW>dyK4Q58!#pn~p)9<=OVnl1L+x1$K}V zRdy}JGYXGjI&v7>)AbQp5!7_5?n?!~)OcO=+*pW^9Q04rZ8`8$8VdU>@y4I_)a+$_ z1M&9^Zyo2`8-!Y?+oYQI*(GZ!?jeDU61p;mM(Q)itAQ)vefTGutpiS)^>R{oF6Y@F zS+luWp?KpG+(=}y5wUeIANr`{_H$C7VWm|j8<;BF%YqHw5x0=cnzqq-YTYHYAci*J}h%`_t z=>w~7b!*YVwA50rb(!{=EYx~5Gf5n@(mM?$iOU)wrbXQ$g3E7;{$&k4-NQu~xOQ7^ zjju{q6pQr)jAr#|ZVDEh+L4b}Gb>PIM-iU2Oj349dPhV;W|7V@#M}zjl0VyJ|~SG0UqUv^tg83ZERX4UDQ0 zb4I+F3R77J?ZI(eikAiNv#YXpUvY1!sFzZMZ*0;ADDIKc-n1#n*SXU{?dI?UXZ12; z#_f8fWp_8SOeK4m)Tcmg-a1z>6R#MDJ95~a79BIax z9ATpP{TmqUNr4}#z`Yd8^J{B=bo+$U&+`+@CaW2eH8Pns5>)kL0Bd4Gj#J7294ZLe zUcc@Odw{RE*o$e)J53~#^^}Iwr4e*S%c6~&Gis>x>E@!scl&Mm+idS7xsH2#t9WIG zJsmcStb#(iMyipknZ1Jo*l{SmcAcWdv(CE1R?e()8l4n#W%zjgxHN^ZRm5USK*WPA zu|^2|CFNBF`!mlCsJ9YRwXyt8B5HbI%P2RGWMj%$={#^R7)tI!TaDD0 zi)V1-nhRL%c-Eo{svq!E3sf6ynil@pxp|;N>g%sg?N!+>h4XZF7v0Vd}@xj1T~BU1i$8&Ejx+ zi*`X>zqxMG-R#Z0&}~kCUhdT+YxYA$b+u`jM;|R$RSy$d(@q+)DrhaIe&D|CeY9>i za7DK7_q#Muf{|^TwUj0)2{Hp9x`rtrid9D-NNU;-t2Q{gbvbLY{X2XCzfW62x2#{& z?bhSB=gLp0OYiTt{yvl+3=1sKETE@om7lEsR<8&=GVcgK$e* zxvflhGtQRs+c+#^g^Gu08QwszpwX&)y;)+Y_VX_46JmRvcKeNz+i#XmlUUqM(w1f& zNln8wc+~|7sW~!be{;+DHsQp+-)|8!ZFiPEuW@fGbZu<6JAR=#vAIZY zqA?L%n@j4*cyoRn9xKL8du~=ruePJM#b!XpssXMZ*@_J>wACcoZDEXly}BN?3pR2{ z?3E`W$~X*XyB$PCn%%x?Y5+^kAp8{bX9Vv0UOeK+IaS;eZ z+yn)fuCU!S(_Hi6;Jnmumy4fpc&z!i;GoU+n|QAG?XK4CW#$BibgGRQ z9;FX%xQDnBI4t6t>dyAzY*O>(<`&Cz#A%PJ;w2IVVITp6Q-*ZMKkVMFS*7(gwuY(o z&v0F<>Cf%J0(U#BTM$>2D2$t`f#v?2O+C2lw5wP-82)RwvGW_bVn>!1*`-zniyw|6 zBNEI{3>1RVM{0VfWgX><6J@=G#TO$O$!`C#m2QD zH>C0RVyudjLhPNzZfq7QGiyWA9(rrZ_AXxxQViC2Sy1-&;TwSKF5h<%@CT^_r~a%b zfXi6<%FF{8QN_3nGl;HY?qGXENdc+On59tN$y+2BbM6tg+jkbaB9@foTuZ0 zY!Gby)3@sX081Rwy93~=KJqbYz`em+*5mVx6rfWHATaU3wiUJo$|?aKp~nzfJvG-- z1-Eq+jf*4rja1Y=G#HW-*p;yD5N}(GZO?P^FL1ILr)26VXya))lrkP|8hz&q%aPSc z+*s9zx2>JFOUZWUl-vXg@lB*BQa2v00iRBF9DFc1^77O?xK%=ZpM4BdErg8Sd7t!# znvYF*{4t-ZP^5QHc`prQE@L6-rLr~g$2PZ?Z?XrxGwpH(+g#nF8oh-YLW&eFwQS65 z%lM5mH1=$wWcuA@pmc@Stp18q{Fn$~wL zt9`JWCv@B`RpQjL31ksTsy|TaOQJ%5yPt+IX2jc+ZKh4Zn9>+r+x=3kc0o;v`B*Cs1{x$0r<@r%yWK>z5-?$!cR;7KH4+ z=HB$i)&w4(Rj)TKJ9#%D-9hQ7mS;^42yTG5po29!dz#6bIrNW}I zDA3GJS!w6sDW422WbQOu3bN3X-5u2(>(P|m`ak5V@a%^_OJDW|qSev-xwAK>RSPFc+S3fSS$R!dOz^bch z^B8sJ)=Rj{Dz$IaFaG2=RhM+xHYKPwmh#_AUrT($^kO60pD!hUs~WOBL}9LyVn(@+ zI7y^Salrz{Mj|T0nB{{L18z>7Mp`3lC~zU;OhTmSjJ8;!4kSjJ zMp&sN4##cc{4pdJ!~L(KJJ*dyl#wXs;xH^an{><8+L{oiPOJt!Ds++^bki?>Eimkl zn<12tu1`0!5x4*eZdUB$^B| z%tcnASZ&_Nb+odUMTT3MV~s^du6Ctqia{}s>o)N3*H+NVWQN{X5tlV#89LA(?l8du z(H_gCEYX<-VqDxG74{sk7h&T?ve#&}zOlF5EuOa^vtC3FlvzWu3XS8cJuFH5N6RxQ zNiNoc?b{?6%L=IJkQ9UG<$|jy&b#g~z13DBQcEP(cz?{7FX4h{23OKIhH#QDVwynD z0N7Ph&fKKBi*YPYm0ZqV%new17KD7+Yxd!SMFmGd4s0umEMeMMipvuekq9b5p#)-C zD+NaA62H2>yf9CFE6pOu(ph9Wn5bg(EOTyWCj(llg=@QvSzS$Idu@LeI<}If#9;6* z69-Uzgp5{da!_6y!!}l5pS);ovsP9^vCo7D{kCc=aW+ zJ-l5M^i-M-6+BJ?oktSw({cpd?dY>C*3N9?C3?g!QCUG1)gGh-m8i&r?Z&Eg5@)wh z#1QLsxyn;{wih-wRSMSKCwW|j(yS+FSRw*NGNG@knA1j@64OUw+iW^ZEvL-w&fMfc zvqssD6wt}=(@YIMXAHej8c8lBxs^`jRNIvjMJy@SJR@Z&1WzNAnLK|tiNh@n3f>6I zcfGW%R?Z`7T56OSf3~~PYt4g}c z0U741PA?m^H#KC#T#G`xal78j7e`+rh8h=?QJYa7-2CvzNJT7_usRFP#>(zH%NvO; zZXq>}NX-}=78GaAFGE4wd!uFVD^%J0x;Sm`03s`-R#?lBMM10RZWXBu_E#D$j0|~Q zb_H%9Pp8<6#L|g2sTj@kt_kTWRTPy}S58&btEW8usI-oE=jkqgNxOE=&gyO67p(1R z1-qh-MCOu-sAOppL}~bVaTw9lIl5*JuFY0EcNq`XJ=gbNs5`kv*9DWuWeKMN1WK}} z!hWVU-RdtDVyL0#UuFRfXyF;wxHKt^NC5Qbiv&O|i$Jr^C_VU$qSC` zI5fawmLfpU;frjfK4MTZr+Dw&sahE2m_LEOyO%qIG0VCsZpNDSlj=R-$$8DOdvq*4?-Zo7|O`a>rY~>f$?B->;0pn_wMF_~?0IQ^$ z(zUM;MF(QurSrA!CX&%-Vy<2g%*t{)fIb)^(LnFm`n&9xI+(g!LJD|k*5Dt!{g{I7 zxGh`!wp~1^23&l;;eg7ZZV_u6vl&Y2RSXVUF>eFsfU6G5SUO^%w5bh2nNkR?e8w1< zn;{UeR;kPbo-PVZTSk*c#0>nsm>7V$NjC3z7q$x&@R4;3CryJI7}PLj)54jJK2-MN z$VR_`Ew1OchCw_jBP>#n#T|JXfS*#(@x@iiFJ3G$+dKB{wkz4%O3xVQSY<5n16q@# zQq$pt<-o4wejcNVqU~MEw~8aZgP}b&a<;I*d({BPBR}If>~!&dPsA@2yMEyhUfyWs zT5|5AqlqH1@7OWQ$hlGPrYH-zHU`+@)kB%ua$Dw^bsu6&; zrJ>0lHbS(og)uSLT|%>z2)f!f`Lk*7HPCD4Ri-vqG#PJ>MyA`O4$Wsf z?CUM2^KjYkAX{lQ-pOsPR!G(~2-Jj<=s!<@%Dg?gdSs}anR2ULqpK%&E2Q2A} z0iiMgMn@J7!V+x`73u|ej2Z#zNpvGGjw(q9VN+V-^;9?qEDXKT6Ui0z$tJV zu=QfSJOnr<3ZG0CWB%Bb}6G{snzgF)@NSxZK0W6;G<$138& zMRUE{&f;{6DAg}i7M^fqtF1X>VCO*5N{t_zKV>#mkx#K@p%0r900-Ys?ZWc0m{C4; zE20o+u4>f#uoW%6L03`q?g}pfha@J?QIaUcdE(>|P|2V$;&CQQgOW=~d6PQjCMOV+ z2yPc9@olGNqb0mH?BAGMpotH^LMX*00b0^8W@sS14RI)jCybcf)`fu_r|dYKidf{m zEwv$zT4vNJe8-r;T4z$7d(d;MQ?B#DQ~I&kC}*qYn%-kfyiIf6*GxHzmQgfgm z%}h5P zc#=rWAn8~YWYwmfT};HS3Cl|;&&HyZ0Fkvwh<6stI*M|#9ZG$;9CszCL%I2g;C6U@ zDo(Y`3}}fI?EuI+Gbmc{6=HG&1}!mi{HkTFFdRS`;9CtMe#v38-mc?udvIWi!ouS2 z6%p$)+(w_H39BZM?UtvnW2I_c6|-lk8hjm$w5{U4nN}D(%R_G6@3^DbTIjZK=8M zv0BY?ELKRT0(K0*F@i;m5Wkto9{dohtw7h84{EntV>PAM`h_O$043BQ>LT|*{#*QKm}-|mI>2HauzvabfPoKseuIf5mSo2bQzBD z!ClAP_npx?bs>V*+T0NZy_{w^lzEX>EXT!|mOdonLO@K1EG(w%`0wqvg%Mru8**s0 zAUuKIK=Eqgi6T5gqPl{U=2r`Mw{XhVt8&%aZe)aOBsh+(P;09y{jCIbG8y%KULx&%`~ymkr?$kqE$i4b2*%_eZ+549j@8GMrLR>=`1W+dWFDf z7o$_nTbDAjar{lI;4m}bN)qDjP1VBBJt4PaP|UBYhlWK$(Ek8d1U3WTZ2m)YFoq5a z2B_c(CzShXLxV!@3!P}d_+W*h5alC|IafSTQYPka(J|AOwZRb&MI*hU$65etFbxEl zo12Lm2*Wcpkg?LIfK~#u{{RjtBHIY|_VYE7WrvYlnUspDKI{|}nR(my&6jaacYxX~ zBVEuSv~H1r@1cfz>N!(3CV_Pv59FP#Vi)=1lE>{%*HkfL#oQ})oPJI%yoWa#Pp^8?g2FrsjpS{YBU>%0qh3wcWYX)3{Pl zn$wm=8clt)#~U|6lCMLrhp(mg(6Y${R{>Z7Byy;9VVzX)VeG~y$k?4hmwO$%0#c~o zb}eLpZHYaB7aK*yM^IgAJVrHFFC#zNf&}q0kfc!g99z%_p%Pi`tyIZ!`XWr-g0oZL zp^x2%VyF*NMW=Rb1``wA2%ewwW04p9WpsV`(y_EH_F{q>N^| zXf)9t^2y_`0zUi=ax_i}fr>SZt|aAw+N)a75(Zx$IMeqziTiv7GON+ngm@e-AT;?N zGB{lWDtLP^)WPC6aK&1Q%)ZQDFnA0Ar;b&@awVuZVv1DJtCxii30wt09p`V5#!F_P zKBcWjA6({p;DSFy+tXd9qvFY(f4hb|rV6VhJh1$Q%rPm6-tV!C=;Ghcod7@-(BgNQ?#YCN$BndJwDC}LxTnt(BE8tPBk z8HVEv6Ghdh-&{pvty5vz*`(Wocu=YF!LpH=?C@?e?p?tIQM20J?otWbfJU-f%WxL7 z{9meT?;d!fgMTX}#@zA)M2&)wL9I=Fl*Kd>*mGv=yIJo=)9#8T5Il$sCr?mm(E})I z^XK}SsI=3iwN*yUx-Cr3(`*+4EfKq~NnL;hk*nXH3i)*^xbz8HY%t9%{aBdW+aNOn zO8)?u7$5{wK2qNdRq|*|c;)I5+pM75HtU;9x*K6>BhN!T^~|5uB>Xu zOBKt&=Ujr~w(lOi8;0nSUv45xi|D8ts)ztk11x|9I{yF_aI1B-S~FEY8+)lijhg7P zt+lx*^QjCe!D&{lcxCtD)kY>6PVl$cH(55zn;#Q zc!DZ$^+7gad#>S+ZMFUPW86`eB8wc5u<8k96%oq=(bqK~>RmZ_)B-N7I_gv|d)s!r zf=giVrhuxD^!-Sb6%F9o4_2z0oj;T~gzQrLhUiVW769c+YoTg;NCOl`F(hd%y2~34 zTp4_@Xj?=er6tU9%MgbiNMj_;0G)VgELNV-M~}P_?8ViR1lvul3Jy__TtYNBLrEY= zz}DFScwksnBu8ZSvTK?}AdJxkP9&Bl5%C_3YC3K32wx$6A1KXyi}%c>IB340v(@?4XCuJYUoP`I4R)n@AIsCJ=4kY~158+^M!L_#FQL>dO-z9wlVI zARkF1bAHK+#! z>993C&sPwl>2H*0#WhJ+eEQ0DtO}ZA!vBZ_s!nLC?LHxcZTC!a)0_L|( zg`vCKq-NZVYuBBP0hB3Yp)*}7{+?d}s1tzCjX3fj(dM-+@{Mi#)$ZmmsH;N4cL30J zC)DJRX{R^09PX#3QkmQQe_6>i{Jrz!7VTg%owJeSs~;-UFLpZk)E;K;LEYJ*x5Ns% zTv$X&uM%dPo8wA+J*OHZ&d?w=b74|3a>_{|3o{FffIO+}#c>!z+(JI5&sM5u(!N3wq7x0y$M^-#pjZP#HI3rJYHN_S~%ASxoVnG`imB6KQCltt#(UHYf zJtx#L5o4CdGpG}g6)JpjWioA~%oyCgO$LBGLjM4>0?7n53;UFEwxx~asbv))dog{) z3f_<&RANa$D^W^@0f*diXr@Wotv4Ge?Kghjy^*ikUFC|#Nn*K+OpKaNQL|N4Hzuw) z44C;K@#ThH_K&Jt%p^g+M7OAiLpf1zGSiNri=mJANA}~Bo1jl)uY<2kam~ZIHqGa_ zY%liOy0yQ{&KVtAGpXN0)76>$I+yvsE=;{UrZcJ2Wmzu>qu$#aTiKunqBklSc!kQi z)r$P8E2^&yhmmyvads6~Csh?1t5Du}84al)5+#4sAebHueM94mRt4isatWq|i`ZPe z9&xd%*N+3rej^s{Dd1s?4(`6&Wr|CiTYHX+2aahi;+cc5;!dhMgZ(taaiv|1*)g`D z^jogWC5^?ZT3Q)q;VN1|6ws*wX%R@*L8sze>s)<;s#q5wPO2pQ;_Dge1Wf> zPAd6laB<^RJ3ap7>YI5#D;vlqtcH;Z3X!RgH{n0{rVAY^$&&2!TZD#FY|)7%QJIK_ zQ|(d>2*6te2D3>c=9Z;oMnZBVih=fF7RJ!kkn-F_Pa}_xSdSYcu%^2zC>RiMN{qVM zA&bqv8;Y(%(G00U zNn=w@d2{$SG~1_*%$4eAV3x}W4p$3<(W7sRJ#dEC5oue$^Z9ye-`aRX>kX0CXJ z9FUBMjwpz2$;1gBQJ$b^a3l@b?hs2FTlAJIxaby9Q>b|y3oRl`e5JAdWmRSXlu{a% z%oQt9h{1$2b0ilEUNNOx@Yb~V92y0lwA$|N_T-&n>qjdYD<7#yC3Sh_rxiMqNr}4M z$@+J?FDMGu?JFwtYbUCOBxWb`mcs5@CT$-p){bj%@yukaM(nHxbzLr@D?$9Cn8*19 zl@7Pz^0k;&^&M{7Rs30;bGmDMr*o~ZD}&>%tZ8h-s{)B6)|@aQNU}koI24PM%A?_k z(Q3ouGJPk9hAWdr@KzGmwz8kqLlbZ^C;KqkXEWsKNiYr0-9fgW=i(IP&r;X$g(p@m z=Bx5E{{XFIKMwI{Kj~c|g}j(#*e)!sAmqC}jOVLtbehx^H5q#_&%DKbl!&t2%VjS@ zwD2sXXT#grOg-k7;OJogEo&jBqc$nVZcGJI3Q%t=_EOnAOQhRXj z9m301g{{0oF;mx7W|B=*^=c(KeT#~*6`0o1WBbVYay0I@5@~@{8WZ_K*8^u%wFyb4 zMf4K?07Fkdaa+Z1g0C^MEgC31>|5r5lEQBG#%%)zb|w zn$zrb`YT6_#ZcTV+?Zmp&i(-d$X(8^t&UTSUUG?d$T$e<0 z2==vG1Cn5v(vYRYrJmTbUBYhS6G9%}S&Xs=4ItE^ifW89hM}RMq2N}j1D&Tsv+YJ0 zcJ#Klfn-bBq&G4ve9a*=MQ2eOx-zc1nDr^83g)GYCvc}wgKJUF)vtizi)gtb++HoJ z>yfgrDDyZvqy?lmxA*(Q40g9stJ+4LFaV)d^IB54V`&S9`-;DHO|>x;cp&uWN*y^9kj2pfmFhEgjJ9oG05Dj06Q0vhP}9@ZUT-XPPE6`TXPqCj$N5l}25dd0bG{=Pyib+GQgnO01~W60x)DeUZ2aMgU=frGRM6rCHfTLKs4Vd;GX{s*h(b-HtFVsY z^r^@mIH)5YL8eZMzfhK-0~Faw7mcpk@(3f1R+_Bs3X1ZI6h0Io7ZfOK2BR^E%c9hi0UEP7PEnO)jS5awrWb1rL$>h*ZAx^taw7wh#Z;Rl za9pN~*b;I0WB>;q&0KLYXMi%{kb7;j+di$bW^u!%GcKy1ZzAJma;t7)wiYng=`sj6 z7))|km5Pu@+lM6vdsOPj2~n&DkMPW%q3^{#L2M;!WP&>D>k4uI0O5&U0(zpmx>>EQ zx6MZ6IBbCMt~$#bId38ZHT}t6XC#k2@v9$5*q4x(v|Y@blrjb@BASXHFmO+%w}2MN zk}m6Yb2JYpSe+wFw!G*#3fBStwSuN)vj$jhHUM-C>l=n6MRAIbO*u3tz1^HT0=H5} z6#!?E@xz@3EQqk|(+5JMGBKO|g(KB;00p;`xbNbNLfkS5WSNBrr4>PHc~k7c=E{;Q z9d#3J0~@I(x&m&Kay3IC6a+H({i7KlTU%^59y!z{xOWYN(Lrrey(SV$K;pntm)p#6 z{8m;5`Y>h2`D${vhE_feB@({?TLAJ7t|edUYbh7FvYRa<)GMg}01Pt*;&a#KE2=eRj*RGXqfxmMnSqG}LliZ| zggUyFfH?z-u|&(D3oC<@A`FE+Di`}OD3%~dKT!K{L_#$CGB|~Yu4k}r(6h6KCKt)L z%{1~ES(T8&i5(#!f)%54DHX1D%T0g)H3JkG;47KIHbE@j09OK7y(f9Jyb&Bda zUb<9*Y3#)`Ni@x;b&g0?+JrFWE5Lilh7|#(9*Kmg9E`a8F*z3lTbxm~q`C=HrnK;` z6`6ddo|(mfRw_(7j|Si|=Q8PI)YDp$8+o;lX}#HREKSDcaXb^*Ze~^uY_Mcu5mQN4 zN04}Wv#ymRtxGTU=eFk?JsRrLT7ObkH`pSATPY$AuW%(Y*V01h(Wm|GUko2krwoIg zy5>laD?v7a^sMA_hUxp*9chco0xAD1r`CM#1=%$s3D)U Date: Wed, 21 Aug 2013 07:14:16 -0400 Subject: [PATCH 18/59] dummy commit - trigger jenkins rebuild --- common/test/data/test_import_course/about/end_date.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/test/data/test_import_course/about/end_date.html b/common/test/data/test_import_course/about/end_date.html index 2fd9f95700..a0990367ef 100644 --- a/common/test/data/test_import_course/about/end_date.html +++ b/common/test/data/test_import_course/about/end_date.html @@ -1 +1 @@ -TBD \ No newline at end of file +TBD From 938b0946793a424df5bb3ce9ee2cf144cf32254f Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 10:32:54 -0400 Subject: [PATCH 19/59] disable the 'dont verify HTML modules' in test_export.py. This better traps the bug whereby XML-parseable HTML content was being dropped on export serialization --- common/lib/xmodule/xmodule/tests/test_export.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index d9b80422e9..5c5d8307af 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase): print("Checking module equality") for location in initial_import.modules[course_id].keys(): print("Checking", location) - if location.category == 'html': - print( - "Skipping html modules--they can't import in" - " final form without writing files..." - ) - continue self.assertEquals(initial_import.modules[course_id][location], second_import.modules[course_id][location]) From 42af561a171883fa722082ade4c457836bc469db Mon Sep 17 00:00:00 2001 From: ichuang Date: Wed, 21 Aug 2013 11:23:00 -0400 Subject: [PATCH 20/59] pep8 and pylint for tests of nostatic import --- .../management/commands/import.py | 2 +- .../tests/test_import_nostatic.py | 37 +++++++++---------- .../xmodule/modulestore/xml_importer.py | 1 - .../courseware/tests/test_module_render.py | 11 ++---- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1d77e9cb54..520e36f4d2 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 4009fa230d..fc68975ebb 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -1,27 +1,21 @@ #pylint: disable=E1101 +''' +Tests for importing with no static +''' -import json -import shutil -import sys -import mock from django.test.client import Client from django.test.utils import override_settings from django.conf import settings -from django.core.urlresolvers import reverse from path import path import copy -from json import loads from django.contrib.auth.models import User -from auth.authz import add_user_to_creator_group - from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.modulestore import Location, mongo +from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.contentstore.django import contentstore, _CONTENTSTORE +from xmodule.contentstore.django import contentstore from xmodule.modulestore.xml_importer import import_from_xml from xmodule.contentstore.content import StaticContent @@ -36,11 +30,20 @@ TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex class MongoCollectionFindWrapper(object): + ''' + MongoCollectionFindWrapper for testing. + ''' def __init__(self, original): + """ + intit func + """ self.original = original self.counter = 0 def find(self, query, *args, **kwargs): + """ + find func + """ self.counter = self.counter + 1 return self.original(query, *args, **kwargs) @@ -76,7 +79,7 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): def load_test_import_course(self): ''' - Load the standard course used to test imports (for do_import_static=False behavior). + Load the standard course used to test imports (for do_import_static=False behavior). ''' content_store = contentstore() module_store = modulestore('direct') @@ -87,12 +90,11 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): return module_store, content_store, course, course_location - def test_static_import(self): ''' Stuff in static_import should always be imported into contentstore ''' - module_store, content_store, course, course_location = self.load_test_import_course() + _, content_store, course, course_location = self.load_test_import_course() # make sure we have ONE asset in our contentstore ("should_be_imported.html") all_assets = content_store.get_all_content_for_course(course_location) @@ -107,12 +109,11 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): pass self.assertIsNotNone(content) - + # make sure course.lms.static_asset_path is correct print "static_asset_path = {0}".format(course.lms.static_asset_path) self.assertEqual(course.lms.static_asset_path, 'test_import_course') - def test_asset_import_nostatic(self): ''' This test validates that an image asset is NOT imported when do_import_static=False @@ -123,14 +124,13 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True) course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') - course = module_store.get_item(course_location) + module_store.get_item(course_location) # make sure we have NO assets in our contentstore all_assets = content_store.get_all_content_for_course(course_location) print "len(all_assets)=%d" % len(all_assets) self.assertEqual(len(all_assets), 0) - def test_no_static_link_rewrites_on_import(self): module_store = modulestore('direct') import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True) @@ -140,4 +140,3 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None])) self.assertIn('/static/', handouts.data) - diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 56c37fe04d..d20bf264aa 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -174,7 +174,6 @@ def import_from_xml(store, data_dir, course_dirs=None, import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, _namespace_rename, subpath=simport, verbose=verbose) - # finally loop through all the modules for module in xml_module_store.modules[course_id].itervalues(): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 85fa6d9be8..bf8d52da6f 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -83,7 +83,7 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): # See if the url got rewritten to the target link # note if the URL mapping changes then this assertion will break - self.assertIn('/courses/'+self.course_id+'/jump_to_id/vertical_test', html) + self.assertIn('/courses/' + self.course_id + '/jump_to_id/vertical_test', html) def test_modx_dispatch(self): self.assertRaises(Http404, render.modx_dispatch, 'dummy', 'dummy', @@ -139,7 +139,6 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): self.course_id ) - def test_xqueue_callback_success(self): """ Test for happy-path xqueue_callback @@ -356,10 +355,9 @@ class TestHtmlModifiers(ModuleStoreTestCase): result_fragment.content ) - def test_static_asset_path_use(self): ''' - when a course is loaded with do_import_static=False (see xml_importer.py), then + when a course is loaded with do_import_static=False (see xml_importer.py), then static_asset_path is set as an lms kv in course. That should make static paths not be mangled (ie not changed to c4x://). ''' @@ -374,7 +372,6 @@ class TestHtmlModifiers(ModuleStoreTestCase): result_fragment = module.runtime.render(module, None, 'student_view') self.assertIn('href="/static/toy_course_dir', result_fragment.content) - def test_course_image(self): url = course_image_url(self.course) self.assertTrue(url.startswith('/c4x/')) @@ -384,14 +381,12 @@ class TestHtmlModifiers(ModuleStoreTestCase): self.assertTrue(url.startswith('/static/toy_course_dir/')) self.course.lms.static_asset_path = "" - def test_get_course_info_section(self): self.course.lms.static_asset_path = "toy_course_dir" - handouts = get_course_info_section(self.request, self.course, "handouts") + get_course_info_section(self.request, self.course, "handouts") # TODO: check handouts output...right now test course seems to have no such content # at least this makes sure get_course_info_section returns without exception - def test_course_link_rewrite(self): module = render.get_module( self.user, From 21f4b058133934465e1d5d983a622eac23d67b25 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 14:46:38 -0400 Subject: [PATCH 21/59] fix some gaps which would allow the temporary xml attributes 'parent_sequential_url' and 'index_in_children_list' to get persisted in the database, whereas they are meant to be only scoped during export/import --- .../contentstore/tests/test_contentstore.py | 15 ++++++++++++++ .../xmodule/modulestore/xml_importer.py | 20 ++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..1cadcd69bf 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -945,8 +945,23 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 'vertical', 'vertical_test', None]), depth=1) self.assertTrue(getattr(vertical, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + if hasattr(vertical, 'data'): + self.assertNotIn('index_in_children_list', vertical.data) + self.assertNotIn('parent_sequential_url', vertical.xml_attributes) + if hasattr(vertical, 'data'): + self.assertNotIn('parent_sequential_url', vertical.data) + for child in vertical.get_children(): self.assertTrue(getattr(child, 'is_draft', False)) + self.assertNotIn('index_in_children_list', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('index_in_children_list', child.data) + self.assertNotIn('parent_sequential_url', child.xml_attributes) + if hasattr(child, 'data'): + self.assertNotIn('parent_sequential_url', child.data) + + # make sure that we don't have a sequential that is in draft mode sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy', diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 7bea0fdcac..109d759693 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -212,6 +212,15 @@ def import_module(module, store, course_data_path, static_content_store, # NOTE: It's important to use own_metadata here to avoid writing # inherited metadata everywhere. + + # remove any export/import only xml_attributes which are used to wire together draft imports + if 'parent_sequential_url' in module.xml_attributes: + del module.xml_attributes['parent_sequential_url'] + + if 'index_in_children_list' in module.xml_attributes: + del module.xml_attributes['index_in_children_list'] + module.save() + store.update_metadata(module.location, dict(own_metadata(module))) @@ -281,7 +290,7 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, # this is to make sure private only verticals show up in the list of children since # they would have been filtered out from the non-draft store export if module.location.category == 'vertical': - module.location = module.location._replace(revision=None) + non_draft_location = module.location._replace(revision=None) sequential_url = module.xml_attributes['parent_sequential_url'] index = int(module.xml_attributes['index_in_children_list']) @@ -291,15 +300,12 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, seq_location = seq_location._replace(org=target_location_namespace.org, course=target_location_namespace.course ) - sequential = store.get_item(seq_location) + sequential = store.get_item(seq_location, depth=0) - if module.location.url() not in sequential.children: - sequential.children.insert(index, module.location.url()) + if non_draft_location.url() not in sequential.children: + sequential.children.insert(index, non_draft_location.url()) store.update_children(sequential.location, sequential.children) - del module.xml_attributes['parent_sequential_url'] - del module.xml_attributes['index_in_children_list'] - import_module(module, draft_store, course_data_path, static_content_store, source_location_namespace, target_location_namespace, allow_not_found=True) for child in module.get_children(): From 81151ab67a0afc082a43a3faf6e997dc1bcb6717 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 8 Aug 2013 13:24:07 -0400 Subject: [PATCH 22/59] Prototype for running acceptance tests via Sauce Labs Connector Added in more desired capabilities and fixed the browser creation Sauce Labs is now updated if the test suite passes or fails --- common/djangoapps/terrain/browser.py | 52 +++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index c2bf2bbbf3..5ae1ca007f 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -11,6 +11,7 @@ from logging import getLogger from django.core.management import call_command from django.conf import settings from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -41,13 +42,45 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 +# https://gist.github.com/santiycr/1644439 +import httplib +import base64 +try: + import json +except ImportError: + import simplejson as json + +config = {"username": "", +"access-key": ""} +desired_capabilities = DesiredCapabilities.CHROME +desired_capabilities['platform'] = "Linux" +desired_capabilities['version'] = "" +desired_capabilities['name'] = "Fail Test" +desired_capabilities['passed'] = True +desired_capabilities['video-upload-on-pass'] = False +desired_capabilities['record-screenshots'] = False +desired_capabilities['selenium-version'] = "2.33.0" +desired_capabilities['max-duration'] = 3600 +jobid='' + +base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] + +def set_job_status(jobid, passed=True): + body_content = json.dumps({"passed": passed}) + connection = httplib.HTTPConnection("saucelabs.com") + connection.request('PUT', '/rest/v1/%s/jobs/%s' % (config['username'], jobid), + body_content, + headers={"Authorization": "Basic %s" % base64string}) + result = connection.getresponse() + return result.status == 200 + @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ - browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + #browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. @@ -57,8 +90,15 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - world.browser = Browser(browser_driver) - + # world.browser = Browser(browser_driver) + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + **desired_capabilities + ) + world.browser.driver.implicitly_wait(30) + global jobid + jobid = world.browser.driver.session_id # Try to visit the main page # If the browser session is invalid, this will # raise a WebDriverException @@ -75,10 +115,10 @@ def initial_setup(server): # If we were unable to get a valid session within the limit of attempts, # then we cannot run the tests. if not success: - raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver)) + raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - world.browser.driver.set_window_size(1280, 1024) + # world.browser.driver.set_window_size(1280, 1024) @before.each_scenario @@ -128,4 +168,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ + if total.scenarios_ran != total.scenarios_passed: + set_job_status(jobid, False) world.browser.quit() From eb7fe7c92712f0457d8cd96da5b0d9e1443de347 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 8 Aug 2013 16:13:07 -0400 Subject: [PATCH 23/59] Added browser matrix (might not display yet) Added build information. Status image will now display properly --- README.md | 4 ++++ common/djangoapps/terrain/browser.py | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0261f87b46..9439d27799 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ + + Selenium Tests Status + + This is the main edX platform which consists of LMS and Studio. See [code.edx.org](http://code.edx.org/) for other parts of the edX code base. diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 5ae1ca007f..69c970fc15 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,17 +50,20 @@ try: except ImportError: import simplejson as json -config = {"username": "", -"access-key": ""} +config = {"username": "", +"access-key": ""} desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" -desired_capabilities['name'] = "Fail Test" +desired_capabilities['name'] = "LMS Lettuce Test" +desired_capabilities['build'] = "Alpha-Beta-123" desired_capabilities['passed'] = True +desired_capabilities['record-video'] = False desired_capabilities['video-upload-on-pass'] = False desired_capabilities['record-screenshots'] = False desired_capabilities['selenium-version'] = "2.33.0" desired_capabilities['max-duration'] = 3600 +desired_capabilities['public'] = 'public restricted' jobid='' base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] From 876651009e3fc98fd44b03923b04f6af9ea8fab9 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 9 Aug 2013 11:01:44 -0400 Subject: [PATCH 24/59] Logging out no longer uses cookies. Removed other cookie reference --- cms/djangoapps/contentstore/features/course-team.py | 3 ++- common/djangoapps/terrain/browser.py | 2 +- common/djangoapps/terrain/steps.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index db7b4d81f9..ab68050866 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,6 +2,7 @@ #pylint: disable=W0621 from lettuce import world, step +from lettuce.django import django_url from common import create_studio_user from django.contrib.auth.models import Group from auth.authz import get_course_groupname_for_role, get_user_by_email @@ -91,7 +92,7 @@ def remove_course_team_admin(_step, outer_capture, name): @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): - world.browser.cookies.delete() + world.visit(django_url('logout')) world.visit('/') signin_css = 'a.action-signin' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 69c970fc15..680885fec4 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -83,7 +83,7 @@ def initial_setup(server): """ Launch the browser once before executing the tests. """ - #browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + # browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 9cf2aeda49..6e11ed19ea 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -99,7 +99,7 @@ def i_am_logged_in_user(step): @step('I am not logged in$') def i_am_not_logged_in(step): - world.browser.cookies.delete() + world.visit(django_url('logout')) @step('I am staff for course "([^"]*)"$') @@ -150,7 +150,7 @@ def i_am_logged_in(step): world.log_in(username='robot', password='test') world.browser.visit(django_url('/')) # You should not see the login link - assert_equals(world.browser.find_by_css('a#login'), []) + world.is_css_not_present('a#login') @step(u'I am an edX user$') From f4c19919cd0f8727f3d6c51c240a5cf13939f1a7 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:49:34 -0400 Subject: [PATCH 25/59] Beginnings of a feature flag Conflicts: common/djangoapps/terrain/browser.py Changed way feature flag was checked Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 2 ++ common/djangoapps/terrain/browser.py | 30 +++++++++++++++++----------- lms/envs/acceptance.py | 2 ++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 7debfe18d1..76a15daa65 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -81,6 +81,8 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +MITX_FEATURES['USE_SAUCE'] = False + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 680885fec4..7a1aff2637 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -20,6 +20,8 @@ from lms import one_time_startup # pylint: disable=W0611 from cms import one_time_startup # pylint: disable=W0611 from pymongo import MongoClient import xmodule.modulestore.django +import datetime +from pytz import UTC from xmodule.contentstore.django import _CONTENTSTORE # There is an import issue when using django-staticfiles with lettuce @@ -55,11 +57,11 @@ config = {"username": "", desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" -desired_capabilities['name'] = "LMS Lettuce Test" -desired_capabilities['build'] = "Alpha-Beta-123" +desired_capabilities['name'] = "CMS Lettuce Test" +desired_capabilities['build'] = datetime.datetime.now(UTC).isoformat(' ') desired_capabilities['passed'] = True -desired_capabilities['record-video'] = False desired_capabilities['video-upload-on-pass'] = False +desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False desired_capabilities['selenium-version'] = "2.33.0" desired_capabilities['max-duration'] = 3600 @@ -83,7 +85,7 @@ def initial_setup(server): """ Launch the browser once before executing the tests. """ - # browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu # in which we sometimes get an invalid browser session. @@ -93,12 +95,15 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - # world.browser = Browser(browser_driver) - world.browser = Browser( - 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), - **desired_capabilities - ) + if settings.MITX_FEATURES.get('USE_SAUCE'): + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + **desired_capabilities + ) + else: + world.browser = Browser(browser_driver) + world.browser.driver.implicitly_wait(30) global jobid jobid = world.browser.driver.session_id @@ -121,7 +126,8 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - # world.browser.driver.set_window_size(1280, 1024) + if not settings.MITX_FEATURES.get('USE_SAUCE'): + world.browser.driver.set_window_size(1280, 1024) @before.each_scenario @@ -171,6 +177,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if total.scenarios_ran != total.scenarios_passed: + if settings.MITX_FEATURES.get('USE_SAUCE') and total.scenarios_ran != total.scenarios_passed: set_job_status(jobid, False) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 1e188d3b45..b86ec56aae 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -90,6 +90,8 @@ USE_I18N = True MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' +MITX_FEATURES['USE_SAUCE'] = False + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) From 28d9bbaff2d141412daaaa3bda5f191a46329810 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 14:13:11 -0400 Subject: [PATCH 26/59] Change build name generation Browser Matrix Data only applies to latest build information --- common/djangoapps/terrain/browser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 7a1aff2637..37fde93b55 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -20,8 +20,6 @@ from lms import one_time_startup # pylint: disable=W0611 from cms import one_time_startup # pylint: disable=W0611 from pymongo import MongoClient import xmodule.modulestore.django -import datetime -from pytz import UTC from xmodule.contentstore.django import _CONTENTSTORE # There is an import issue when using django-staticfiles with lettuce @@ -58,8 +56,7 @@ desired_capabilities = DesiredCapabilities.CHROME desired_capabilities['platform'] = "Linux" desired_capabilities['version'] = "" desired_capabilities['name'] = "CMS Lettuce Test" -desired_capabilities['build'] = datetime.datetime.now(UTC).isoformat(' ') -desired_capabilities['passed'] = True +desired_capabilities['build'] = "Branch Test" desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False From 010fd5771d7114bd032c514ac053540cffb35084 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:50:28 -0400 Subject: [PATCH 27/59] Pass/Fail status updated properly Conflicts: common/djangoapps/terrain/browser.py --- common/djangoapps/terrain/browser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 37fde93b55..52ed8d1510 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -174,6 +174,9 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if settings.MITX_FEATURES.get('USE_SAUCE') and total.scenarios_ran != total.scenarios_passed: - set_job_status(jobid, False) + if settings.MITX_FEATURES.get('USE_SAUCE'): + if total.scenarios_ran != total.scenarios_passed: + set_job_status(jobid, False) + else: + set_job_status(jobid, True) world.browser.quit() From 1d7284b7cdd3beaed31309e864505d56a84c5671 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 15:13:30 -0400 Subject: [PATCH 28/59] Refactored out more for feature flags Refactored more for feature flags --- cms/envs/acceptance.py | 9 +++++++++ common/djangoapps/terrain/browser.py | 16 +++++++++------- lms/envs/acceptance.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 76a15daa65..0404d0005b 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -82,6 +83,14 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True MITX_FEATURES['USE_SAUCE'] = False +MITX_FEATURES['SAUCE_USERNAME'] = '' +MITX_FEATURES['SAUCE_ACCESS_ID'] = '' +MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME +MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' +MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' +MITX_FEATURES['SAUCE_TAGS'] = '' + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 52ed8d1510..22aefc90bd 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,13 +50,15 @@ try: except ImportError: import simplejson as json -config = {"username": "", -"access-key": ""} -desired_capabilities = DesiredCapabilities.CHROME -desired_capabilities['platform'] = "Linux" -desired_capabilities['version'] = "" -desired_capabilities['name'] = "CMS Lettuce Test" -desired_capabilities['build'] = "Branch Test" +config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), +"access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} + +desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) +desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') +desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') +desired_capabilities['name'] = "Lettuce Test" +desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') +desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index b86ec56aae..f30c22a486 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -9,6 +9,8 @@ so that we can run the lettuce acceptance tests. from .test import * +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly DEBUG = True @@ -91,6 +93,14 @@ MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' MITX_FEATURES['USE_SAUCE'] = False +MITX_FEATURES['SAUCE_USERNAME'] = '' +MITX_FEATURES['SAUCE_ACCESS_ID'] = '' +MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME +MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' +MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' +MITX_FEATURES['SAUCE_TAGS'] = '' + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) From 97df5aa997381c8996293f8a812f0d5fc924a3d0 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 12 Aug 2013 16:20:09 -0400 Subject: [PATCH 29/59] Added feature flag for device type. Needed for android tablets --- cms/envs/acceptance.py | 1 + common/djangoapps/terrain/browser.py | 1 + lms/envs/acceptance.py | 1 + 3 files changed, 3 insertions(+) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 0404d0005b..7b91a7570e 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -88,6 +88,7 @@ MITX_FEATURES['SAUCE_ACCESS_ID'] = '' MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_DEVICE'] = '' MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' MITX_FEATURES['SAUCE_TAGS'] = '' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 22aefc90bd..204bd96b8a 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -56,6 +56,7 @@ config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') +desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') desired_capabilities['name'] = "Lettuce Test" desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index f30c22a486..17c769648a 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -98,6 +98,7 @@ MITX_FEATURES['SAUCE_ACCESS_ID'] = '' MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' +MITX_FEATURES['SAUCE_DEVICE'] = '' MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' MITX_FEATURES['SAUCE_TAGS'] = '' From 3746c654d8e78577674ab56127b81cf53113bbd3 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 10:06:53 -0400 Subject: [PATCH 30/59] Added a tag for things that will not work on Firefox Added tags for tests that will not work on Sauce Changed build name Tightened up logic Conflicts: common/djangoapps/terrain/browser.py Added flag for session name --- .../contentstore/features/advanced-settings.feature | 8 ++++++++ cms/djangoapps/contentstore/features/checklists.feature | 2 ++ cms/djangoapps/contentstore/features/video-editor.feature | 4 ++++ cms/envs/acceptance.py | 3 ++- common/djangoapps/terrain/browser.py | 7 ++----- lms/djangoapps/courseware/features/login.feature | 1 + lms/djangoapps/courseware/features/signup.feature | 1 + lms/djangoapps/courseware/features/video.feature | 4 +++- lms/envs/acceptance.py | 3 ++- 9 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index a11a6cb869..2f0d396e63 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -2,6 +2,8 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists I want to be able to manually enter JSON key /value pairs +#Sauce labs does not play nicely with CodeMirror + Scenario: A course author sees default advanced settings Given I have opened a new course in Studio When I select the Advanced Settings @@ -11,6 +13,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + @Sauce Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key @@ -19,6 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + @Sauce Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key and save @@ -26,6 +30,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + @Sauce Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "discussion_topics" @@ -33,6 +38,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + @Sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "display_name" @@ -41,6 +47,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window + @Sauce Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio When I create a non-JSON value not in quotes @@ -48,6 +55,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string + @Sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index f13ce53fc2..72cff726f4 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -11,6 +11,7 @@ Feature: Course checklists And They are correctly selected after reloading the page # CHROME ONLY, due to issues getting link to be active in firefox + @Firefox Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -19,6 +20,7 @@ Feature: Course checklists Then I am brought back to the course outline in the correct state # CHROME ONLY, due to issues getting link to be active in firefox + @Firefox Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index a53183e37c..d75f21e9c0 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -1,6 +1,8 @@ Feature: Video Component Editor As a course author, I want to be able to create video components. + #Sauce Labs cannot delete cookies + Scenario: User can view Video metadata Given I have created a Video component And I edit the component @@ -12,11 +14,13 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save + @Sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions + @Sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component And I have set "show captions" to True diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 7b91a7570e..c962ec5560 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -89,7 +89,8 @@ MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_BUILD'] = 'Feature Test' +MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' +MITX_FEATURES['SAUCE_BUILD'] = 'CMS TESTS' MITX_FEATURES['SAUCE_TAGS'] = '' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 204bd96b8a..9aff0f9999 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -57,7 +57,7 @@ desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapab desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') -desired_capabilities['name'] = "Lettuce Test" +desired_capabilities['name'] = settings.MITX_FEATURES.get('SAUCE_SESSION', 'Lettuce Tests') desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') desired_capabilities['video-upload-on-pass'] = False @@ -178,8 +178,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if settings.MITX_FEATURES.get('USE_SAUCE'): - if total.scenarios_ran != total.scenarios_passed: - set_job_status(jobid, False) - else: - set_job_status(jobid, True) + set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 2b90c56f2d..28cba2e874 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -12,6 +12,7 @@ Feature: Login in as a registered user Then I should see the login error message "This account has not been activated" # CHROME ONLY, firefox will not redirect properly + @Firefox Scenario: Login to an activated account Given I am an edX user And I am an activated user diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index 19dfd74f1c..e723071fd5 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -4,6 +4,7 @@ Feature: Sign in I want to signup for a student account # CHROME ONLY, firefox will not redirect properly + @Firefox Scenario: Sign up from the homepage Given I visit the homepage When I click the link with the text "Register Now" diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 74cd9cbcbb..e68e8b1ada 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -11,6 +11,8 @@ Feature: Video component Given the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode + #Firefox doesn't have HTML5 + @Firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode - Then when I view the video it has autoplay enabled \ No newline at end of file + Then when I view the video it has autoplay enabled diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 17c769648a..e6cac76312 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -99,7 +99,8 @@ MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' MITX_FEATURES['SAUCE_VERSION'] = '' MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_BUILD'] = 'edX Platform' +MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' +MITX_FEATURES['SAUCE_BUILD'] = 'LMS TESTS' MITX_FEATURES['SAUCE_TAGS'] = '' From 682d85c2f307e3f62b7e3cdb5ecb599a8a305cba Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:51:08 -0400 Subject: [PATCH 31/59] Changed feature to being one dictionary Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 24 ++++++++++++------------ common/djangoapps/terrain/browser.py | 21 +++++++++++---------- lms/envs/acceptance.py | 26 ++++++++++++-------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index c962ec5560..54407a8c2f 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -76,23 +76,23 @@ DATABASES = { # Use the auto_auth workflow for creating users and logging them in MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True - # HACK # Setting this flag to false causes imports to not load correctly in the lettuce python files # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -MITX_FEATURES['USE_SAUCE'] = False -MITX_FEATURES['SAUCE_USERNAME'] = '' -MITX_FEATURES['SAUCE_ACCESS_ID'] = '' -MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME -MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' -MITX_FEATURES['SAUCE_VERSION'] = '' -MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' -MITX_FEATURES['SAUCE_BUILD'] = 'CMS TESTS' -MITX_FEATURES['SAUCE_TAGS'] = '' - +MITX_FEATURES['SAUCE'] = { + 'USE' : False, + 'USERNAME' : '', + 'ACCESS_ID' : '', + 'BROWSER' : DesiredCapabilities.CHROME, + 'PLATFORM' : 'Linux', + 'VERSION' : '', + 'DEVICE' : '', + 'SESSION' : 'Lettuce Tests', + 'BUILD' : 'CMS TESTS', + 'TAGS' : '' +} # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 9aff0f9999..2757a2fa79 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -53,13 +53,14 @@ except ImportError: config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), "access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} -desired_capabilities = settings.MITX_FEATURES.get('SAUCE_BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = settings.MITX_FEATURES.get('SAUCE_PLATFORM', 'Linux') -desired_capabilities['version'] = settings.MITX_FEATURES.get('SAUCE_VERSION', '') -desired_capabilities['device-type'] = settings.MITX_FEATURES.get('SAUCE_DEVICE', '') -desired_capabilities['name'] = settings.MITX_FEATURES.get('SAUCE_SESSION', 'Lettuce Tests') -desired_capabilities['build'] = settings.MITX_FEATURES.get('SAUCE_BUILD', 'edX Plaform') -desired_capabilities['tags'] = settings.MITX_FEATURES.get('SAUCE_TAGS', '') +SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) +desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) +desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') +desired_capabilities['version'] = SAUCE.get('VERSION', '') +desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') +desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') +desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') +desired_capabilities['tags'] = SAUCE.get('TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -95,7 +96,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if settings.MITX_FEATURES.get('USE_SAUCE'): + if SAUCE.get('USE'): world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -126,7 +127,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not settings.MITX_FEATURES.get('USE_SAUCE'): + if not SAUCE.get('USE'): world.browser.driver.set_window_size(1280, 1024) @@ -177,6 +178,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if settings.MITX_FEATURES.get('USE_SAUCE'): + if SAUCE.get('USE'): set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e6cac76312..3689b0a18d 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -89,20 +89,18 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -MITX_FEATURES['ENABLE_FEEDBACK_SUBMISSION'] = True -FEEDBACK_SUBMISSION_EMAIL = 'dummy@example.com' - -MITX_FEATURES['USE_SAUCE'] = False -MITX_FEATURES['SAUCE_USERNAME'] = '' -MITX_FEATURES['SAUCE_ACCESS_ID'] = '' -MITX_FEATURES['SAUCE_BROWSER'] = DesiredCapabilities.CHROME -MITX_FEATURES['SAUCE_PLATFORM'] = 'Linux' -MITX_FEATURES['SAUCE_VERSION'] = '' -MITX_FEATURES['SAUCE_DEVICE'] = '' -MITX_FEATURES['SAUCE_SESSION'] = 'Lettuce Tests' -MITX_FEATURES['SAUCE_BUILD'] = 'LMS TESTS' -MITX_FEATURES['SAUCE_TAGS'] = '' - +MITX_FEATURES['SAUCE'] = { + 'USE' : False, + 'USERNAME' : '', + 'ACCESS_ID' : '', + 'BROWSER' : DesiredCapabilities.CHROME, + 'PLATFORM' : 'Linux', + 'VERSION' : '', + 'DEVICE' : '', + 'SESSION' : 'Lettuce Tests', + 'BUILD' : 'CMS TESTS', + 'TAGS' : '' +} # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) From 2812b9cd5c5ad2834518f72a76b40ea63d542044 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 11:36:45 -0400 Subject: [PATCH 32/59] Fixed how config was obtained Added comment about the feature --- cms/envs/acceptance.py | 1 + common/djangoapps/terrain/browser.py | 6 ++++-- lms/envs/acceptance.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 54407a8c2f..94d6d7697a 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -81,6 +81,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +# Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { 'USE' : False, 'USERNAME' : '', diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 2757a2fa79..b92354e755 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -50,10 +50,12 @@ try: except ImportError: import simplejson as json -config = {"username": settings.MITX_FEATURES.get('SAUCE_USERNAME'), -"access-key": settings.MITX_FEATURES.get('SAUCE_ACCESS_ID')} SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) + +config = {"username": SAUCE.get('USERNAME'), +"access-key": SAUCE.get('ACCESS_ID')} + desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') desired_capabilities['version'] = SAUCE.get('VERSION', '') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 3689b0a18d..e9b7163d24 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -89,6 +89,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +# Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { 'USE' : False, 'USERNAME' : '', @@ -98,7 +99,7 @@ MITX_FEATURES['SAUCE'] = { 'VERSION' : '', 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'BUILD' : 'LMS TESTS', 'TAGS' : '' } From 3cf8083717419746de31a5960c52eb572cc0dc9b Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 11:53:26 -0400 Subject: [PATCH 33/59] Upgraded selenium version for sauce --- common/djangoapps/terrain/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index b92354e755..9b4885e160 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -66,7 +66,7 @@ desired_capabilities['tags'] = SAUCE.get('TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False -desired_capabilities['selenium-version'] = "2.33.0" +desired_capabilities['selenium-version'] = "2.34.0" desired_capabilities['max-duration'] = 3600 desired_capabilities['public'] = 'public restricted' jobid='' From 9fb0529036058a2ef1370cdf8b6e82e5e928d4bc Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 13 Aug 2013 14:03:31 -0400 Subject: [PATCH 34/59] Sauce can only connect on certain ports --- cms/envs/acceptance.py | 11 ++++++++--- common/djangoapps/terrain/browser.py | 7 ++++--- lms/envs/acceptance.py | 16 ++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 94d6d7697a..24a5a02533 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -18,7 +18,12 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -import random +from random import choice +PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] def seed(): @@ -92,11 +97,11 @@ MITX_FEATURES['SAUCE'] = { 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', 'BUILD' : 'CMS TESTS', - 'TAGS' : '' + 'CUSTOM_TAGS' : {} } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) -LETTUCE_SERVER_PORT = random.randint(1024, 65535) +LETTUCE_SERVER_PORT = choice(PORTS) LETTUCE_BROWSER = 'chrome' diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 9b4885e160..62e9cde9c0 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -62,7 +62,7 @@ desired_capabilities['version'] = SAUCE.get('VERSION', '') desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') -desired_capabilities['tags'] = SAUCE.get('TAGS', '') +desired_capabilities['custom-data'] = SAUCE.get('CUSTOM_TAGS', '') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -104,12 +104,13 @@ def initial_setup(server): url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), **desired_capabilities ) + global jobid + jobid = world.browser.driver.session_id else: world.browser = Browser(browser_driver) world.browser.driver.implicitly_wait(30) - global jobid - jobid = world.browser.driver.session_id + # Try to visit the main page # If the browser session is invalid, this will # raise a WebDriverException diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e9b7163d24..f908c31bdc 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -19,8 +19,12 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -import random - +from random import choice +PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] def seed(): return os.getppid() @@ -67,7 +71,7 @@ DATABASES = { # Set up XQueue information so that the lms will send # requests to a mock XQueue server running locally -XQUEUE_PORT = random.randint(1024, 65535) +XQUEUE_PORT = choice(PORTS) XQUEUE_INTERFACE = { "url": "http://127.0.0.1:%d" % XQUEUE_PORT, "django_auth": { @@ -99,12 +103,12 @@ MITX_FEATURES['SAUCE'] = { 'VERSION' : '', 'DEVICE' : '', 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'LMS TESTS', - 'TAGS' : '' + 'BUILD' : 'CMS TESTS', + 'CUSTOM_TAGS' : {} } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) -LETTUCE_SERVER_PORT = random.randint(1024, 65535) +LETTUCE_SERVER_PORT = choice(PORTS) LETTUCE_BROWSER = 'chrome' From ee23b9d16180b1b56564b67c592563f7486c1e41 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 16:51:24 -0400 Subject: [PATCH 35/59] Changed method of obtaining feature flags to environment variables Conflicts: common/djangoapps/terrain/browser.py --- cms/envs/acceptance.py | 28 ++++++++++++++++++--------- common/djangoapps/terrain/browser.py | 6 +++--- lms/envs/acceptance.py | 29 +++++++++++++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 24a5a02533..f503bbd043 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -25,6 +25,16 @@ PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] +DESIRED_CAPABILITIES={ + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} def seed(): return os.getppid() @@ -88,15 +98,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'USE' : False, - 'USERNAME' : '', - 'ACCESS_ID' : '', - 'BROWSER' : DesiredCapabilities.CHROME, - 'PLATFORM' : 'Linux', - 'VERSION' : '', - 'DEVICE' : '', - 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'ENABLED' : os.environ.get('ENABLED'), + 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), + 'VERSION' : os.environ.get('SAUCE_VERSION'), + 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'SESSION' : 'Jenkins Acceptance Tests', + 'BUILD' : os.environ.get('JOB_NAME'), 'CUSTOM_TAGS' : {} } diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 62e9cde9c0..223418972d 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -98,7 +98,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if SAUCE.get('USE'): + if SAUCE.get('ENABLED'): world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -130,7 +130,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not SAUCE.get('USE'): + if not SAUCE.get('ENABLED'): world.browser.driver.set_window_size(1280, 1024) @@ -181,6 +181,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if SAUCE.get('USE'): + if SAUCE.get('ENABLED'): set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index f908c31bdc..951ddf8418 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -26,6 +26,17 @@ PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] +DESIRED_CAPABILITIES={ + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} + def seed(): return os.getppid() @@ -95,15 +106,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'USE' : False, - 'USERNAME' : '', - 'ACCESS_ID' : '', - 'BROWSER' : DesiredCapabilities.CHROME, - 'PLATFORM' : 'Linux', - 'VERSION' : '', - 'DEVICE' : '', - 'SESSION' : 'Lettuce Tests', - 'BUILD' : 'CMS TESTS', + 'ENABLED' : os.environ.get('ENABLED'), + 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), + 'VERSION' : os.environ.get('SAUCE_VERSION'), + 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'SESSION' : 'Jenkins Acceptance Tests', + 'BUILD' : os.environ.get('JOB_NAME'), 'CUSTOM_TAGS' : {} } From d422cb9206c007bd012324fcd6e18f23a53cad6c Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 14:47:30 -0400 Subject: [PATCH 36/59] Fixed desired_capabilities issues Also, now lettuce browser can be specified by jenkins --- cms/envs/acceptance.py | 4 ++-- lms/envs/acceptance.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index f503bbd043..37107c7f70 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -101,7 +101,7 @@ MITX_FEATURES['SAUCE'] = { 'ENABLED' : os.environ.get('ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), 'VERSION' : os.environ.get('SAUCE_VERSION'), 'DEVICE' : os.environ.get('SAUCE_DEVICE'), @@ -114,4 +114,4 @@ MITX_FEATURES['SAUCE'] = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) LETTUCE_SERVER_PORT = choice(PORTS) -LETTUCE_BROWSER = 'chrome' +LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 951ddf8418..6de7861d71 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -109,7 +109,7 @@ MITX_FEATURES['SAUCE'] = { 'ENABLED' : os.environ.get('ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER')), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), 'VERSION' : os.environ.get('SAUCE_VERSION'), 'DEVICE' : os.environ.get('SAUCE_DEVICE'), @@ -122,4 +122,4 @@ MITX_FEATURES['SAUCE'] = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) LETTUCE_SERVER_PORT = choice(PORTS) -LETTUCE_BROWSER = 'chrome' +LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') From 027cbfd09a85001bc20023942c86e8458c72085a Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 15:15:34 -0400 Subject: [PATCH 37/59] Added logic to test_acceptance.sh so that the proper tests will be skipped for each browser This logic also covers if a lettuce_browser is specified Removing browser matrix from readme --- README.md | 4 ---- cms/envs/acceptance.py | 2 +- jenkins/test_acceptance.sh | 12 ++++++++++-- lms/envs/acceptance.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9439d27799..0261f87b46 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ - - Selenium Tests Status - - This is the main edX platform which consists of LMS and Studio. See [code.edx.org](http://code.edx.org/) for other parts of the edX code base. diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 37107c7f70..335a027c7c 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -19,7 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, diff --git a/jenkins/test_acceptance.sh b/jenkins/test_acceptance.sh index b7a244fe99..1bc88744bf 100755 --- a/jenkins/test_acceptance.sh +++ b/jenkins/test_acceptance.sh @@ -30,10 +30,18 @@ TESTS_FAILED=0 # /usr/bin/Xvfb :1 -screen 0 1024x268x24 # This allows us to run Chrome without a display export DISPLAY=:1 +SKIP_TESTS="" + +if [ ! -z ${LETTUCE_BROWSER+x} ]; then + SKIP_TESTS="--tag -$(tr '[:lower:]' '[:upper:]' <<< ${LETTUCE_BROWSER:0:1})${LETTUCE_BROWSER:1}" +fi +if [ ! -z ${SAUCE_ENABLED+x} ]; then + SKIP_TESTS="--tag -Sauce --tag -$(tr '[:lower:]' '[:upper:]' <<< ${SAUCE_BROWSER:0:1})${SAUCE_BROWSER:1}" +fi # Run the lms and cms acceptance tests # (the -v flag turns off color in the output) -rake test_acceptance_lms["-v 3"] || TESTS_FAILED=1 -rake test_acceptance_cms["-v 3"] || TESTS_FAILED=1 +rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1 +rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1 [ $TESTS_FAILED == '0' ] diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 6de7861d71..60c5d04997 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -20,7 +20,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, From 58bb6e1ea37bdddb3ed5e21cd9dd966864ec5b04 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Wed, 14 Aug 2013 15:46:12 -0400 Subject: [PATCH 38/59] Changed tags to be lowercase to prevent preprocessing Changed to have a default for device since it isn't always needed Tags are more clear Fixed stylistic issues --- .../features/advanced-settings.feature | 12 +++++------ .../contentstore/features/checklists.feature | 4 ++-- .../features/video-editor.feature | 4 ++-- cms/envs/acceptance.py | 17 ++++++++-------- common/djangoapps/terrain/browser.py | 20 +++++++++---------- jenkins/test_acceptance.sh | 4 ++-- .../courseware/features/login.feature | 2 +- .../courseware/features/signup.feature | 2 +- .../courseware/features/video.feature | 2 +- lms/envs/acceptance.py | 17 ++++++++-------- 10 files changed, 41 insertions(+), 43 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 2f0d396e63..767dafb796 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -13,7 +13,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized - @Sauce + @skip_sauce Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key @@ -22,7 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged - @Sauce + @skip_sauce Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key and save @@ -30,7 +30,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed - @Sauce + @skip_sauce Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "discussion_topics" @@ -38,7 +38,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted - @Sauce + @skip_sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value for "display_name" @@ -47,7 +47,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window - @Sauce + @skip_sauce Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio When I create a non-JSON value not in quotes @@ -55,7 +55,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string - @Sauce + @skip_sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 72cff726f4..28a38b307e 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -11,7 +11,7 @@ Feature: Course checklists And They are correctly selected after reloading the page # CHROME ONLY, due to issues getting link to be active in firefox - @Firefox + @skip_firefox Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -20,7 +20,7 @@ Feature: Course checklists Then I am brought back to the course outline in the correct state # CHROME ONLY, due to issues getting link to be active in firefox - @Firefox + @skip_firefox Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index d75f21e9c0..6f5fbd48b9 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -14,13 +14,13 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save - @Sauce + @skip_sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions - @Sauce + @skip_sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component And I have set "show captions" to True diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 335a027c7c..f51a697f36 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -22,10 +22,10 @@ from random import choice PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES={ +DESIRED_CAPABILITIES = { 'chrome': DesiredCapabilities.CHROME, 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, 'firefox': DesiredCapabilities.FIREFOX, @@ -98,16 +98,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'ENABLED' : os.environ.get('ENABLED'), + 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), - 'VERSION' : os.environ.get('SAUCE_VERSION'), - 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), + 'VERSION' : os.environ.get('SAUCE_VERSION', ''), + 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME'), - 'CUSTOM_TAGS' : {} + 'BUILD' : os.environ.get('JOB_NAME', 'CMS TESTS'), } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 223418972d..15c822e159 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -56,13 +56,13 @@ SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) config = {"username": SAUCE.get('USERNAME'), "access-key": SAUCE.get('ACCESS_ID')} +world.absorb(SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = SAUCE.get('PLATFORM', 'Linux') -desired_capabilities['version'] = SAUCE.get('VERSION', '') -desired_capabilities['device-type'] = SAUCE.get('DEVICE', '') -desired_capabilities['name'] = SAUCE.get('SESSION', 'Lettuce Tests') -desired_capabilities['build'] = SAUCE.get('BUILD', 'edX Plaform') -desired_capabilities['custom-data'] = SAUCE.get('CUSTOM_TAGS', '') +desired_capabilities['platform'] = SAUCE.get('PLATFORM') +desired_capabilities['version'] = SAUCE.get('VERSION') +desired_capabilities['device-type'] = SAUCE.get('DEVICE') +desired_capabilities['name'] = SAUCE.get('SESSION') +desired_capabilities['build'] = SAUCE.get('BUILD') desired_capabilities['video-upload-on-pass'] = False desired_capabilities['sauce-advisor'] = False desired_capabilities['record-screenshots'] = False @@ -71,7 +71,7 @@ desired_capabilities['max-duration'] = 3600 desired_capabilities['public'] = 'public restricted' jobid='' -base64string = base64.encodestring('%s:%s' % (config['username'], config['access-key']))[:-1] +base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) @@ -98,7 +98,7 @@ def initial_setup(server): while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: # Get a browser session - if SAUCE.get('ENABLED'): + if world.SAUCE_ENABLED: world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), @@ -130,7 +130,7 @@ def initial_setup(server): raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) # Set the browser size to 1280x1024 - if not SAUCE.get('ENABLED'): + if not world.SAUCE_ENABLED: world.browser.driver.set_window_size(1280, 1024) @@ -181,6 +181,6 @@ def teardown_browser(total): """ Quit the browser after executing the tests. """ - if SAUCE.get('ENABLED'): + if world.SAUCE_ENABLED: set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/jenkins/test_acceptance.sh b/jenkins/test_acceptance.sh index 1bc88744bf..aaa7cfc3da 100755 --- a/jenkins/test_acceptance.sh +++ b/jenkins/test_acceptance.sh @@ -33,10 +33,10 @@ export DISPLAY=:1 SKIP_TESTS="" if [ ! -z ${LETTUCE_BROWSER+x} ]; then - SKIP_TESTS="--tag -$(tr '[:lower:]' '[:upper:]' <<< ${LETTUCE_BROWSER:0:1})${LETTUCE_BROWSER:1}" + SKIP_TESTS="--tag -skip_$LETTUCE_BROWSER" fi if [ ! -z ${SAUCE_ENABLED+x} ]; then - SKIP_TESTS="--tag -Sauce --tag -$(tr '[:lower:]' '[:upper:]' <<< ${SAUCE_BROWSER:0:1})${SAUCE_BROWSER:1}" + SKIP_TESTS="--tag -skip_sauce --tag -skip_$SAUCE_BROWSER" fi # Run the lms and cms acceptance tests diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 28cba2e874..5c777fd64f 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -12,7 +12,7 @@ Feature: Login in as a registered user Then I should see the login error message "This account has not been activated" # CHROME ONLY, firefox will not redirect properly - @Firefox + @skip_firefox Scenario: Login to an activated account Given I am an edX user And I am an activated user diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index e723071fd5..c1fce04b54 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -4,7 +4,7 @@ Feature: Sign in I want to signup for a student account # CHROME ONLY, firefox will not redirect properly - @Firefox + @skip_firefox Scenario: Sign up from the homepage Given I visit the homepage When I click the link with the text "Register Now" diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index e68e8b1ada..260887290e 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -12,7 +12,7 @@ Feature: Video component Then when I view the video it has rendered in Youtube mode #Firefox doesn't have HTML5 - @Firefox + @skip_firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode Then when I view the video it has autoplay enabled diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 60c5d04997..e4928301d7 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -23,10 +23,10 @@ from random import choice PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES={ +DESIRED_CAPABILITIES = { 'chrome': DesiredCapabilities.CHROME, 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, 'firefox': DesiredCapabilities.FIREFOX, @@ -106,16 +106,15 @@ USE_I18N = True # Information needed to utilize Sauce Labs. MITX_FEATURES['SAUCE'] = { - 'ENABLED' : os.environ.get('ENABLED'), + 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome')), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM'), - 'VERSION' : os.environ.get('SAUCE_VERSION'), - 'DEVICE' : os.environ.get('SAUCE_DEVICE'), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), + 'VERSION' : os.environ.get('SAUCE_VERSION', ''), + 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME'), - 'CUSTOM_TAGS' : {} + 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), } # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command From 0b6e62984e3992f349c3eec7463b619522b28556 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 10:58:18 -0400 Subject: [PATCH 39/59] Using requests instead of a HTTPConnect --- cms/envs/acceptance.py | 2 +- common/djangoapps/terrain/browser.py | 12 +++++------- lms/envs/acceptance.py | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index f51a697f36..493e2e1028 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -101,7 +101,7 @@ MITX_FEATURES['SAUCE'] = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), 'VERSION' : os.environ.get('SAUCE_VERSION', ''), 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 15c822e159..d351f7433a 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -43,7 +43,7 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 # https://gist.github.com/santiycr/1644439 -import httplib +import requests import base64 try: import json @@ -75,12 +75,10 @@ base64string = base64.encodestring('{}:{}'.format(config['username'], config['ac def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) - connection = httplib.HTTPConnection("saucelabs.com") - connection.request('PUT', '/rest/v1/%s/jobs/%s' % (config['username'], jobid), - body_content, - headers={"Authorization": "Basic %s" % base64string}) - result = connection.getresponse() - return result.status == 200 + result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], jobid), + data=body_content, + headers={"Authorization": "Basic {}".format(base64string)}) + return result.status_code == 200 @before.harvest diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e4928301d7..618d52a995 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -109,7 +109,7 @@ MITX_FEATURES['SAUCE'] = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome'), DesiredCapabilities.CHROME), + 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), 'VERSION' : os.environ.get('SAUCE_VERSION', ''), 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), @@ -117,6 +117,7 @@ MITX_FEATURES['SAUCE'] = { 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), } + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) From 35e5f4cab5a00384acad61fa86751f59bf65b626 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 11:39:27 -0400 Subject: [PATCH 40/59] Moved global things in browser.py to functions --- cms/envs/acceptance.py | 2 +- common/djangoapps/terrain/browser.py | 53 ++++++++++++++-------------- lms/envs/acceptance.py | 2 +- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 493e2e1028..8b208123cc 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -97,7 +97,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True # Information needed to utilize Sauce Labs. -MITX_FEATURES['SAUCE'] = { +SAUCE = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index d351f7433a..0068e587b0 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -51,41 +51,40 @@ except ImportError: import simplejson as json -SAUCE = settings.MITX_FEATURES.get('SAUCE', {}) - -config = {"username": SAUCE.get('USERNAME'), -"access-key": SAUCE.get('ACCESS_ID')} - -world.absorb(SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') -desired_capabilities = SAUCE.get('BROWSER', DesiredCapabilities.CHROME) -desired_capabilities['platform'] = SAUCE.get('PLATFORM') -desired_capabilities['version'] = SAUCE.get('VERSION') -desired_capabilities['device-type'] = SAUCE.get('DEVICE') -desired_capabilities['name'] = SAUCE.get('SESSION') -desired_capabilities['build'] = SAUCE.get('BUILD') -desired_capabilities['video-upload-on-pass'] = False -desired_capabilities['sauce-advisor'] = False -desired_capabilities['record-screenshots'] = False -desired_capabilities['selenium-version'] = "2.34.0" -desired_capabilities['max-duration'] = 3600 -desired_capabilities['public'] = 'public restricted' -jobid='' - -base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) - result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], jobid), + config = get_username_and_key() + base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] + result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 +def make_desired_capabilities(): + desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) + desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') + desired_capabilities['version'] = settings.SAUCE.get('VERSION') + desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE') + desired_capabilities['name'] = settings.SAUCE.get('SESSION') + desired_capabilities['build'] = settings.SAUCE.get('BUILD') + desired_capabilities['video-upload-on-pass'] = False + desired_capabilities['sauce-advisor'] = False + desired_capabilities['record-screenshots'] = False + desired_capabilities['selenium-version'] = "2.34.0" + desired_capabilities['max-duration'] = 3600 + desired_capabilities['public'] = 'public restricted' + return desired_capabilities + +def get_username_and_key(): + return {"username": settings.SAUCE.get('USERNAME'),"access-key": settings.SAUCE.get('ACCESS_ID')} + @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ + world.absorb(settings.SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') # There is an issue with ChromeDriver2 r195627 on Ubuntu @@ -97,13 +96,13 @@ def initial_setup(server): # Get a browser session if world.SAUCE_ENABLED: + config = get_username_and_key() world.browser = Browser( 'remote', url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), - **desired_capabilities + **make_desired_capabilities() ) - global jobid - jobid = world.browser.driver.session_id + world.absorb(world.browser.driver.session_id, 'jobid') else: world.browser = Browser(browser_driver) @@ -180,5 +179,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if world.SAUCE_ENABLED: - set_job_status(jobid, total.scenarios_ran == total.scenarios_passed) + set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 618d52a995..80e8fd980e 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -105,7 +105,7 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True USE_I18N = True # Information needed to utilize Sauce Labs. -MITX_FEATURES['SAUCE'] = { +SAUCE = { 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), From 22b3f2b161041315a7ec925343b7eb1d8ecd53a4 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Thu, 15 Aug 2013 13:04:28 -0400 Subject: [PATCH 41/59] Factored out Sauce related things to one file LMS was arbitrarily chosen for now. Fixed up pylint and pep8 errors Fixed up pylint and pep8 errors Changed naming to be better Changed Sauce Info to obtaining a JSON string --- cms/envs/acceptance.py | 30 +----------------- common/djangoapps/terrain/browser.py | 11 ++++--- lms/envs/acceptance.py | 32 +------------------ lms/envs/sauce.py | 46 ++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 65 deletions(-) create mode 100644 lms/envs/sauce.py diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 8b208123cc..708583719e 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -8,7 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from lms.envs.sauce import * # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -19,22 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, - 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES = { - 'chrome': DesiredCapabilities.CHROME, - 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, - 'firefox': DesiredCapabilities.FIREFOX, - 'opera': DesiredCapabilities.OPERA, - 'iphone': DesiredCapabilities.IPHONE, - 'ipad': DesiredCapabilities.IPAD, - 'safari': DesiredCapabilities.SAFARI, - 'android': DesiredCapabilities.ANDROID -} def seed(): return os.getppid() @@ -96,19 +81,6 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -# Information needed to utilize Sauce Labs. -SAUCE = { - 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), - 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), - 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), - 'VERSION' : os.environ.get('SAUCE_VERSION', ''), - 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), - 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME', 'CMS TESTS'), -} - # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 0068e587b0..80c47433b7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -55,11 +55,12 @@ def set_job_status(jobid, passed=True): body_content = json.dumps({"passed": passed}) config = get_username_and_key() base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - result=requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), + result = requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 + def make_desired_capabilities(): desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') @@ -75,8 +76,9 @@ def make_desired_capabilities(): desired_capabilities['public'] = 'public restricted' return desired_capabilities + def get_username_and_key(): - return {"username": settings.SAUCE.get('USERNAME'),"access-key": settings.SAUCE.get('ACCESS_ID')} + return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} @before.harvest @@ -99,7 +101,7 @@ def initial_setup(server): config = get_username_and_key() world.browser = Browser( 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'],config['access-key']), + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), **make_desired_capabilities() ) world.absorb(world.browser.driver.session_id, 'jobid') @@ -147,7 +149,6 @@ def clear_data(scenario): world.spew('scenario_dict') - @after.each_scenario def reset_databases(scenario): ''' @@ -179,5 +180,5 @@ def teardown_browser(total): Quit the browser after executing the tests. """ if world.SAUCE_ENABLED: - set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) + set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed) world.browser.quit() diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 80e8fd980e..969f461640 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -8,8 +8,7 @@ so that we can run the lettuce acceptance tests. # pylint: disable=W0401, W0614 from .test import * - -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from .sauce import * # You need to start the server in debug mode, # otherwise the browser will not render the pages correctly @@ -20,22 +19,7 @@ import logging logging.disable(logging.ERROR) import os from random import choice -PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, - 9080, 9090, 9876, 9999, 49221, 55001] -DESIRED_CAPABILITIES = { - 'chrome': DesiredCapabilities.CHROME, - 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, - 'firefox': DesiredCapabilities.FIREFOX, - 'opera': DesiredCapabilities.OPERA, - 'iphone': DesiredCapabilities.IPHONE, - 'ipad': DesiredCapabilities.IPAD, - 'safari': DesiredCapabilities.SAFARI, - 'android': DesiredCapabilities.ANDROID -} def seed(): return os.getppid() @@ -104,20 +88,6 @@ MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True -# Information needed to utilize Sauce Labs. -SAUCE = { - 'SAUCE_ENABLED' : os.environ.get('SAUCE_ENABLED'), - 'USERNAME' : os.environ.get('SAUCE_USER_NAME'), - 'ACCESS_ID' : os.environ.get('SAUCE_API_KEY'), - 'BROWSER' : DESIRED_CAPABILITIES.get(os.environ.get('SAUCE_BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM' : os.environ.get('SAUCE_PLATFORM', 'Linux'), - 'VERSION' : os.environ.get('SAUCE_VERSION', ''), - 'DEVICE' : os.environ.get('SAUCE_DEVICE', ''), - 'SESSION' : 'Jenkins Acceptance Tests', - 'BUILD' : os.environ.get('JOB_NAME', 'LMS TESTS'), -} - - # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py new file mode 100644 index 0000000000..2692037082 --- /dev/null +++ b/lms/envs/sauce.py @@ -0,0 +1,46 @@ +""" +This config file extends the test environment configuration +so that we can run the lettuce acceptance tests on SauceLabs. +""" + +# We intentionally define lots of variables that aren't used, and +# want to import all variables from base settings files +# pylint: disable=W0401, W0614 + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +import os +import json + +PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, + 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 9080, 9090, 9876, 9999, 49221, 55001] + +DESIRED_CAPABILITIES = { + 'chrome': DesiredCapabilities.CHROME, + 'internet explorer': DesiredCapabilities.INTERNETEXPLORER, + 'firefox': DesiredCapabilities.FIREFOX, + 'opera': DesiredCapabilities.OPERA, + 'iphone': DesiredCapabilities.IPHONE, + 'ipad': DesiredCapabilities.IPAD, + 'safari': DesiredCapabilities.SAFARI, + 'android': DesiredCapabilities.ANDROID +} + +DEFAULT_CONFIG='{"PLATFORM":"Linux", "BROWSER":"chrome", "VERISON":"", "DEVICE":""}' + +SAUCE_INFO = json.loads(os.environ.get('SAUCE_INFO', DEFAULT_CONFIG)) + +# Information needed to utilize Sauce Labs. +SAUCE = { + 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), + 'USERNAME': os.environ.get('SAUCE_USER_NAME'), + 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO.get('BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO.get('PLATFORM', 'Linux'), + 'VERSION': SAUCE_INFO.get('VERSION', ''), + 'DEVICE': SAUCE_INFO.get('DEVICE', ''), + 'SESSION': 'Jenkins Acceptance Tests', + 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), +} From 4c3dcda7e8c794f24b6905c534c8e0445d0d7928 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Fri, 16 Aug 2013 15:25:31 -0400 Subject: [PATCH 42/59] Hack to get around many types of string encodings Changed the port listing Changing to better readability --- lms/envs/sauce.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index 2692037082..1704edd68b 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -9,12 +9,11 @@ so that we can run the lettuce acceptance tests on SauceLabs. from selenium.webdriver.common.desired_capabilities import DesiredCapabilities import os -import json PORTS = [2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, - 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 5000, 5001, - 5050, 5555, 5432, 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, - 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9000, 9001, + 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, + 5050, 5555, 5432, 6060, 6666, 6543, 7000, 7070, 7774, + 7777, 8003, 8031, 8080, 8081, 8765, 8888, 9080, 9090, 9876, 9999, 49221, 55001] DESIRED_CAPABILITIES = { @@ -28,19 +27,25 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } -DEFAULT_CONFIG='{"PLATFORM":"Linux", "BROWSER":"chrome", "VERISON":"", "DEVICE":""}' -SAUCE_INFO = json.loads(os.environ.get('SAUCE_INFO', DEFAULT_CONFIG)) +#HACK +#This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON +#This is the simplest way to adhere to all of these requirements and still be readible +DEFAULT_CONFIG = 'Linux-chrome--' + +SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') +if len(SAUCE_INFO) !=4: + SAUCE_INFO = DEFAULT_CONFIG.split('-') # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO.get('BROWSER', 'chrome').lower(), DesiredCapabilities.CHROME), - 'PLATFORM': SAUCE_INFO.get('PLATFORM', 'Linux'), - 'VERSION': SAUCE_INFO.get('VERSION', ''), - 'DEVICE': SAUCE_INFO.get('DEVICE', ''), + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[0].lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO[0], + 'VERSION': SAUCE_INFO[2], + 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), } From bb7cbf4d6263c30de056758c6e39d0cc532a3179 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 19 Aug 2013 09:01:40 -0400 Subject: [PATCH 43/59] If sauce is not enabled, allow full range of ports Forgot an import --- cms/envs/acceptance.py | 4 ++-- lms/envs/acceptance.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 708583719e..3b89e2e988 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -18,7 +18,7 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -from random import choice +from random import choice, randint def seed(): @@ -84,5 +84,5 @@ USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) -LETTUCE_SERVER_PORT = choice(PORTS) +LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index 969f461640..232e8b86b4 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -18,7 +18,7 @@ DEBUG = True import logging logging.disable(logging.ERROR) import os -from random import choice +from random import choice, randint def seed(): @@ -66,7 +66,7 @@ DATABASES = { # Set up XQueue information so that the lms will send # requests to a mock XQueue server running locally -XQUEUE_PORT = choice(PORTS) +XQUEUE_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) XQUEUE_INTERFACE = { "url": "http://127.0.0.1:%d" % XQUEUE_PORT, "django_auth": { @@ -91,5 +91,5 @@ USE_I18N = True # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('courseware',) -LETTUCE_SERVER_PORT = choice(PORTS) +LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535) LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome') From 21f75ff250a7a4a687d0bb0bd47d6ee71cb2cfc6 Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Mon, 19 Aug 2013 09:08:02 -0400 Subject: [PATCH 44/59] Various stylistic and pylint fixes / changes Version numbers have very different ranges for different browsers so not having a dictionary of those. Fixed a whitespace issue Fixed pylint/pep8 violations Don't need django_url Spacing issues Changed how commenting works Forgot one Used wrong name Changed around importing Remove django_url Fixed function orderingn Made logic nicer for getting a new browser Modifying tests to run in opera Needed to increase time to account for slow sauce loading Now safari LMS works Forgot an assert statement Skipping a few tests for opera --- .../features/advanced-settings.feature | 7 +- .../contentstore/features/checklists.feature | 6 +- .../contentstore/features/course-team.py | 3 +- .../contentstore/features/grading.py | 9 +- .../features/video-editor.feature | 4 +- common/djangoapps/terrain/browser.py | 110 +++++++++--------- common/djangoapps/terrain/steps.py | 11 +- .../courseware/features/login.feature | 2 +- .../courseware/features/navigation.feature | 2 + .../courseware/features/signup.feature | 2 +- .../courseware/features/video.feature | 2 +- lms/envs/sauce.py | 13 ++- 12 files changed, 93 insertions(+), 78 deletions(-) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index 767dafb796..b2941ac7a5 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -2,7 +2,6 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists I want to be able to manually enter JSON key /value pairs -#Sauce labs does not play nicely with CodeMirror Scenario: A course author sees default advanced settings Given I have opened a new course in Studio @@ -13,6 +12,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio @@ -22,6 +22,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio @@ -30,6 +31,7 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is changed + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio @@ -38,6 +40,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test error if value supplied is of the wrong type Given I am on the Advanced Course Settings page in Studio @@ -47,6 +50,7 @@ Feature: Advanced (manual) course policy Then the policy key value is unchanged # This feature will work in Firefox only when Firefox is the active window + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio @@ -55,6 +59,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as a string + # Sauce labs does not play nicely with CodeMirror @skip_sauce Scenario: Confirmation is shown on save Given I am on the Advanced Course Settings page in Studio diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 28a38b307e..1649cd0749 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -10,8 +10,9 @@ Feature: Course checklists Then I can check and uncheck tasks in a checklist And They are correctly selected after reloading the page - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome @skip_firefox + @skip_opera Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -19,8 +20,9 @@ Feature: Course checklists And I press the browser back button Then I am brought back to the course outline in the correct state - # CHROME ONLY, due to issues getting link to be active in firefox + # There are issues getting link to be active in browsers other than chrome @skip_firefox + @skip_opera Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index ab68050866..8b31d325e5 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -2,7 +2,6 @@ #pylint: disable=W0621 from lettuce import world, step -from lettuce.django import django_url from common import create_studio_user from django.contrib.auth.models import Group from auth.authz import get_course_groupname_for_role, get_user_by_email @@ -92,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name): @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): - world.visit(django_url('logout')) + world.visit('logout') world.visit('/') signin_css = 'a.action-signin' diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 719b3f7f7c..93e44b3893 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -112,10 +112,10 @@ def changes_not_persisted(step): @step(u'I see the assignment type "(.*)"$') def i_see_the_assignment_type(_step, name): - assignment_css = '#course-grading-assignment-name' - assignments = world.css_find(assignment_css) - types = [ele['value'] for ele in assignments] - assert name in types + assignment_css = '#course-grading-assignment-name' + assignments = world.css_find(assignment_css) + types = [ele['value'] for ele in assignments] + assert name in types @step(u'I change the highest grade range to "(.*)"$') @@ -144,6 +144,7 @@ def cannot_edit_fail(_step): pass # We should get this exception on failing to edit the element + @step(u'I change the grace period to "(.*)"$') def i_change_grace_period(_step, grace_period): grace_period_css = '#course-grading-graceperiod' diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature index 6f5fbd48b9..7117926c60 100644 --- a/cms/djangoapps/contentstore/features/video-editor.feature +++ b/cms/djangoapps/contentstore/features/video-editor.feature @@ -1,8 +1,6 @@ Feature: Video Component Editor As a course author, I want to be able to create video components. - #Sauce Labs cannot delete cookies - Scenario: User can view Video metadata Given I have created a Video component And I edit the component @@ -14,12 +12,14 @@ Feature: Video Component Editor Then I can modify the display name And my video display name change is persisted on save + # Sauce Labs cannot delete cookies @skip_sauce Scenario: Captions are hidden when "show captions" is false Given I have created a Video component And I have set "show captions" to False Then when I view the video it does not show the captions + # Sauce Labs cannot delete cookies @skip_sauce Scenario: Captions are shown when "show captions" is true Given I have created a Video component diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py index 80c47433b7..5820ad46f7 100644 --- a/common/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -12,6 +12,9 @@ from django.core.management import call_command from django.conf import settings from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from requests import put +from base64 import encodestring +from json import dumps # Let the LMS and CMS do their one-time setup # For example, setting up mongo caches @@ -42,27 +45,32 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") MAX_VALID_BROWSER_ATTEMPTS = 20 -# https://gist.github.com/santiycr/1644439 -import requests -import base64 -try: - import json -except ImportError: - import simplejson as json + +def get_username_and_key(): + """ + Returns the Sauce Labs username and access ID as set by environment variables + """ + return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} def set_job_status(jobid, passed=True): - body_content = json.dumps({"passed": passed}) + """ + Sets the job status on sauce labs + """ + body_content = dumps({"passed": passed}) config = get_username_and_key() - base64string = base64.encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] - result = requests.put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), + base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1] + result = put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid), data=body_content, headers={"Authorization": "Basic {}".format(base64string)}) return result.status_code == 200 def make_desired_capabilities(): - desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) + """ + Returns a DesiredCapabilities object corresponding to the environment sauce parameters + """ + desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME) desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM') desired_capabilities['version'] = settings.SAUCE.get('VERSION') desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE') @@ -77,60 +85,54 @@ def make_desired_capabilities(): return desired_capabilities -def get_username_and_key(): - return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')} - - @before.harvest def initial_setup(server): """ Launch the browser once before executing the tests. """ - world.absorb(settings.SAUCE.get('SAUCE_ENABLED'),'SAUCE_ENABLED') - browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') + world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED') - # There is an issue with ChromeDriver2 r195627 on Ubuntu - # in which we sometimes get an invalid browser session. - # This is a work-around to ensure that we get a valid session. - success = False - num_attempts = 0 - while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: + if not world.SAUCE_ENABLED: + browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') - # Get a browser session - if world.SAUCE_ENABLED: - config = get_username_and_key() - world.browser = Browser( - 'remote', - url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), - **make_desired_capabilities() - ) - world.absorb(world.browser.driver.session_id, 'jobid') - else: + # There is an issue with ChromeDriver2 r195627 on Ubuntu + # in which we sometimes get an invalid browser session. + # This is a work-around to ensure that we get a valid session. + success = False + num_attempts = 0 + while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: world.browser = Browser(browser_driver) + # Try to visit the main page + # If the browser session is invalid, this will + # raise a WebDriverException + try: + world.visit('/') + + except WebDriverException: + world.browser.quit() + num_attempts += 1 + + else: + success = True + + # If we were unable to get a valid session within the limit of attempts, + # then we cannot run the tests. + if not success: + raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) + + world.browser.driver.set_window_size(1280, 1024) + + else: + config = get_username_and_key() + world.browser = Browser( + 'remote', + url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']), + **make_desired_capabilities() + ) world.browser.driver.implicitly_wait(30) - # Try to visit the main page - # If the browser session is invalid, this will - # raise a WebDriverException - try: - world.visit('/') - - except WebDriverException: - world.browser.quit() - num_attempts += 1 - - else: - success = True - - # If we were unable to get a valid session within the limit of attempts, - # then we cannot run the tests. - if not success: - raise IOError("Could not acquire valid {driver} browser session.".format(driver='remote')) - - # Set the browser size to 1280x1024 - if not world.SAUCE_ENABLED: - world.browser.driver.set_window_size(1280, 1024) + world.absorb(world.browser.driver.session_id, 'jobid') @before.each_scenario diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py index 6e11ed19ea..f13b3ff932 100644 --- a/common/djangoapps/terrain/steps.py +++ b/common/djangoapps/terrain/steps.py @@ -99,7 +99,7 @@ def i_am_logged_in_user(step): @step('I am not logged in$') def i_am_not_logged_in(step): - world.visit(django_url('logout')) + world.visit('logout') @step('I am staff for course "([^"]*)"$') @@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text): @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page') def should_see_in_the_page(step, doesnt_appear, text): + multiplier = 1 + if world.SAUCE_ENABLED: + multiplier = 2 if doesnt_appear: - assert world.browser.is_text_not_present(text, wait_time=5) + assert world.browser.is_text_not_present(text, wait_time=5*multiplier) else: - assert world.browser.is_text_present(text, wait_time=5) + assert world.browser.is_text_present(text, wait_time=5*multiplier) @step('I am logged in$') @@ -150,7 +153,7 @@ def i_am_logged_in(step): world.log_in(username='robot', password='test') world.browser.visit(django_url('/')) # You should not see the login link - world.is_css_not_present('a#login') + assert world.is_css_not_present('a#login') @step(u'I am an edX user$') diff --git a/lms/djangoapps/courseware/features/login.feature b/lms/djangoapps/courseware/features/login.feature index 5c777fd64f..4165a9bb9f 100644 --- a/lms/djangoapps/courseware/features/login.feature +++ b/lms/djangoapps/courseware/features/login.feature @@ -11,7 +11,7 @@ Feature: Login in as a registered user And I submit my credentials on the login form Then I should see the login error message "This account has not been activated" - # CHROME ONLY, firefox will not redirect properly + # firefox will not redirect properly when the whole suite is run @skip_firefox Scenario: Login to an activated account Given I am an edX user diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 8fd8b54c1a..70cc93fd93 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -13,6 +13,8 @@ Feature: Navigate Course When I click on subsection "2" Then I should see the content of subsection "2" + # Clicking on the sequence link doesn't work on opera through sauce + @skip_opera Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" diff --git a/lms/djangoapps/courseware/features/signup.feature b/lms/djangoapps/courseware/features/signup.feature index c1fce04b54..3c9f491f7d 100644 --- a/lms/djangoapps/courseware/features/signup.feature +++ b/lms/djangoapps/courseware/features/signup.feature @@ -3,7 +3,7 @@ Feature: Sign in As a new user I want to signup for a student account - # CHROME ONLY, firefox will not redirect properly + # firefox will not redirect properly @skip_firefox Scenario: Sign up from the homepage Given I visit the homepage diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 260887290e..6c8299f2c5 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -11,7 +11,7 @@ Feature: Video component Given the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode - #Firefox doesn't have HTML5 + # Firefox doesn't have HTML5 @skip_firefox Scenario: Autoplay is enabled in LMS for a Video component Given the course has a Video component in HTML5 mode diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index 1704edd68b..e33d4dff62 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -27,25 +27,26 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } +PLATFORMS = ['Linux', 'OS X 10.8', 'OS X 10.6', 'Windows 8', 'Windows 7', 'Windows XP'] #HACK #This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON -#This is the simplest way to adhere to all of these requirements and still be readible +#This is the simplest way to adhere to all of these requirements and still be readable DEFAULT_CONFIG = 'Linux-chrome--' SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') -if len(SAUCE_INFO) !=4: - SAUCE_INFO = DEFAULT_CONFIG.split('-') +if len(SAUCE_INFO) != 4: + SAUCE_INFO = DEFAULT_CONFIG.split('-') # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[0].lower(), DesiredCapabilities.CHROME), - 'PLATFORM': SAUCE_INFO[0], + 'PLATFORM': SAUCE_INFO[0] if SAUCE_INFO[0] in PLATFORMS else 'Linux', + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1].lower(), DesiredCapabilities.CHROME), 'VERSION': SAUCE_INFO[2], 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', - 'BUILD': os.environ.get('JOB_NAME', 'LETTUCE TESTS'), + 'BUILD': os.environ.get('BUILD_DISPLAY_NAME', 'LETTUCE TESTS'), } From b2480b5f00782cffa4020db8f493ca5b93192c8d Mon Sep 17 00:00:00 2001 From: JonahStanley Date: Tue, 20 Aug 2013 10:43:05 -0400 Subject: [PATCH 45/59] Changed way feature flags are interpreted Configurations must be defined before hand Reduced pylint violations We only support 4 browsers --- .../contentstore/features/checklists.feature | 2 -- .../contentstore/features/upload.py | 3 +- .../courseware/features/navigation.feature | 2 -- lms/envs/sauce.py | 29 +++++++++++++++---- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index 1649cd0749..b48d8608b6 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -12,7 +12,6 @@ Feature: Course checklists # There are issues getting link to be active in browsers other than chrome @skip_firefox - @skip_opera Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -22,7 +21,6 @@ Feature: Course checklists # There are issues getting link to be active in browsers other than chrome @skip_firefox - @skip_opera Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index a989d6c07f..882b36e6b2 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -10,6 +10,7 @@ import os TEST_ROOT = settings.COMMON_TEST_DATA_ROOT + @step(u'I go to the files and uploads page') def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' @@ -106,8 +107,8 @@ def get_index(file_name): def get_file(file_name): index = get_index(file_name) assert index != -1 - url_css = 'a.filename' + def get_url(): return world.css_find(url_css)[index]._element.get_attribute('href') url = world.retry_on_exception(get_url) diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature index 70cc93fd93..8fd8b54c1a 100644 --- a/lms/djangoapps/courseware/features/navigation.feature +++ b/lms/djangoapps/courseware/features/navigation.feature @@ -13,8 +13,6 @@ Feature: Navigate Course When I click on subsection "2" Then I should see the content of subsection "2" - # Clicking on the sequence link doesn't work on opera through sauce - @skip_opera Scenario: I can navigate to sequences Given I am viewing a section with multiple sequences When I click on sequence "2" diff --git a/lms/envs/sauce.py b/lms/envs/sauce.py index e33d4dff62..e4764f7cf8 100644 --- a/lms/envs/sauce.py +++ b/lms/envs/sauce.py @@ -27,24 +27,41 @@ DESIRED_CAPABILITIES = { 'android': DesiredCapabilities.ANDROID } -PLATFORMS = ['Linux', 'OS X 10.8', 'OS X 10.6', 'Windows 8', 'Windows 7', 'Windows XP'] +ALL_CONFIG = { + 'Linux-chrome--': ['Linux', 'chrome', '', ''], + 'Windows 8-chrome--': ['Windows 8', 'chrome', '', ''], + 'Windows 7-chrome--': ['Windows 7', 'chrome', '', ''], + 'Windows XP-chrome--': ['Windows XP', 'chrome', '', ''], + 'OS X 10.8-chrome--': ['OS X 10.8', 'chrome', '', ''], + 'OS X 10.6-chrome--': ['OS X 10.6', 'chrome', '', ''], + + 'Linux-firefox-23-': ['Linux', 'firefox', '23', ''], + 'Windows 8-firefox-23-': ['Windows 8', 'firefox', '23', ''], + 'Windows 7-firefox-23-': ['Windows 7', 'firefox', '23', ''], + 'Windows XP-firefox-23-': ['Windows XP', 'firefox', '23', ''], + + 'OS X 10.8-safari-6-': ['OS X 10.8', 'safari', '6', ''], + + 'Windows 8-internetexplorer-10-': ['Windows 8', 'internetexplorer', '10', ''], +} #HACK #This needs to be done because Jenkins needs to satisfy URLs, JSON, BASH, SAUCE, and PYTHON #This is the simplest way to adhere to all of these requirements and still be readable +# PLATFORM-BROWSER-VERSION_NUM-DEVICE DEFAULT_CONFIG = 'Linux-chrome--' -SAUCE_INFO = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG).split('-') -if len(SAUCE_INFO) != 4: - SAUCE_INFO = DEFAULT_CONFIG.split('-') +SAUCE_CONFIG = os.environ.get('SAUCE_INFO', DEFAULT_CONFIG) + +SAUCE_INFO = ALL_CONFIG[SAUCE_CONFIG] # Information needed to utilize Sauce Labs. SAUCE = { 'SAUCE_ENABLED': os.environ.get('SAUCE_ENABLED'), 'USERNAME': os.environ.get('SAUCE_USER_NAME'), 'ACCESS_ID': os.environ.get('SAUCE_API_KEY'), - 'PLATFORM': SAUCE_INFO[0] if SAUCE_INFO[0] in PLATFORMS else 'Linux', - 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1].lower(), DesiredCapabilities.CHROME), + 'PLATFORM': SAUCE_INFO[0], + 'BROWSER': DESIRED_CAPABILITIES.get(SAUCE_INFO[1]), 'VERSION': SAUCE_INFO[2], 'DEVICE': SAUCE_INFO[3], 'SESSION': 'Jenkins Acceptance Tests', From 2ba0d40d7b69c2997d0d47bd10a302aa0b68c1a6 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:02:58 -0400 Subject: [PATCH 46/59] fix pep8 violations --- cms/djangoapps/contentstore/management/commands/import.py | 2 +- cms/djangoapps/contentstore/tests/test_import_nostatic.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 520e36f4d2..e0d58b32f0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -18,7 +18,7 @@ class Command(BaseCommand): make_option('--nostatic', action='store_true', help='Skip import of static content'), - ) + ) def handle(self, *args, **options): "Execute the command" diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index fc68975ebb..24b00dbbf3 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -1,6 +1,6 @@ #pylint: disable=E1101 ''' -Tests for importing with no static +Tests for importing with no static ''' from django.test.client import Client From ef98c54f5bed8efbdfb8b232882bfada8dd5eb3c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:06:10 -0400 Subject: [PATCH 47/59] fix some pylint violations --- cms/djangoapps/contentstore/tests/test_import_nostatic.py | 2 +- lms/djangoapps/courseware/tests/test_module_render.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 24b00dbbf3..1eb36bc75f 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -52,7 +52,7 @@ class MongoCollectionFindWrapper(object): class ContentStoreImportNoStaticTest(ModuleStoreTestCase): """ Tests that rely on the toy and test_import_course courses. - TODO: refactor using CourseFactory so they do not. + NOTE: refactor using CourseFactory so they do not. """ def setUp(self): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index bf8d52da6f..56659b7c2b 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -384,7 +384,7 @@ class TestHtmlModifiers(ModuleStoreTestCase): def test_get_course_info_section(self): self.course.lms.static_asset_path = "toy_course_dir" get_course_info_section(self.request, self.course, "handouts") - # TODO: check handouts output...right now test course seems to have no such content + # NOTE: check handouts output...right now test course seems to have no such content # at least this makes sure get_course_info_section returns without exception def test_course_link_rewrite(self): From fb3a5bf9db75307fa973774c5d3c9ddca507ab40 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:10:36 -0400 Subject: [PATCH 48/59] remove unused class --- .../tests/test_import_nostatic.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py index 1eb36bc75f..aad6ffbfe4 100644 --- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py +++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py @@ -29,25 +29,6 @@ TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex -class MongoCollectionFindWrapper(object): - ''' - MongoCollectionFindWrapper for testing. - ''' - def __init__(self, original): - """ - intit func - """ - self.original = original - self.counter = 0 - - def find(self, query, *args, **kwargs): - """ - find func - """ - self.counter = self.counter + 1 - return self.original(query, *args, **kwargs) - - @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ContentStoreImportNoStaticTest(ModuleStoreTestCase): """ From 0c1c3f11af676a4bf0449e9a183ddea96bb10e9e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 20:48:56 -0400 Subject: [PATCH 49/59] loop in static import testing into common/* tests --- cms/djangoapps/contentstore/tests/test_contentstore.py | 2 +- common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 96b0b84e36..2a69fc451c 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -476,7 +476,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): content_store = contentstore() module_store = modulestore('direct') - import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store) + import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True) course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') course = module_store.get_item(course_location) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index 17036a16bf..f047ef8e41 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -11,6 +11,7 @@ from xmodule.tests import DATA_DIR from xmodule.modulestore import Location from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.contentstore.mongo import MongoContentStore from xmodule.modulestore.tests.test_modulestore import check_path_to_location @@ -46,9 +47,12 @@ class TestMongoModuleStore(object): def initdb(): # connect to the db store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) + # since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class + # as well + content_store = MongoContentStore(HOST, DB) # Explicitly list the courses to load (don't want the big one) courses = ['toy', 'simple'] - import_from_xml(store, DATA_DIR, courses) + import_from_xml(store, DATA_DIR, courses, static_content_store=content_store) return store @staticmethod From 77a38afaeb01f4b295af483d7b3ec991fbf35a4c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 21 Aug 2013 22:00:33 -0400 Subject: [PATCH 50/59] add some draft courseware importing paths in common/* tests --- .../xmodule/modulestore/tests/test_mongo.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index f047ef8e41..53128b919f 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -10,6 +10,7 @@ from xblock.runtime import KeyValueStore, InvalidScopeError from xmodule.tests import DATA_DIR from xmodule.modulestore import Location from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore +from xmodule.modulestore.draft import DraftModuleStore from xmodule.modulestore.xml_importer import import_from_xml from xmodule.contentstore.mongo import MongoContentStore @@ -36,7 +37,7 @@ class TestMongoModuleStore(object): # is ok only as long as none of the tests modify the db. # If (when!) that changes, need to either reload the db, or load # once and copy over to a tmp db for each test. - cls.store = cls.initdb() + cls.store, cls.content_store, cls.draft_store = cls.initdb() @classmethod def teardownClass(cls): @@ -50,10 +51,14 @@ class TestMongoModuleStore(object): # since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class # as well content_store = MongoContentStore(HOST, DB) + # + # Also test draft store imports + # + draft_store = DraftModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) # Explicitly list the courses to load (don't want the big one) - courses = ['toy', 'simple'] - import_from_xml(store, DATA_DIR, courses, static_content_store=content_store) - return store + courses = ['toy', 'simple', 'simple_with_draft'] + import_from_xml(store, DATA_DIR, courses, draft_store=draft_store, static_content_store=content_store) + return store, content_store, draft_store @staticmethod def destroy_db(connection): @@ -81,10 +86,11 @@ class TestMongoModuleStore(object): def test_get_courses(self): '''Make sure the course objects loaded properly''' courses = self.store.get_courses() - assert_equals(len(courses), 2) + assert_equals(len(courses), 3) courses.sort(key=lambda c: c.id) assert_equals(courses[0].id, 'edX/simple/2012_Fall') - assert_equals(courses[1].id, 'edX/toy/2012_Fall') + assert_equals(courses[1].id, 'edX/simple_with_draft/2012_Fall') + assert_equals(courses[2].id, 'edX/toy/2012_Fall') def test_loads(self): assert_not_equals( @@ -133,7 +139,7 @@ class TestMongoModuleStore(object): Assumes the information is desired for courses[1] ('toy' course). """ - return courses[1].tabs[index]['name'] + return courses[2].tabs[index]['name'] # There was a bug where model.save was not getting called after the static tab name # was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that From 63c5ec5277422f67015f4cadec821b4da2460dae Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Mon, 19 Aug 2013 15:48:24 +0300 Subject: [PATCH 51/59] Add supporting failover from Youtube. --- .../lib/xmodule/xmodule/js/spec/helper.coffee | 7 +- .../xmodule/js/spec/video/general_spec.js | 40 +++++ .../js/spec/video/video_player_spec.js | 2 + .../xmodule/js/src/video/01_initialize.js | 147 +++++++++++------- 4 files changed, 142 insertions(+), 54 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 1dfccdf521..5fbbd27fad 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -90,7 +90,12 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'] jasmine.stubRequests = -> spyOn($, 'ajax').andCallFake (settings) -> if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ - settings.success data: jasmine.stubbedMetadata[match[1]] + if settings.success + settings.success data: jasmine.stubbedMetadata[match[1]] + else { + always: (callback) -> + callback.call(window, {}, 'success'); + } else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/ settings.success jasmine.stubbedCaption else if settings.url.match /.+\/problem_get$/ diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index accfba0dbe..f4dd1db206 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -58,6 +58,46 @@ expect(this.state.speed).toEqual('0.75'); }); }); + + describe('Check Youtube link existence', function () { + var statusList = { + 'error': 'html5', + 'timeout': 'html5', + 'abort': 'html5', + 'parsererror': 'html5', + 'success': 'youtube', + 'notmodified': 'youtube' + }; + + function stubDeffered(data, status) { + return { + always: function(callback) { + callback.call(window, data, status); + } + } + } + + function checkPlayer(videoType, data, status) { + this.state = new window.Video('#example'); + spyOn(this.state , 'getVideoMetadata') + .andReturn(stubDeffered(data, status)); + this.state.initialize('#example'); + + expect(this.state.videoType).toEqual(videoType); + } + + it('if video id is incorrect', function () { + checkPlayer('html5', { error: {} }, 'success'); + }); + + $.each(statusList, function(status, mode){ + it('Status:' + status + ', mode:' + mode, function () { + checkPlayer(mode, {}, status); + }); + }); + + }); + }); describe('HTML5', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index e92f251f70..0873426aa9 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -79,6 +79,8 @@ it('create Youtube player', function() { var oldYT = window.YT; + jasmine.stubRequests(); + window.YT = { Player: function () { }, PlayerState: oldYT.PlayerState diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 9a6e20421d..62c9e88141 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -30,8 +30,7 @@ function (VideoPlayer) { */ return function (state, element) { _makeFunctionsPublic(state); - _initialize(state, element); - _renderElements(state); + state.initialize(element); }; // *************************************************************** @@ -56,59 +55,12 @@ function (VideoPlayer) { // Old private functions. Now also public so that can be // tested by Jasmine. + state.initialize = _.bind(initialize, state); state.parseSpeed = _.bind(parseSpeed, state); state.fetchMetadata = _.bind(fetchMetadata, state); state.parseYoutubeStreams = _.bind(parseYoutubeStreams, state); state.parseVideoSources = _.bind(parseVideoSources, state); - } - - // function _initialize(element) - // The function set initial configuration and preparation. - - function _initialize(state, element) { - // This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'. - state.isFullScreen = false; - - // The parent element of the video, and the ID. - state.el = $(element).find('.video'); - state.id = state.el.attr('id').replace(/video_/, ''); - - // We store all settings passed to us by the server in one place. These are "read only", so don't - // modify them. All variable content lives in 'state' object. - state.config = { - element: element, - - start: state.el.data('start'), - end: state.el.data('end'), - - caption_data_dir: state.el.data('caption-data-dir'), - caption_asset_path: state.el.data('caption-asset-path'), - show_captions: (state.el.data('show-captions').toString().toLowerCase() === 'true'), - youtubeStreams: state.el.data('streams'), - - sub: state.el.data('sub'), - mp4Source: state.el.data('mp4-source'), - webmSource: state.el.data('webm-source'), - oggSource: state.el.data('ogg-source'), - - fadeOutTimeout: 1400, - - availableQualities: ['hd720', 'hd1080', 'highres'] - }; - - if (!(_parseYouTubeIDs(state))) { - // If we do not have YouTube ID's, try parsing HTML5 video sources. - _prepareHTML5Video(state); - } - - _configureCaptions(state); - _setPlayerMode(state); - - // Possible value are: 'visible', 'hiding', and 'invisible'. - state.controlState = 'visible'; - state.controlHideTimeout = null; - state.captionState = 'visible'; - state.captionHideTimeout = null; + state.getVideoMetadata = _.bind(getVideoMetadata, state); } // function _renderElements(state) @@ -228,12 +180,83 @@ function (VideoPlayer) { state.setSpeed($.cookie('video_speed')); } + function _setConfigurations(state) { + _configureCaptions(state); + _setPlayerMode(state); + + // Possible value are: 'visible', 'hiding', and 'invisible'. + state.controlState = 'visible'; + state.controlHideTimeout = null; + state.captionState = 'visible'; + state.captionHideTimeout = null; + } + // *************************************************************** // Public functions start here. // These are available via the 'state' object. Their context ('this' keyword) is the 'state' object. // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). // *************************************************************** + // function initialize(element) + // The function set initial configuration and preparation. + + function initialize(element) { + var state = this; + // This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'. + state.isFullScreen = false; + + // The parent element of the video, and the ID. + state.el = $(element).find('.video'); + state.id = state.el.attr('id').replace(/video_/, ''); + + // We store all settings passed to us by the server in one place. These are "read only", so don't + // modify them. All variable content lives in 'state' object. + state.config = { + element: element, + + start: state.el.data('start'), + end: state.el.data('end'), + + caption_data_dir: state.el.data('caption-data-dir'), + caption_asset_path: state.el.data('caption-asset-path'), + show_captions: (state.el.data('show-captions').toString().toLowerCase() === 'true'), + youtubeStreams: state.el.data('streams'), + + sub: state.el.data('sub'), + mp4Source: state.el.data('mp4-source'), + webmSource: state.el.data('webm-source'), + oggSource: state.el.data('ogg-source'), + + fadeOutTimeout: 1400, + + availableQualities: ['hd720', 'hd1080', 'highres'] + }; + + if (!(_parseYouTubeIDs(state))) { + // If we do not have YouTube ID's, try parsing HTML5 video sources. + _prepareHTML5Video(state); + _setConfigurations(state); + _renderElements(state); + } else { + state.getVideoMetadata() + .always(function(json, status) { + var err = $.isPlainObject(json.error) || + (status !== "success" && status !== "notmodified"); + + if (err){ + // When the youtube link doesn't work for any reason + // (for example, the great firewall in china) any + // alternate sources should automatically play. + _prepareHTML5Video(state); + state.el.find('a.quality_control').hide(); + } + + _setConfigurations(state); + _renderElements(state); + }); + } + } + // function parseYoutubeStreams(state, youtubeStreams) // // Take a string in the form: @@ -297,9 +320,9 @@ function (VideoPlayer) { this.metadata = {}; $.each(this.videos, function (speed, url) { - $.get('https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc', (function(data) { + _this.getVideoMetadata(url, function(data) { _this.metadata[data.data.id] = data.data; - }), 'jsonp'); + }); }); } @@ -329,6 +352,24 @@ function (VideoPlayer) { } } + function getVideoMetadata(url, callback) { + var successHandler, xhr; + + if (typeof url !== 'string') { + url = this.videos['1.0'] || ''; + } + + successHandler = ($.isFunction(callback)) ? callback : null; + xhr = $.ajax({ + url: 'https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc', + timeout: 500, + dataType: 'jsonp', + success: successHandler + }); + + return xhr; + } + function stopBuffering() { var video; From 22b5decc95aabbe67c21479b6d772f2026903b05 Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Tue, 20 Aug 2013 16:49:33 +0300 Subject: [PATCH 52/59] Small fixes. --- .../lib/xmodule/xmodule/js/spec/helper.coffee | 1 + .../xmodule/js/spec/video/general_spec.js | 12 ++--- .../xmodule/js/src/video/01_initialize.js | 48 +++++++++---------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 5fbbd27fad..f3cecf71cb 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -91,6 +91,7 @@ jasmine.stubRequests = -> spyOn($, 'ajax').andCallFake (settings) -> if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ if settings.success + # match[1] - it's video ID settings.success data: jasmine.stubbedMetadata[match[1]] else { always: (callback) -> diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index f4dd1db206..33d7277bb4 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -61,12 +61,12 @@ describe('Check Youtube link existence', function () { var statusList = { - 'error': 'html5', - 'timeout': 'html5', - 'abort': 'html5', - 'parsererror': 'html5', - 'success': 'youtube', - 'notmodified': 'youtube' + error: 'html5', + timeout: 'html5', + abort: 'html5', + parsererror: 'html5', + success: 'youtube', + notmodified: 'youtube' }; function stubDeffered(data, status) { diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 62c9e88141..79bc16dbda 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -201,44 +201,44 @@ function (VideoPlayer) { // The function set initial configuration and preparation. function initialize(element) { - var state = this; + var _this = this; // This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'. - state.isFullScreen = false; + this.isFullScreen = false; // The parent element of the video, and the ID. - state.el = $(element).find('.video'); - state.id = state.el.attr('id').replace(/video_/, ''); + this.el = $(element).find('.video'); + this.id = this.el.attr('id').replace(/video_/, ''); // We store all settings passed to us by the server in one place. These are "read only", so don't // modify them. All variable content lives in 'state' object. - state.config = { + this.config = { element: element, - start: state.el.data('start'), - end: state.el.data('end'), + start: this.el.data('start'), + end: this.el.data('end'), - caption_data_dir: state.el.data('caption-data-dir'), - caption_asset_path: state.el.data('caption-asset-path'), - show_captions: (state.el.data('show-captions').toString().toLowerCase() === 'true'), - youtubeStreams: state.el.data('streams'), + caption_data_dir: this.el.data('caption-data-dir'), + caption_asset_path: this.el.data('caption-asset-path'), + show_captions: (this.el.data('show-captions').toString().toLowerCase() === 'true'), + youtubeStreams: this.el.data('streams'), - sub: state.el.data('sub'), - mp4Source: state.el.data('mp4-source'), - webmSource: state.el.data('webm-source'), - oggSource: state.el.data('ogg-source'), + sub: this.el.data('sub'), + mp4Source: this.el.data('mp4-source'), + webmSource: this.el.data('webm-source'), + oggSource: this.el.data('ogg-source'), fadeOutTimeout: 1400, availableQualities: ['hd720', 'hd1080', 'highres'] }; - if (!(_parseYouTubeIDs(state))) { + if (!(_parseYouTubeIDs(this))) { // If we do not have YouTube ID's, try parsing HTML5 video sources. - _prepareHTML5Video(state); - _setConfigurations(state); - _renderElements(state); + _prepareHTML5Video(this); + _setConfigurations(this); + _renderElements(this); } else { - state.getVideoMetadata() + this.getVideoMetadata() .always(function(json, status) { var err = $.isPlainObject(json.error) || (status !== "success" && status !== "notmodified"); @@ -247,12 +247,12 @@ function (VideoPlayer) { // When the youtube link doesn't work for any reason // (for example, the great firewall in china) any // alternate sources should automatically play. - _prepareHTML5Video(state); - state.el.find('a.quality_control').hide(); + _prepareHTML5Video(_this); + _this.el.find('a.quality_control').hide(); } - _setConfigurations(state); - _renderElements(state); + _setConfigurations(_this); + _renderElements(_this); }); } } From 03111cb98ed4a335288193c79a6fe8a75b9e6669 Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Tue, 20 Aug 2013 16:52:51 +0300 Subject: [PATCH 53/59] Remove onTouchBasedDevice function. --- common/lib/xmodule/xmodule/js/spec/video/general_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index 33d7277bb4..9194106fff 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -4,8 +4,6 @@ beforeEach(function () { jasmine.stubRequests(); - oldOTBD = window.onTouchBasedDevice; - window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false); this.videosDefinition = '0.75:7tqY6eQzVhE,1.0:cogebirgzzM'; this['7tqY6eQzVhE'] = '7tqY6eQzVhE'; this['cogebirgzzM'] = 'cogebirgzzM'; @@ -16,7 +14,6 @@ window.onYouTubePlayerAPIReady = undefined; window.onHTML5PlayerAPIReady = undefined; $('source').remove(); - window.onTouchBasedDevice = oldOTBD; }); describe('constructor', function () { From 842556d284a6429a9738b7dc1bfd9a73fecdb65e Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 22 Aug 2013 08:57:10 -0400 Subject: [PATCH 54/59] add new test data with draft content --- common/test/data/simple_with_draft/README.md | 2 + common/test/data/simple_with_draft/course.xml | 31 ++++++++++ .../drafts/vertical/test_vertical.xml | 5 ++ .../data/simple_with_draft/html/toylab.html | 3 + .../problem/L1_Problem_1.xml | 43 +++++++++++++ .../simple_with_draft/problem/ps01-simple.xml | 62 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 common/test/data/simple_with_draft/README.md create mode 100644 common/test/data/simple_with_draft/course.xml create mode 100644 common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml create mode 100644 common/test/data/simple_with_draft/html/toylab.html create mode 100644 common/test/data/simple_with_draft/problem/L1_Problem_1.xml create mode 100644 common/test/data/simple_with_draft/problem/ps01-simple.xml diff --git a/common/test/data/simple_with_draft/README.md b/common/test/data/simple_with_draft/README.md new file mode 100644 index 0000000000..69ff6b4ed0 --- /dev/null +++ b/common/test/data/simple_with_draft/README.md @@ -0,0 +1,2 @@ +This is a simple, but non-trivial, course using multiple module types and some nested structure. + diff --git a/common/test/data/simple_with_draft/course.xml b/common/test/data/simple_with_draft/course.xml new file mode 100644 index 0000000000..c130686012 --- /dev/null +++ b/common/test/data/simple_with_draft/course.xml @@ -0,0 +1,31 @@ + + +

+ + +
+ + +
+ + + +
+
+ +
diff --git a/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml b/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml new file mode 100644 index 0000000000..4433d282a4 --- /dev/null +++ b/common/test/data/simple_with_draft/drafts/vertical/test_vertical.xml @@ -0,0 +1,5 @@ + + + Foobar - edit in draft + + \ No newline at end of file diff --git a/common/test/data/simple_with_draft/html/toylab.html b/common/test/data/simple_with_draft/html/toylab.html new file mode 100644 index 0000000000..81df84bd63 --- /dev/null +++ b/common/test/data/simple_with_draft/html/toylab.html @@ -0,0 +1,3 @@ +Lab 2A: Superposition Experiment + +

Isn't the toy course great?

diff --git a/common/test/data/simple_with_draft/problem/L1_Problem_1.xml b/common/test/data/simple_with_draft/problem/L1_Problem_1.xml new file mode 100644 index 0000000000..2ba0617904 --- /dev/null +++ b/common/test/data/simple_with_draft/problem/L1_Problem_1.xml @@ -0,0 +1,43 @@ + + +

+

Finger Exercise 1

+

+

+Here are two definitions:

+
    +
  1. +

    +Declarative knowledge refers to statements of fact.

    +
  2. +
  3. +

    +Imperative knowledge refers to 'how to' methods.

    +
  4. +
+

+Which of the following choices is correct?

+
    +
  1. +

    +Statement 1 is true, Statement 2 is false

    +
  2. +
  3. +

    +Statement 1 is false, Statement 2 is true

    +
  4. +
  5. +

    +Statement 1 and Statement 2 are both false

    +
  6. +
  7. +

    +Statement 1 and Statement 2 are both true

    +
  8. +
+

+ + + +

+
diff --git a/common/test/data/simple_with_draft/problem/ps01-simple.xml b/common/test/data/simple_with_draft/problem/ps01-simple.xml new file mode 100644 index 0000000000..e70d8f2c8d --- /dev/null +++ b/common/test/data/simple_with_draft/problem/ps01-simple.xml @@ -0,0 +1,62 @@ +