-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Merge pull request #533 from BitBagCommerce/feature/OP-525
OP-525: Content migration script
Showing
30 changed files
with
493 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
code,type,name_en_US,content_en_US,sections,channels,products,image_en_US,slug_en_US | ||
test4,image,Test,test,"blog, general",US_WEB,"010ba66b-adee-3d6e-9d63-67c44d686db1, 01d35db9-247d-3834-b300-20483d5e34e8",https://bitbag.shop/assets/web/images/header-logo.png,https://bitbag.shop/assets/web/images/header-logo.png |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Legacy data migration | ||
|
||
## Introduction | ||
|
||
You can migrate your blocks & pages from the 4.x version to the 5.x version of the plugin. | ||
To do so, you need to follow the steps below. | ||
|
||
## Steps | ||
|
||
1. Create new CSV files with blocks & pages data in the 4.x format. | ||
See an example in [block_legacy.csv](block_legacy.csv) or [page_legacy.csv](page_legacy.csv). | ||
2. Install the 5.x version of the plugin. | ||
3. Go to the console and run the following command: | ||
```bash | ||
bin/console cms:import:csv page_legacy {file_path}.csv | ||
bin/console cms:import:csv block_legacy {file_path}.csv | ||
``` | ||
|
||
## Info about legacy CSV files columns | ||
|
||
### Blocks | ||
|
||
- **code** - block code. | ||
- **type** - it will be ignored. | ||
- **name_LOCALE** - block name. First occurrence of its column is the default name for the block. | ||
For each locale, there will be created a Heading content element. | ||
- **content_LOCALE** - block content. For each locale, there will be created a Textarea content element. | ||
- **sections** - it will be converted to the block's collections. | ||
- **channels** - block channels. | ||
- **products** - block products. There will be created Products grid content element. | ||
- **image_LOCALE** - block image. For each locale, there will be created a Single media content element. | ||
- **slug_LOCALE** - it will be ignored. | ||
|
||
### Pages | ||
|
||
- **code** - page code. | ||
- **sections** - it will be converted to the page's collections. | ||
- **channels** - page channels. | ||
- **products** - page products. There will be created Products grid content element. | ||
- **slug_LOCALE** - page slug. | ||
- **name_LOCALE** - page name. First occurrence of its column is the default name for the page. | ||
For each locale, there will be created a Heading content element. | ||
- **image_LOCALE** - page image. For each locale, there will be created a Single media content element. | ||
- **meta_keywords_LOCALE** - page meta keywords. | ||
- **meta_description_LOCALE** - page meta description. | ||
- **content_LOCALE** - page content. For each locale, there will be created a Textarea content element. | ||
- **breadcrumb_LOCALE** - it will be ignored. | ||
- **name_when_linked_LOCALE** - for each locale, there will be created a teaser title. | ||
- **description_when_linked_LOCALE** - for each locale, there will be created a teaser content. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
code,sections,channels,products,slug_en_US,name_en_US,image_en_US,meta_keywords_en_US,meta_description_en_US,content_en_US,breadcrumb_en_US,name_when_linked_en_US,description_when_linked_en_US | ||
aboutUS,,US_WEB,,about_us,About US,,About US,About US,"",,, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Factory; | ||
|
||
use Sylius\CmsPlugin\Entity\ContentConfiguration; | ||
|
||
final class ContentElementFactory | ||
{ | ||
public static function createHeadingContentElement( | ||
?string $locale, | ||
?string $headingType, | ||
?string $headingContent, | ||
): ?ContentConfiguration { | ||
if (null === $headingContent) { | ||
return null; | ||
} | ||
|
||
$contentConfiguration = new ContentConfiguration(); | ||
$contentConfiguration->setLocale($locale ?? 'en_US'); | ||
$contentConfiguration->setType('heading'); | ||
$contentConfiguration->setConfiguration([ | ||
'heading_type' => $headingType ?? 'h1', | ||
'heading' => $headingContent, | ||
]); | ||
|
||
return $contentConfiguration; | ||
} | ||
|
||
public static function createTextareaContentElement(?string $locale, ?string $content): ?ContentConfiguration | ||
{ | ||
if (null === $content) { | ||
return null; | ||
} | ||
|
||
$contentConfiguration = new ContentConfiguration(); | ||
$contentConfiguration->setLocale($locale ?? 'en_US'); | ||
$contentConfiguration->setType('textarea'); | ||
$contentConfiguration->setConfiguration([ | ||
'textarea' => $content, | ||
]); | ||
|
||
return $contentConfiguration; | ||
} | ||
|
||
public static function createProductsGridContentElement(?string $locale, ?string $codes): ?ContentConfiguration | ||
{ | ||
if (null === $codes) { | ||
return null; | ||
} | ||
|
||
$productsCodes = explode(',', $codes); | ||
$productsCodes = array_map(static function (string $element): string { | ||
return trim($element); | ||
}, $productsCodes); | ||
|
||
$contentConfiguration = new ContentConfiguration(); | ||
$contentConfiguration->setLocale($locale ?? 'en_US'); | ||
$contentConfiguration->setType('products_grid'); | ||
$contentConfiguration->setConfiguration([ | ||
'products_grid' => [ | ||
'products' => $productsCodes, | ||
], | ||
]); | ||
|
||
return $contentConfiguration; | ||
} | ||
|
||
public static function createSingleMediaContentElement(?string $locale, ?string $code): ?ContentConfiguration | ||
{ | ||
if (null === $code) { | ||
return null; | ||
} | ||
|
||
$contentConfiguration = new ContentConfiguration(); | ||
$contentConfiguration->setLocale($locale ?? 'en_US'); | ||
$contentConfiguration->setType('single_media'); | ||
$contentConfiguration->setConfiguration([ | ||
'single_media' => $code, | ||
]); | ||
|
||
return $contentConfiguration; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Importer\Legacy; | ||
|
||
use Sylius\CmsPlugin\Entity\BlockInterface; | ||
use Sylius\CmsPlugin\Factory\ContentElementFactory; | ||
use Sylius\CmsPlugin\Importer\AbstractImporter; | ||
use Sylius\CmsPlugin\Repository\BlockRepositoryInterface; | ||
use Sylius\CmsPlugin\Resolver\Importer\ImporterChannelsResolverInterface; | ||
use Sylius\CmsPlugin\Resolver\Importer\ImporterCollectionsResolverInterface; | ||
use Sylius\CmsPlugin\Resolver\ResourceResolverInterface; | ||
use Sylius\Component\Locale\Model\LocaleInterface; | ||
use Sylius\Component\Resource\Repository\RepositoryInterface; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
use Webmozart\Assert\Assert; | ||
|
||
final class LegacyBlockImporter extends AbstractImporter implements LegacyBlockImporterInterface | ||
{ | ||
public function __construct( | ||
private ResourceResolverInterface $blockResourceResolver, | ||
private ImporterCollectionsResolverInterface $importerCollectionsResolver, | ||
private ImporterChannelsResolverInterface $importerChannelsResolver, | ||
private BlockRepositoryInterface $blockRepository, | ||
private RepositoryInterface $localeRepository, | ||
ValidatorInterface $validator, | ||
) { | ||
parent::__construct($validator); | ||
} | ||
|
||
public function import(array $row): void | ||
{ | ||
/** @var string|null $code */ | ||
$code = $this->getColumnValue(self::CODE_COLUMN, $row); | ||
Assert::notNull($code); | ||
/** @var BlockInterface $block */ | ||
$block = $this->blockResourceResolver->getResource($code); | ||
$block->setCode($code); | ||
|
||
$this->importerCollectionsResolver->resolve($block, $this->getColumnValue(self::SECTIONS_COLUMN, $row)); | ||
$this->importerChannelsResolver->resolve($block, $this->getColumnValue(self::CHANNELS_COLUMN, $row)); | ||
|
||
$translationArray = $this->getAvailableLocales($this->getTranslatableColumns(), array_keys($row)); | ||
foreach ($translationArray as $key => $locale) { | ||
if ($key === array_key_first($translationArray)) { | ||
$block->setName($this->getTranslatableColumnValue(self::NAME_COLUMN, $locale, $row)); | ||
} | ||
|
||
$heading = ContentElementFactory::createHeadingContentElement( | ||
$locale, | ||
'h2', | ||
$this->getTranslatableColumnValue(self::NAME_COLUMN, $locale, $row), | ||
); | ||
if ($heading) { | ||
$heading->setBlock($block); | ||
$block->addContentElement($heading); | ||
} | ||
|
||
$singleMedia = ContentElementFactory::createSingleMediaContentElement( | ||
$locale, | ||
$this->getTranslatableColumnValue(self::IMAGE_COLUMN, $locale, $row), | ||
); | ||
if ($singleMedia) { | ||
$singleMedia->setBlock($block); | ||
$block->addContentElement($singleMedia); | ||
} | ||
|
||
$content = ContentElementFactory::createTextareaContentElement( | ||
$locale, | ||
$this->getTranslatableColumnValue(self::CONTENT_COLUMN, $locale, $row), | ||
); | ||
if ($content) { | ||
$content->setBlock($block); | ||
$block->addContentElement($content); | ||
} | ||
} | ||
|
||
$locales = $this->localeRepository->findAll(); | ||
/** @var LocaleInterface $locale */ | ||
foreach ($locales as $locale) { | ||
$productsGrid = ContentElementFactory::createProductsGridContentElement( | ||
$locale->getCode(), | ||
$this->getColumnValue(self::PRODUCTS_COLUMN, $row), | ||
); | ||
if ($productsGrid) { | ||
$productsGrid->setBlock($block); | ||
$block->addContentElement($productsGrid); | ||
} | ||
} | ||
|
||
$this->validateResource($block, ['cms']); | ||
$this->blockRepository->add($block); | ||
} | ||
|
||
public function getResourceCode(): string | ||
{ | ||
return 'block_legacy'; | ||
} | ||
|
||
private function getTranslatableColumns(): array | ||
{ | ||
return [ | ||
self::NAME_COLUMN, | ||
self::CONTENT_COLUMN, | ||
self::IMAGE_COLUMN, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Importer\Legacy; | ||
|
||
use Sylius\CmsPlugin\Importer\ImporterInterface; | ||
|
||
interface LegacyBlockImporterInterface extends ImporterInterface | ||
{ | ||
public const CODE_COLUMN = 'code'; | ||
|
||
public const SECTIONS_COLUMN = 'sections'; | ||
|
||
public const CHANNELS_COLUMN = 'channels'; | ||
|
||
public const PRODUCTS_COLUMN = 'products'; | ||
|
||
public const NAME_COLUMN = 'name__locale__'; | ||
|
||
public const CONTENT_COLUMN = 'content__locale__'; | ||
|
||
public const IMAGE_COLUMN = 'image__locale__'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Importer\Legacy; | ||
|
||
use Doctrine\ORM\EntityManagerInterface; | ||
use Sylius\CmsPlugin\Entity\PageInterface; | ||
use Sylius\CmsPlugin\Factory\ContentElementFactory; | ||
use Sylius\CmsPlugin\Importer\AbstractImporter; | ||
use Sylius\CmsPlugin\Resolver\Importer\ImporterChannelsResolverInterface; | ||
use Sylius\CmsPlugin\Resolver\Importer\ImporterCollectionsResolverInterface; | ||
use Sylius\CmsPlugin\Resolver\ResourceResolverInterface; | ||
use Sylius\Component\Locale\Context\LocaleContextInterface; | ||
use Sylius\Component\Locale\Model\LocaleInterface; | ||
use Sylius\Component\Resource\Repository\RepositoryInterface; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
use Webmozart\Assert\Assert; | ||
|
||
final class LegacyPageImporter extends AbstractImporter implements LegacyPageImporterInterface | ||
{ | ||
public function __construct( | ||
private ResourceResolverInterface $pageResourceResolver, | ||
private LocaleContextInterface $localeContext, | ||
private ImporterCollectionsResolverInterface $importerCollectionsResolver, | ||
private ImporterChannelsResolverInterface $importerChannelsResolver, | ||
private EntityManagerInterface $entityManager, | ||
private RepositoryInterface $localeRepository, | ||
ValidatorInterface $validator, | ||
) { | ||
parent::__construct($validator); | ||
} | ||
|
||
public function import(array $row): void | ||
{ | ||
/** @var string|null $code */ | ||
$code = $this->getColumnValue(self::CODE_COLUMN, $row); | ||
Assert::notNull($code); | ||
|
||
/** @var PageInterface $page */ | ||
$page = $this->pageResourceResolver->getResource($code); | ||
|
||
$page->setCode($code); | ||
$page->setFallbackLocale($this->localeContext->getLocaleCode()); | ||
|
||
$this->importerCollectionsResolver->resolve($page, $this->getColumnValue(self::SECTIONS_COLUMN, $row)); | ||
$this->importerChannelsResolver->resolve($page, $this->getColumnValue(self::CHANNELS_COLUMN, $row)); | ||
|
||
$translationArray = $this->getAvailableLocales($this->getTranslatableColumns(), array_keys($row)); | ||
foreach ($translationArray as $key => $locale) { | ||
$page->setCurrentLocale($locale); | ||
$page->setSlug($this->getTranslatableColumnValue(self::SLUG_COLUMN, $locale, $row)); | ||
$page->setMetaKeywords($this->getTranslatableColumnValue(self::META_KEYWORDS_COLUMN, $locale, $row)); | ||
$page->setMetaDescription($this->getTranslatableColumnValue(self::META_DESCRIPTION_COLUMN, $locale, $row)); | ||
|
||
if ($key === array_key_first($translationArray)) { | ||
$page->setName($this->getTranslatableColumnValue(self::NAME_COLUMN, $locale, $row)); | ||
} | ||
|
||
$page->setTeaserTitle($this->getTranslatableColumnValue(self::NAME_WHEN_LINKED_COLUMN, $locale, $row)); | ||
$page->setTeaserContent($this->getTranslatableColumnValue(self::DESCRIPTION_WHEN_LINKED_COLUMN, $locale, $row)); | ||
|
||
$heading = ContentElementFactory::createHeadingContentElement( | ||
$locale, | ||
'h2', | ||
$this->getTranslatableColumnValue(self::NAME_COLUMN, $locale, $row), | ||
); | ||
if ($heading) { | ||
$heading->setPage($page); | ||
$page->addContentElement($heading); | ||
} | ||
|
||
$singleMedia = ContentElementFactory::createSingleMediaContentElement( | ||
$locale, | ||
$this->getTranslatableColumnValue(self::IMAGE_COLUMN, $locale, $row), | ||
); | ||
if ($singleMedia) { | ||
$singleMedia->setPage($page); | ||
$page->addContentElement($singleMedia); | ||
} | ||
|
||
$content = ContentElementFactory::createTextareaContentElement( | ||
$locale, | ||
$this->getTranslatableColumnValue(self::CONTENT_COLUMN, $locale, $row), | ||
); | ||
if ($content) { | ||
$content->setPage($page); | ||
$page->addContentElement($content); | ||
} | ||
} | ||
|
||
$locales = $this->localeRepository->findAll(); | ||
/** @var LocaleInterface $locale */ | ||
foreach ($locales as $locale) { | ||
$productsGrid = ContentElementFactory::createProductsGridContentElement( | ||
$locale->getCode(), | ||
$this->getColumnValue(self::PRODUCTS_COLUMN, $row), | ||
); | ||
if ($productsGrid) { | ||
$productsGrid->setPage($page); | ||
$page->addContentElement($productsGrid); | ||
} | ||
} | ||
|
||
$this->validateResource($page, ['cms']); | ||
|
||
$this->entityManager->persist($page); | ||
$this->entityManager->flush(); | ||
} | ||
|
||
public function getResourceCode(): string | ||
{ | ||
return 'page_legacy'; | ||
} | ||
|
||
private function getTranslatableColumns(): array | ||
{ | ||
return [ | ||
self::SLUG_COLUMN, | ||
self::NAME_COLUMN, | ||
self::IMAGE_COLUMN, | ||
self::CONTENT_COLUMN, | ||
self::META_KEYWORDS_COLUMN, | ||
self::META_DESCRIPTION_COLUMN, | ||
self::NAME_WHEN_LINKED_COLUMN, | ||
self::DESCRIPTION_WHEN_LINKED_COLUMN, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Importer\Legacy; | ||
|
||
use Sylius\CmsPlugin\Importer\ImporterInterface; | ||
|
||
interface LegacyPageImporterInterface extends ImporterInterface | ||
{ | ||
public const CODE_COLUMN = 'code'; | ||
|
||
public const SECTIONS_COLUMN = 'sections'; | ||
|
||
public const CHANNELS_COLUMN = 'channels'; | ||
|
||
public const PRODUCTS_COLUMN = 'products'; | ||
|
||
public const SLUG_COLUMN = 'slug__locale__'; | ||
|
||
public const NAME_COLUMN = 'name__locale__'; | ||
|
||
public const IMAGE_COLUMN = 'image__locale__'; | ||
|
||
public const META_KEYWORDS_COLUMN = 'meta_keywords__locale__'; | ||
|
||
public const META_DESCRIPTION_COLUMN = 'meta_description__locale__'; | ||
|
||
public const CONTENT_COLUMN = 'content__locale__'; | ||
|
||
public const NAME_WHEN_LINKED_COLUMN = 'name_when_linked__locale__'; | ||
|
||
public const DESCRIPTION_WHEN_LINKED_COLUMN = 'description_when_linked__locale__'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Sylius\CmsPlugin\Migrations; | ||
|
||
use Doctrine\DBAL\Schema\Schema; | ||
use Doctrine\Migrations\AbstractMigration; | ||
|
||
final class Version20240916100051 extends AbstractMigration | ||
{ | ||
public function getDescription(): string | ||
{ | ||
return 'This migration removes the sylius_cms_block_locales table.'; | ||
} | ||
|
||
public function up(Schema $schema): void | ||
{ | ||
$this->addSql('ALTER TABLE sylius_cms_block_locales DROP FOREIGN KEY FK_49C0AACE559DFD1'); | ||
$this->addSql('ALTER TABLE sylius_cms_block_locales DROP FOREIGN KEY FK_49C0AACE9ED820C'); | ||
$this->addSql('DROP TABLE sylius_cms_block_locales'); | ||
} | ||
|
||
public function down(Schema $schema): void | ||
{ | ||
$this->addSql('CREATE TABLE sylius_cms_block_locales (block_id INT NOT NULL, locale_id INT NOT NULL, INDEX IDX_49C0AACE9ED820C (block_id), INDEX IDX_49C0AACE559DFD1 (locale_id), PRIMARY KEY(block_id, locale_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); | ||
$this->addSql('ALTER TABLE sylius_cms_block_locales ADD CONSTRAINT FK_49C0AACE559DFD1 FOREIGN KEY (locale_id) REFERENCES sylius_locale (id) ON DELETE CASCADE'); | ||
$this->addSql('ALTER TABLE sylius_cms_block_locales ADD CONSTRAINT FK_49C0AACE9ED820C FOREIGN KEY (block_id) REFERENCES sylius_cms_block (id) ON DELETE CASCADE'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters