From bf0affc25d4b89e33fe7bfa4088a3b91fd7b15e8 Mon Sep 17 00:00:00 2001 From: asadiqbal Date: Wed, 28 Oct 2015 15:51:47 +0500 Subject: [PATCH] SOL-1292 --- .../contentstore/views/entrance_exam.py | 86 ++++++++++------- .../contentstore/views/import_export.py | 32 +++++++ .../views/tests/test_entrance_exam.py | 55 ++++++++++- .../views/tests/test_import_export.py | 90 ++++++++++++++++++ .../contentstore/views/tests/test_item.py | 2 +- .../tests/test_mongo_call_count.py | 4 +- common/lib/xmodule/xmodule/seq_module.py | 2 +- .../tests/studio/test_import_export.py | 47 +++++++++ .../imports/entrance_exam_course.2015.tar.gz | Bin 0 -> 2804 bytes 9 files changed, 276 insertions(+), 42 deletions(-) create mode 100644 common/test/data/imports/entrance_exam_course.2015.tar.gz diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py index 320ce3df3a..8087b5a02a 100644 --- a/cms/djangoapps/contentstore/views/entrance_exam.py +++ b/cms/djangoapps/contentstore/views/entrance_exam.py @@ -164,35 +164,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N category='sequential', display_name=_('Entrance Exam - Subsection') ) - - # Add an entrance exam milestone if one does not already exist - namespace_choices = milestones_helpers.get_namespace_choices() - milestone_namespace = milestones_helpers.generate_milestone_namespace( - namespace_choices.get('ENTRANCE_EXAM'), - course_key - ) - milestones = milestones_helpers.get_milestones(milestone_namespace) - if len(milestones): - milestone = milestones[0] - else: - description = 'Autogenerated during {} entrance exam creation.'.format(unicode(course.id)) - milestone = milestones_helpers.add_milestone({ - 'name': _('Completed Course Entrance Exam'), - 'namespace': milestone_namespace, - 'description': description - }) - relationship_types = milestones_helpers.get_milestone_relationship_types() - milestones_helpers.add_course_milestone( - unicode(course.id), - relationship_types['REQUIRES'], - milestone - ) - milestones_helpers.add_course_content_milestone( - unicode(course.id), - unicode(created_block.location), - relationship_types['FULFILLS'], - milestone - ) + add_entrance_exam_milestone(course.id, created_block) return HttpResponse(status=201) @@ -250,14 +222,7 @@ def _delete_entrance_exam(request, course_key): if course is None: return HttpResponse(status=400) - course_children = store.get_items( - course_key, - qualifiers={'category': 'chapter'} - ) - for course_child in course_children: - if course_child.is_entrance_exam: - delete_item(request, course_child.scope_ids.usage_id) - milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id)) + remove_entrance_exam_milestone_reference(request, course_key) # Reset the entrance exam flags on the course # Reload the course so we have the latest state @@ -283,3 +248,50 @@ def _serialize_entrance_exam(entrance_exam_module): return json.dumps({ 'locator': unicode(entrance_exam_module.location) }) + + +def add_entrance_exam_milestone(course_id, x_block): + # Add an entrance exam milestone if one does not already exist for given xBlock + # As this is a standalone method for entrance exam, We should check that given xBlock should be an entrance exam. + if x_block.is_entrance_exam: + namespace_choices = milestones_helpers.get_namespace_choices() + milestone_namespace = milestones_helpers.generate_milestone_namespace( + namespace_choices.get('ENTRANCE_EXAM'), + course_id + ) + milestones = milestones_helpers.get_milestones(milestone_namespace) + if len(milestones): + milestone = milestones[0] + else: + description = 'Autogenerated during {} entrance exam creation.'.format(unicode(course_id)) + milestone = milestones_helpers.add_milestone({ + 'name': _('Completed Course Entrance Exam'), + 'namespace': milestone_namespace, + 'description': description + }) + relationship_types = milestones_helpers.get_milestone_relationship_types() + milestones_helpers.add_course_milestone( + unicode(course_id), + relationship_types['REQUIRES'], + milestone + ) + milestones_helpers.add_course_content_milestone( + unicode(course_id), + unicode(x_block.location), + relationship_types['FULFILLS'], + milestone + ) + + +def remove_entrance_exam_milestone_reference(request, course_key): + """ + Remove content reference for entrance exam. + """ + course_children = modulestore().get_items( + course_key, + qualifiers={'category': 'chapter'} + ) + for course_child in course_children: + if course_child.is_entrance_exam: + delete_item(request, course_child.scope_ids.usage_id) + milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id)) diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index fbb786d680..58187d3b53 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -37,6 +37,11 @@ from student.auth import has_course_author_access from openedx.core.lib.extract_tar import safetar_extractall from util.json_request import JsonResponse from util.views import ensure_valid_course_key +from models.settings.course_metadata import CourseMetadata +from contentstore.views.entrance_exam import ( + add_entrance_exam_milestone, + remove_entrance_exam_milestone_reference +) from contentstore.utils import reverse_course_url, reverse_usage_url, reverse_library_url @@ -110,6 +115,17 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_ session_status = request.session.setdefault("import_status", {}) courselike_string = unicode(courselike_key) + filename _save_request_status(request, courselike_string, 0) + + # If the course has an entrance exam then remove it and its corresponding milestone. + # current course state before import. + if root_name == COURSE_ROOT: + if courselike_module.entrance_exam_enabled: + remove_entrance_exam_milestone_reference(request, courselike_key) + log.info( + "entrance exam milestone content reference for course %s has been removed", + courselike_module.id + ) + if not filename.endswith('.tar.gz'): _save_request_status(request, courselike_string, -1) return JsonResponse( @@ -300,6 +316,22 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_ if session_status[courselike_string] != 4: _save_request_status(request, courselike_string, -abs(session_status[courselike_string])) + # status == 4 represents that course has been imported successfully. + if session_status[courselike_string] == 4 and root_name == COURSE_ROOT: + # Reload the course so we have the latest state + course = modulestore().get_course(courselike_key) + if course.entrance_exam_enabled: + entrance_exam_chapter = modulestore().get_items( + course.id, + qualifiers={'category': 'chapter'}, + settings={'is_entrance_exam': True} + )[0] + + metadata = {'entrance_exam_id': unicode(entrance_exam_chapter.location)} + CourseMetadata.update_from_dict(metadata, course, request.user) + add_entrance_exam_milestone(course.id, entrance_exam_chapter) + log.info("Course %s Entrance exam imported", course.id) + return JsonResponse({'Status': 'OK'}) elif request.method == 'GET': # assume html status_url = reverse_course_url( diff --git a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py index dfcd1de091..a773f5e0e2 100644 --- a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py +++ b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py @@ -10,7 +10,8 @@ from django.test.client import RequestFactory from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase from contentstore.utils import reverse_url -from contentstore.views.entrance_exam import create_entrance_exam, update_entrance_exam, delete_entrance_exam +from contentstore.views.entrance_exam import create_entrance_exam, update_entrance_exam, delete_entrance_exam,\ + add_entrance_exam_milestone, remove_entrance_exam_milestone_reference from contentstore.views.helpers import GRADER_TYPES from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata @@ -18,6 +19,7 @@ from opaque_keys.edx.keys import UsageKey from student.tests.factories import UserFactory from util import milestones_helpers from xmodule.modulestore.django import modulestore +from contentstore.views.helpers import create_xblock @patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True}) @@ -37,6 +39,57 @@ class EntranceExamHandlerTests(CourseTestCase): milestones_helpers.seed_milestone_relationship_types() self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types() + def test_entrance_exam_milestone_addition(self): + """ + Unit Test: test addition of entrance exam milestone content + """ + parent_locator = unicode(self.course.location) + created_block = create_xblock( + parent_locator=parent_locator, + user=self.user, + category='chapter', + display_name=('Entrance Exam'), + is_entrance_exam=True + ) + add_entrance_exam_milestone(self.course.id, created_block) + content_milestones = milestones_helpers.get_course_content_milestones( + unicode(self.course.id), + unicode(created_block.location), + self.milestone_relationship_types['FULFILLS'] + ) + self.assertTrue(len(content_milestones)) + self.assertEqual(len(milestones_helpers.get_course_milestones(self.course.id)), 1) + + def test_entrance_exam_milestone_removal(self): + """ + Unit Test: test removal of entrance exam milestone content + """ + parent_locator = unicode(self.course.location) + created_block = create_xblock( + parent_locator=parent_locator, + user=self.user, + category='chapter', + display_name=('Entrance Exam'), + is_entrance_exam=True + ) + add_entrance_exam_milestone(self.course.id, created_block) + content_milestones = milestones_helpers.get_course_content_milestones( + unicode(self.course.id), + unicode(created_block.location), + self.milestone_relationship_types['FULFILLS'] + ) + self.assertEqual(len(content_milestones), 1) + user = UserFactory() + request = RequestFactory().request() + request.user = user + remove_entrance_exam_milestone_reference(request, self.course.id) + content_milestones = milestones_helpers.get_course_content_milestones( + unicode(self.course.id), + unicode(created_block.location), + self.milestone_relationship_types['FULFILLS'] + ) + self.assertEqual(len(content_milestones), 0) + def test_contentstore_views_entrance_exam_post(self): """ Unit Test: test_contentstore_views_entrance_exam_post diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 60548896c7..b26c93407e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -26,6 +26,10 @@ from contentstore.tests.utils import CourseTestCase from openedx.core.lib.extract_tar import safetar_extractall from student import auth from student.roles import CourseInstructorRole, CourseStaffRole +from util.milestones_helpers import seed_milestone_relationship_types +from models.settings.course_metadata import CourseMetadata +from util import milestones_helpers +from xmodule.modulestore.django import modulestore TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex @@ -34,6 +38,92 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT log = logging.getLogger(__name__) +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) +class ImportEntranceExamTestCase(CourseTestCase): + """ + Unit tests for importing a course with entrance exam + """ + def setUp(self): + super(ImportEntranceExamTestCase, self).setUp() + self.url = reverse_course_url('import_handler', self.course.id) + self.content_dir = path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.content_dir) + + # Create tar test file ----------------------------------------------- + # OK course with entrance exam section: + seed_milestone_relationship_types() + entrance_exam_dir = tempfile.mkdtemp(dir=self.content_dir) + # test course being deeper down than top of tar file + embedded_exam_dir = os.path.join(entrance_exam_dir, "grandparent", "parent") + os.makedirs(os.path.join(embedded_exam_dir, "course")) + os.makedirs(os.path.join(embedded_exam_dir, "chapter")) + with open(os.path.join(embedded_exam_dir, "course.xml"), "w+") as f: + f.write('') + + with open(os.path.join(embedded_exam_dir, "course", "2013_Spring.xml"), "w+") as f: + f.write( + '' + '' + ) + + with open(os.path.join(embedded_exam_dir, "chapter", "2015_chapter_entrance_exam.xml"), "w+") as f: + f.write('') + + self.entrance_exam_tar = os.path.join(self.content_dir, "entrance_exam.tar.gz") + with tarfile.open(self.entrance_exam_tar, "w:gz") as gtar: + gtar.add(entrance_exam_dir) + + def test_import_existing_entrance_exam_course(self): + """ + Check that course is imported successfully as an entrance exam. + """ + course = self.store.get_course(self.course.id) + self.assertIsNotNone(course) + self.assertEquals(course.entrance_exam_enabled, False) + + with open(self.entrance_exam_tar) as gtar: + args = {"name": self.entrance_exam_tar, "course-data": [gtar]} + resp = self.client.post(self.url, args) + self.assertEquals(resp.status_code, 200) + course = self.store.get_course(self.course.id) + self.assertIsNotNone(course) + self.assertEquals(course.entrance_exam_enabled, True) + self.assertEquals(course.entrance_exam_minimum_score_pct, 0.7) + + def test_import_delete_pre_exiting_entrance_exam(self): + """ + Check that pre existed entrance exam content should be overwrite with the imported course. + """ + exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id)) + resp = self.client.post(exam_url, {'entrance_exam_minimum_score_pct': 0.5}, http_accept='application/json') + self.assertEqual(resp.status_code, 201) + + # Reload the test course now that the exam module has been added + self.course = modulestore().get_course(self.course.id) + metadata = CourseMetadata.fetch_all(self.course) + self.assertTrue(metadata['entrance_exam_enabled']) + self.assertIsNotNone(metadata['entrance_exam_minimum_score_pct']) + self.assertEqual(metadata['entrance_exam_minimum_score_pct']['value'], 0.5) + self.assertTrue(len(milestones_helpers.get_course_milestones(unicode(self.course.id)))) + content_milestones = milestones_helpers.get_course_content_milestones( + unicode(self.course.id), + metadata['entrance_exam_id']['value'], + milestones_helpers.get_milestone_relationship_types()['FULFILLS'] + ) + self.assertTrue(len(content_milestones)) + + # Now import entrance exam course + with open(self.entrance_exam_tar) as gtar: + args = {"name": self.entrance_exam_tar, "course-data": [gtar]} + resp = self.client.post(self.url, args) + self.assertEquals(resp.status_code, 200) + course = self.store.get_course(self.course.id) + self.assertIsNotNone(course) + self.assertEquals(course.entrance_exam_enabled, True) + self.assertEquals(course.entrance_exam_minimum_score_pct, 0.7) + + @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class ImportTestCase(CourseTestCase): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index f633e0b170..7784184a44 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -1459,7 +1459,7 @@ class TestXBlockInfo(ItemTest): self.validate_course_xblock_info(json_response, course_outline=True) @ddt.data( - (ModuleStoreEnum.Type.split, 5, 5), + (ModuleStoreEnum.Type.split, 4, 4), (ModuleStoreEnum.Type.mongo, 5, 7), ) @ddt.unpack diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py index 3cbdd2c39f..4e9b336bc8 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py @@ -108,9 +108,9 @@ class CountMongoCallsCourseTraversal(TestCase): # The line below shows the way this traversal *should* be done # (if you'll eventually access all the fields and load all the definitions anyway). (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 4), - (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 143), + (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 41), (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 143), - (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 143), + (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 41), (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, False, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4), # TODO: The call count below seems like a bug - should be 4? diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index f9b3d1cdbf..b7143e762d 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -52,7 +52,7 @@ class SequenceFields(object): "Note, you must enable Entrance Exams for this course setting to take effect." ), default=False, - scope=Scope.content, + scope=Scope.settings, ) diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py index 7fa9fa445a..ecfb0eaeac 100644 --- a/common/test/acceptance/tests/studio/test_import_export.py +++ b/common/test/acceptance/tests/studio/test_import_export.py @@ -12,6 +12,8 @@ from ...pages.studio.import_export import ExportLibraryPage, ExportCoursePage, I from ...pages.studio.library import LibraryEditPage from ...pages.studio.container import ContainerPage from ...pages.studio.overview import CourseOutlinePage +from ...pages.lms.courseware import CoursewarePage +from ...pages.lms.staff_view import StaffPage class ExportTestMixin(object): @@ -269,6 +271,51 @@ class ImportTestMixin(object): self.import_page.wait_for_tasks(fail_on='Updating') +class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest): + """ + Tests the Course import page + """ + tarball_name = 'entrance_exam_course.2015.tar.gz' + bad_tarball_name = 'bad_course.tar.gz' + import_page_class = ImportCoursePage + landing_page_class = CourseOutlinePage + + def page_args(self): + return [self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']] + + def test_course_updated_with_entrance_exam(self): + """ + Given that I visit an empty course before import + I should not see a section named 'Section' or 'Entrance Exam' + When I visit the import page + And I upload a course that has an entrance exam section named 'Entrance Exam' + And I visit the course outline page again + The section named 'Entrance Exam' should now be available. + And when I switch the view mode to student view and Visit CourseWare + Then I see one section in the sidebar that is 'Entrance Exam' + """ + self.landing_page.visit() + # Should not exist yet. + self.assertRaises(IndexError, self.landing_page.section, "Section") + self.assertRaises(IndexError, self.landing_page.section, "Entrance Exam") + self.import_page.visit() + self.import_page.upload_tarball(self.tarball_name) + self.import_page.wait_for_upload() + self.landing_page.visit() + # There should be two sections. 'Entrance Exam' and 'Section' on the landing page. + self.landing_page.section("Entrance Exam") + self.landing_page.section("Section") + + self.landing_page.view_live() + courseware = CoursewarePage(self.browser, self.course_id) + courseware.wait_for_page() + StaffPage(self.browser, self.course_id).set_staff_view_mode('Student') + self.assertEqual(courseware.num_sections, 1) + self.assertIn( + "To access course materials, you must score", courseware.entrance_exam_message_selector.text[0] + ) + + class TestCourseImport(ImportTestMixin, StudioCourseTest): """ Tests the Course import page diff --git a/common/test/data/imports/entrance_exam_course.2015.tar.gz b/common/test/data/imports/entrance_exam_course.2015.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7dbb6107461bbde430ce4a0ed82abb4bc0c8aa6b GIT binary patch literal 2804 zcmV|6^};a&u)aHZ)FRb$KpyVR8WN9NTW&I5yAy3PJ<)f+jI_ zv+Op>Oxh+h#cpT2Z3ms*L19o5Wpg7-Dk;S=1_kyv_V@Nn_D~nwvg4@n*h(`?BC%{< z56_k7h@=;1T+q(mE)$02U@V`yW1Ho(o9CBlPdB={)zuA4xA(Aa>Xx~O?7bFJCQ`x? z+N1szNs{&Tn#!w*RL^^oyn_Bj2%3t{4(adMx1hfTUlsk^r+@J{yc&nwrhm6xDyL-JNs`*{V%I2{eK7aH*CGv$F^f( zLad+{^i5s&49n2_fk}yjH?a*(`X9`Fo&JVycTJ`LThad&<&n=s5|Zg4B4c{2{SvWM zJ9&W6QNm+4q+^uvuq-n@ppRkt*tG)7x4a(3LEvHE4Lq}N28Px-d2lpisgnmv%ibUP z_dGY~QIBHFr|5%oa|J()ri$~l3f7dpa{eK%P{ojuMFG#+m{m-J5lgOj! z?23#vM9ZH83O@%lB47~wV=d(wT`pvh?}o8=ad4?WlR3XWJ^NMH^`{2b?ZKJ;G@T~& z2X91@l=7!}%^$#!_@?FbFvT_qKJ8P>FihPD`ns>1#9kg_%px|<#slHSoDLE%J=XA{ zqoI&Q!;B08ei~^=(iKV`e$L|bxg7lf>+tsh<7fHb)BLk2E-YOM)qWkoB&FJ{pB z2K0aK|Jx2$`oB&6U(&ycZ#Oaxjq86KFZ=(divPBy|4ZFfHvsRcm$Eo2^rE1jGtj^c zoP-6{==MDm%&u==)UxKl&NqMaNy3am)e%V%&@Y@W4Ehfk z&m$4a!2ZcGgILZ7x<=CMCE*MTL}0FH9Q!m}XpSBZ)93R40CW-z)3 zfV-Te@RgC>^eF%5gZ%c}SmJpFS;CrdNQ3h)DDn`6zD;So8|KEDWBaqGAn%eWdp-=Wtzj zCn@{B4KEytl*iHVq~eLAPMLwAuNr?;H5d70Ui&77nM1Y_!<5a-scnl>=DoVSQ+_K@ z$*D*Hp|r@HWwR*B(hA$h+T%w#V?5e_lyen4bGCwK$ybk_$YmerREF2{x>_|(XLX<> z5r>(~EUWnJ5_usJFp^JG!b0jFR8_PYWXZ|doWW)@$_qAQ+c{P`06`8|XAap>W#*J) zxz7GKZS85v{>uQcvj58dEBmkPzq0?z{ww>xWBX6sI7@eR3UGt{Uxsa~_)qKfzjmL` z;VAusZv`6T|JbV2-_R{n?f+^e$@g_i3o8A8jr^}{v)fb?G|T_e{!hcSZ6*J$BnK-Q zQ1V~Oe|$;#C8Lu+Zt`y<|E6ItpZ|8S(N*%_O8iF@JN+teEoX$n%O4phr}7v2Fk%9| z%o*jV1Gx`M56ufRpGr@iqcASq*nBHF6&`1Wvkx9MJ%kSE+8SaMXQ2-z5x9vFxN`Ww z!Wd0r?w3JULr#(rGAc)UP1lF$)c3&&=XBmkNY6Z?)Q5E~dP6Se0R?ce z!?^J4$KY))jFXaK2?;op1-1aFWyB>MrusQ!f`RisL*P4^#pxIFoUk3DS9t6 zXV4LOY%8Qy!;}&asHm+v_GQTd8e`d9c2zOzDkZ9Z=BS1l zy^7K6ncTRU3`$9na2n^ z3w@r;`a~AF6p>59Lg|3-gP_ORaFmBcW%M!0xHlp|Wr7lbB!#I}a6V2lH)LW2Suosr zNYaln2krWoN|ii8ERY@j_uv2VQOGXnV#sWf%fAx46;*2dQ^iU-Oi@gLQ6{;HcW0-J z0KD8VTsL+eva48V;j3Dzwe9~ge@pAYX8&&~{%2szRQtc%r$4O4csf{ILT!ZsHlV+Q z)%)Kz=|A7Bzk`W*%!^PjDx-@hmwsPz9_{&#T^-0k&$*HHTZPU)Zjo^HtuG@bv@jn(+C(*G^#|NQ-n zR=Wm5dxm~5U*td$cm-Z2x>cZ7{AKp*%RZ_N zICDc})4G$fSo7Nbs?bFGAM5KC0B!w}y7dFDY@~zn@u9QWpt`zFLPn!nU->@T1sie~O)BgaA>aunK GcmM$O$*QRU literal 0 HcmV?d00001