Skip to content

Commit

Permalink
Improve synonym functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
loevgaard committed Aug 22, 2024
1 parent 5991095 commit 0ad3a81
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 41 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
"doctrine/collections": "^1.8 || ^2.0",
"doctrine/event-manager": "^1.2 || ^2.0",
"doctrine/orm": "^2.14 || ^3.0",
"doctrine/persistence": "^2.5 || ^3.0",
Expand Down
9 changes: 6 additions & 3 deletions src/DependencyInjection/SetonoSyliusMeilisearchExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@ public function prepend(ContainerBuilder $container): void
'type' => 'string',
'label' => 'sylius.ui.locale',
],
'channel' => [
'type' => 'string',
'label' => 'sylius.ui.channel',
'channels' => [
'type' => 'twig',
'label' => 'sylius.ui.channels',
'options' => [
'template' => '@SyliusAdmin/Grid/Field/_channels.html.twig',
],
],
],
'filters' => [
Expand Down
5 changes: 4 additions & 1 deletion src/Factory/SynonymFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public function createInverseFromExisting(SynonymInterface $synonym): SynonymInt
$obj->setTerm($synonym->getSynonym());
$obj->setSynonym($synonym->getTerm());
$obj->setLocale($synonym->getLocale());
$obj->setChannel($synonym->getChannel());

foreach ($synonym->getChannels() as $channel) {
$obj->addChannel($channel);

Check warning on line 35 in src/Factory/SynonymFactory.php

View check run for this annotation

Codecov / codecov/patch

src/Factory/SynonymFactory.php#L34-L35

Added lines #L34 - L35 were not covered by tests
}

return $obj;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\EventSubscriber;
namespace Setono\SyliusMeilisearchPlugin\Form\EventSubscriber;

use Doctrine\Persistence\ManagerRegistry;
use Setono\Doctrine\ORMTrait;
Expand Down
37 changes: 37 additions & 0 deletions src/Form/Type/IndexChoiceType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Form\Type;

use Setono\SyliusMeilisearchPlugin\Config\IndexRegistryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class IndexChoiceType extends AbstractType
{
public function __construct(private readonly IndexRegistryInterface $indexRegistry)

Check warning on line 14 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L14

Added line #L14 was not covered by tests
{
}

public function configureOptions(OptionsResolver $resolver): void

Check warning on line 18 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L18

Added line #L18 was not covered by tests
{
$names = $this->indexRegistry->getNames();

Check warning on line 20 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L20

Added line #L20 was not covered by tests

$resolver->setDefaults([
'choices' => array_combine($names, $names),
'choice_label' => static fn (string $name): string => ucfirst($name),
]);

Check warning on line 25 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L22-L25

Added lines #L22 - L25 were not covered by tests
}

public function getParent(): string

Check warning on line 28 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L28

Added line #L28 was not covered by tests
{
return ChoiceType::class;

Check warning on line 30 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L30

Added line #L30 was not covered by tests
}

public function getBlockPrefix(): string

Check warning on line 33 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L33

Added line #L33 was not covered by tests
{
return 'setono_sylius_meilisearch_index_choice';

Check warning on line 35 in src/Form/Type/IndexChoiceType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/IndexChoiceType.php#L35

Added line #L35 was not covered by tests
}
}
17 changes: 15 additions & 2 deletions src/Form/Type/SynonymType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Sylius\Bundle\LocaleBundle\Form\Type\LocaleChoiceType;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
Expand Down Expand Up @@ -42,8 +43,20 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
->add('locale', LocaleChoiceType::class, [
'label' => 'sylius.ui.locale',
])
->add('channel', ChannelChoiceType::class, [
'label' => 'sylius.ui.channel',
->add('channels', ChannelChoiceType::class, [
'multiple' => true,
'expanded' => true,
'label' => 'sylius.ui.channels',
'required' => false,
])
->add('indexes', IndexChoiceType::class, [
'multiple' => true,
'expanded' => true,
'label' => 'setono_sylius_meilisearch.form.synonym.indexes',
'required' => false,
])
->add('enabled', CheckboxType::class, [
'label' => 'sylius.ui.enabled',

Check warning on line 59 in src/Form/Type/SynonymType.php

View check run for this annotation

Codecov / codecov/patch

src/Form/Type/SynonymType.php#L46-L59

Added lines #L46 - L59 were not covered by tests
'required' => false,
])
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
Expand Down
3 changes: 2 additions & 1 deletion src/Meilisearch/SynonymResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(private readonly SynonymRepositoryInterface $synonym
public function resolve(IndexScope $indexScope): array
{
// it doesn't make sense to resolve synonyms if the locale is not set
// todo I am not sure this is true. You could have a single index with no scope and you'd still want synonyms
if (null === $indexScope->localeCode) {
return [];
}
Expand All @@ -25,7 +26,7 @@ public function resolve(IndexScope $indexScope): array
$synonyms = array_map(static fn (SynonymInterface $synonym): array => [
'term' => (string) $synonym->getTerm(),
'synonym' => (string) $synonym->getSynonym(),
], $this->synonymRepository->findByLocaleAndChannel($indexScope->localeCode, $indexScope->channelCode));
], $this->synonymRepository->findEnabledByIndexScope($indexScope));

Check warning on line 29 in src/Meilisearch/SynonymResolver.php

View check run for this annotation

Codecov / codecov/patch

src/Meilisearch/SynonymResolver.php#L29

Added line #L29 was not covered by tests

$resolvedSynonyms = [];
foreach ($synonyms as $synonym) {
Expand Down
64 changes: 59 additions & 5 deletions src/Model/Synonym.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

namespace Setono\SyliusMeilisearchPlugin\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Sylius\Component\Channel\Model\ChannelInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Model\ToggleableTrait;
use function Symfony\Component\String\u;

class Synonym implements SynonymInterface
{
use ToggleableTrait;

protected ?int $id = null;

protected ?string $term = null;
Expand All @@ -18,7 +23,16 @@ class Synonym implements SynonymInterface

protected ?LocaleInterface $locale = null;

protected ?ChannelInterface $channel = null;
/** @var Collection<array-key, ChannelInterface> */
protected Collection $channels;

/** @var list<string>|null */
protected ?array $indexes = null;

public function __construct()

Check warning on line 32 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L32

Added line #L32 was not covered by tests
{
$this->channels = new ArrayCollection();

Check warning on line 34 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L34

Added line #L34 was not covered by tests
}

public function getId(): ?int
{
Expand Down Expand Up @@ -55,13 +69,53 @@ public function setLocale(?LocaleInterface $locale): void
$this->locale = $locale;
}

public function getChannel(): ?ChannelInterface
public function getChannels(): Collection

Check warning on line 72 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L72

Added line #L72 was not covered by tests
{
return $this->channels;

Check warning on line 74 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L74

Added line #L74 was not covered by tests
}

public function addChannel(ChannelInterface $channel): void

Check warning on line 77 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L77

Added line #L77 was not covered by tests
{
if (!$this->hasChannel($channel)) {
$this->channels->add($channel);

Check warning on line 80 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L79-L80

Added lines #L79 - L80 were not covered by tests
}
}

public function removeChannel(ChannelInterface $channel): void

Check warning on line 84 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L84

Added line #L84 was not covered by tests
{
if ($this->hasChannel($channel)) {
$this->channels->removeElement($channel);

Check warning on line 87 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L86-L87

Added lines #L86 - L87 were not covered by tests
}
}

public function hasChannel(ChannelInterface $channel): bool

Check warning on line 91 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L91

Added line #L91 was not covered by tests
{
return $this->channels->contains($channel);

Check warning on line 93 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L93

Added line #L93 was not covered by tests
}

public function getIndexes(): array

Check warning on line 96 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L96

Added line #L96 was not covered by tests
{
return $this->indexes ?? [];

Check warning on line 98 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L98

Added line #L98 was not covered by tests
}

public function addIndex(string $index): void

Check warning on line 101 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L101

Added line #L101 was not covered by tests
{
$this->indexes[] = $index;

Check warning on line 103 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L103

Added line #L103 was not covered by tests
}

public function removeIndex(string $index): void

Check warning on line 106 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L106

Added line #L106 was not covered by tests
{
return $this->channel;
$indexes = $this->getIndexes();
$key = array_search($index, $indexes, true);
if ($key !== false) {
unset($indexes[$key]);

Check warning on line 111 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L108-L111

Added lines #L108 - L111 were not covered by tests
}

$this->indexes = [] === $indexes ? null : array_values($indexes);

Check warning on line 114 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L114

Added line #L114 was not covered by tests
}

public function setChannel(?ChannelInterface $channel): void
public function hasIndex(string $index): bool

Check warning on line 117 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L117

Added line #L117 was not covered by tests
{
$this->channel = $channel;
return in_array($index, $this->getIndexes(), true);

Check warning on line 119 in src/Model/Synonym.php

View check run for this annotation

Codecov / codecov/patch

src/Model/Synonym.php#L119

Added line #L119 was not covered by tests
}
}
18 changes: 12 additions & 6 deletions src/Model/SynonymInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

namespace Setono\SyliusMeilisearchPlugin\Model;

use Sylius\Component\Channel\Model\ChannelAwareInterface;
use Sylius\Component\Channel\Model\ChannelInterface;
use Sylius\Component\Channel\Model\ChannelsAwareInterface;
use Sylius\Component\Locale\Model\LocaleInterface;
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Component\Resource\Model\ToggleableInterface;

interface SynonymInterface extends ResourceInterface, ChannelAwareInterface
interface SynonymInterface extends ResourceInterface, ChannelsAwareInterface, ToggleableInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

These ancestors of Setono\SyliusMeilisearchPlugin\Model\SynonymInterface have been removed: ["Sylius\\Component\\Channel\\Model\\ChannelAwareInterface"]

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method getIndexes() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method addIndex() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method removeIndex() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method hasIndex() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method getChannels() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method hasChannel() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method addChannel() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method removeChannel() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface

Check failure on line 12 in src/Model/SynonymInterface.php

View workflow job for this annotation

GitHub Actions / Backwards Compatibility Check

Method isEnabled() was added to interface Setono\SyliusMeilisearchPlugin\Model\SynonymInterface
{
public function getId(): ?int;

Expand All @@ -25,8 +25,14 @@ public function getLocale(): ?LocaleInterface;

public function setLocale(?LocaleInterface $locale): void;

// todo can we think of a scenario where we _really_ need the channel?
public function getChannel(): ?ChannelInterface;
/**
* @return list<string>
*/
public function getIndexes(): array;

public function setChannel(?ChannelInterface $channel): void;
public function addIndex(string $index): void;

public function removeIndex(string $index): void;

public function hasIndex(string $index): bool;
}
21 changes: 14 additions & 7 deletions src/Repository/SynonymRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
namespace Setono\SyliusMeilisearchPlugin\Repository;

use Setono\SyliusMeilisearchPlugin\Model\SynonymInterface;
use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScope;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Webmozart\Assert\Assert;

class SynonymRepository extends EntityRepository implements SynonymRepositoryInterface
{
public function findByLocaleAndChannel(string $localeCode, string $channelCode = null): array
public function findEnabledByIndexScope(IndexScope $indexScope): array

Check warning on line 14 in src/Repository/SynonymRepository.php

View check run for this annotation

Codecov / codecov/patch

src/Repository/SynonymRepository.php#L14

Added line #L14 was not covered by tests
{
$qb = $this->createQueryBuilder('o')
->join('o.locale', 'locale', 'WITH', 'locale.code = :localeCode')
->setParameter('localeCode', $localeCode)
->andWhere('o.enabled = true')
->andWhere('o.indexes LIKE :index')
->setParameter('index', '%"' . $indexScope->index->name . '"%')

Check warning on line 19 in src/Repository/SynonymRepository.php

View check run for this annotation

Codecov / codecov/patch

src/Repository/SynonymRepository.php#L17-L19

Added lines #L17 - L19 were not covered by tests
;

if (null !== $channelCode) {
$qb->leftJoin('o.channel', 'c')
->andWhere('o.channel IS NULL OR c.code = :channelCode')
->setParameter('channelCode', $channelCode)
if (null !== $indexScope->localeCode) {
$qb->join('o.locale', 'locale', 'WITH', 'locale.code = :localeCode')
->setParameter('localeCode', $indexScope->localeCode)
;

Check warning on line 25 in src/Repository/SynonymRepository.php

View check run for this annotation

Codecov / codecov/patch

src/Repository/SynonymRepository.php#L22-L25

Added lines #L22 - L25 were not covered by tests
}

if (null !== $indexScope->channelCode) {
$qb->join('o.channels', 'c', 'WITH', 'c.code = :channelCode')
->setParameter('channelCode', $indexScope->channelCode)

Check warning on line 30 in src/Repository/SynonymRepository.php

View check run for this annotation

Codecov / codecov/patch

src/Repository/SynonymRepository.php#L28-L30

Added lines #L28 - L30 were not covered by tests
;
}

Expand Down
3 changes: 2 additions & 1 deletion src/Repository/SynonymRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Setono\SyliusMeilisearchPlugin\Repository;

use Setono\SyliusMeilisearchPlugin\Model\SynonymInterface;
use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScope;
use Sylius\Component\Resource\Repository\RepositoryInterface;

/**
Expand All @@ -15,5 +16,5 @@ interface SynonymRepositoryInterface extends RepositoryInterface
/**
* @return array<array-key, SynonymInterface>
*/
public function findByLocaleAndChannel(string $localeCode, string $channelCode = null): array;
public function findEnabledByIndexScope(IndexScope $indexScope): array;
}
17 changes: 13 additions & 4 deletions src/Resources/config/doctrine/model/Synonym.orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,22 @@

<field name="term" type="string"/>
<field name="synonym" type="string"/>

<many-to-one field="channel" target-entity="Sylius\Component\Channel\Model\ChannelInterface">
<join-column name="channel_id" referenced-column-name="id" nullable="true"/>
</many-to-one>
<field name="enabled" type="boolean" />
<field name="indexes" type="json" nullable="true"/>

<many-to-one field="locale" target-entity="Sylius\Component\Locale\Model\LocaleInterface">
<join-column name="locale_id" referenced-column-name="id" nullable="false"/>
</many-to-one>

<many-to-many field="channels" target-entity="Sylius\Component\Channel\Model\ChannelInterface">
<join-table name="setono_sylius_meilisearch__synonym_channels">
<join-columns>
<join-column name="synonym_id" referenced-column-name="id" nullable="false" on-delete="CASCADE"/>
</join-columns>
<inverse-join-columns>
<join-column name="channel_id" referenced-column-name="id" nullable="false" on-delete="CASCADE"/>
</inverse-join-columns>
</join-table>
</many-to-many>
</mapped-superclass>
</doctrine-mapping>
7 changes: 0 additions & 7 deletions src/Resources/config/services/event_subscriber.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,5 @@
<service id="Setono\SyliusMeilisearchPlugin\EventSubscriber\AddMenuSubscriber">
<tag name="kernel.event_subscriber"/>
</service>

<service id="Setono\SyliusMeilisearchPlugin\EventSubscriber\NewSynonymSubscriber">
<argument type="service" id="doctrine"/>
<argument type="service" id="setono_sylius_meilisearch.factory.synonym"/>

<tag name="kernel.event_subscriber"/>
</service>
</services>
</container>
16 changes: 15 additions & 1 deletion src/Resources/config/services/form.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,32 @@
</service>

<!-- Types -->
<service id="Setono\SyliusMeilisearchPlugin\Form\Type\IndexChoiceType">
<argument type="service" id="setono_sylius_meilisearch.config.index_registry"/>

<tag name="form.type"/>
</service>

<service id="Setono\SyliusMeilisearchPlugin\Form\Type\SearchWidgetType">
<argument type="service" id="request_stack"/>

<tag name="form.type"/>
</service>

<service id="Setono\SyliusMeilisearchPlugin\Form\Type\SynonymType">
<argument type="service" id="Setono\SyliusMeilisearchPlugin\EventSubscriber\NewSynonymSubscriber"/>
<argument type="service" id="Setono\SyliusMeilisearchPlugin\Form\EventSubscriber\NewSynonymSubscriber"/>
<argument>%setono_sylius_meilisearch.model.synonym.class%</argument>
<argument>%setono_sylius_meilisearch.form.type.synonym.validation_groups%</argument>

<tag name="form.type"/>
</service>

<!-- Event subscribers -->
<service id="Setono\SyliusMeilisearchPlugin\Form\EventSubscriber\NewSynonymSubscriber">
<argument type="service" id="doctrine"/>
<argument type="service" id="setono_sylius_meilisearch.factory.synonym"/>

<tag name="kernel.event_subscriber"/>
</service>
</services>
</container>
1 change: 1 addition & 0 deletions src/Resources/translations/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ setono_sylius_meilisearch:
label: Direction
one_way: One way
two_way: Two way
indexes: Indexes
menu:
admin:
main:
Expand Down
Loading

0 comments on commit 0ad3a81

Please sign in to comment.