diff --git a/symfony/config/fuzzrake/payments.yaml b/symfony/config/fuzzrake/payments.yaml index 7ba4f303d..d4f0527b5 100644 --- a/symfony/config/fuzzrake/payments.yaml +++ b/symfony/config/fuzzrake/payments.yaml @@ -1,17 +1,4 @@ parameters: - noPayPlans: - regex_prefix: "^" - regex_suffix: "$" - replacements: [] # grep-payment-plans-none - # FIXME: https://github.com/veelkoov/fuzzrake/issues/305 - # "None/100% up ?front": "None" - # '"None, 100% upfront"': "None" - # "None": "None" - # "No payment plans/100% upfront": "None" - # "100% upfront every part": "None" - # '100% Up ?Front\.?': "None" - # "1 complete payment": "None" - currencies: regex_prefix: '(?<=^|\n)' regex_suffix: '(?=\n|$)' diff --git a/symfony/global/functions.php b/symfony/global/functions.php index 0e8f7b33f..7d1b1ab6c 100644 --- a/symfony/global/functions.php +++ b/symfony/global/functions.php @@ -135,7 +135,7 @@ function iter_sortl(iterable $iterable): array * * @param array $array * - * @return V|null + * @return ($array is non-empty-array ? V : null) */ function array_first(array $array): mixed { diff --git a/symfony/migrations/Version20250816154014.php b/symfony/migrations/Version20250816154014.php new file mode 100644 index 000000000..f61591fef --- /dev/null +++ b/symfony/migrations/Version20250816154014.php @@ -0,0 +1,59 @@ +addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__creators AS SELECT id, creator_id, name, formerly, intro, since, country, state, city, payment_plans, species_does, species_doesnt, notes, contact_allowed, inactive_reason, production_models_comment, styles_comment, order_types_comment, features_comment, payment_methods, currencies_accepted, species_comment FROM creators + SQL); + $this->addSql('DROP TABLE creators'); + $this->addSql(<<<'SQL' + CREATE TABLE creators (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, creator_id CLOB NOT NULL, name CLOB NOT NULL, formerly CLOB NOT NULL, intro CLOB NOT NULL, since CLOB NOT NULL, country CLOB NOT NULL, state CLOB NOT NULL, city CLOB NOT NULL, allergy_warning_info CLOB NOT NULL, species_does CLOB NOT NULL, species_doesnt CLOB NOT NULL, notes CLOB NOT NULL, contact_allowed CLOB DEFAULT NULL, inactive_reason CLOB NOT NULL, production_models_comment CLOB NOT NULL, styles_comment CLOB NOT NULL, order_types_comment CLOB NOT NULL, features_comment CLOB NOT NULL, payment_methods CLOB NOT NULL, currencies_accepted CLOB NOT NULL, species_comment CLOB NOT NULL, ages CLOB DEFAULT NULL, has_allergy_warning BOOLEAN DEFAULT NULL, offers_payment_plans BOOLEAN DEFAULT NULL, payment_plans_info CLOB NOT NULL) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO creators (id, creator_id, name, formerly, intro, since, country, state, city, allergy_warning_info, species_does, species_doesnt, notes, contact_allowed, inactive_reason, production_models_comment, styles_comment, order_types_comment, features_comment, payment_methods, currencies_accepted, species_comment, payment_plans_info) SELECT id, creator_id, name, formerly, intro, since, country, state, city, '', species_does, species_doesnt, notes, contact_allowed, inactive_reason, production_models_comment, styles_comment, order_types_comment, features_comment, payment_methods, currencies_accepted, species_comment, payment_plans FROM __temp__creators + SQL); + $this->addSql(<<<'SQL' + DROP TABLE __temp__creators + SQL); + $this->addSql(<<<'SQL' + UPDATE creators SET offers_payment_plans = true WHERE payment_plans_info <> '' + SQL); + $this->addSql(<<<'SQL' + UPDATE creators SET offers_payment_plans = false WHERE payment_plans_info = 'None' + SQL); + $this->addSql(<<<'SQL' + UPDATE creators SET payment_plans_info = '' WHERE payment_plans_info = 'None' + SQL); + $this->addSql(<<<'SQL' + UPDATE creators SET payment_plans_info = '• ' || REPLACE(payment_plans_info, char(10), char(10) || '• ') WHERE payment_plans_info LIKE '%' || char(10) || '%' + SQL); + $this->addSql(<<<'SQL' + UPDATE creators SET ages = (SELECT value FROM creators_values AS cv WHERE cv.creator_id = creators.id AND cv.field_name = 'AGES') + SQL); + $this->addSql(<<<'SQL' + DELETE FROM creators_values WHERE field_name = 'AGES' + SQL); + } + + #[Override] + public function down(Schema $schema): void + { + $this->throwIrreversibleMigrationException(); // Restore the backup. + } +} diff --git a/symfony/src/Controller/MainController.php b/symfony/src/Controller/MainController.php index c774fd18b..1e29e3580 100644 --- a/symfony/src/Controller/MainController.php +++ b/symfony/src/Controller/MainController.php @@ -5,9 +5,9 @@ namespace App\Controller; use App\Controller\Traits\CreatorByCreatorIdTrait; -use App\Filtering\DataRequests\FilteredDataProvider; -use App\Filtering\DataRequests\RequestParser; use App\Filtering\FiltersData\FiltersService; +use App\Filtering\RequestsHandling\FilteredDataProvider; +use App\Filtering\RequestsHandling\RequestParser; use App\Repository\CreatorRepository; use App\Service\DataService; use App\Utils\Creator\CreatorId; diff --git a/symfony/src/Data/Definitions/Fields/Field.php b/symfony/src/Data/Definitions/Fields/Field.php index 93729759f..bb5f51971 100644 --- a/symfony/src/Data/Definitions/Fields/Field.php +++ b/symfony/src/Data/Definitions/Fields/Field.php @@ -81,8 +81,17 @@ enum Field: string // Backing by strings gives free ::from() and ::tryFrom() #[Props('otherFeatures', type: Type::STR_LIST, validationRegex: V::LIST_VALIDATION)] case OTHER_FEATURES = 'OTHER_FEATURES'; - #[Props('paymentPlans', type: Type::STR_LIST)] - case PAYMENT_PLANS = 'PAYMENT_PLANS'; + #[Props('hasAllergyWarning', type: Type::BOOLEAN)] + case HAS_ALLERGY_WARNING = 'HAS_ALLERGY_WARNING'; + + #[Props('allergyWarningInfo')] + case ALLERGY_WARNING_INFO = 'ALLERGY_WARNING_INFO'; + + #[Props('offersPaymentPlans', type: Type::BOOLEAN)] + case OFFERS_PAYMENT_PLANS = 'OFFERS_PAYMENT_PLANS'; + + #[Props('paymentPlansInfo')] + case PAYMENT_PLANS_INFO = 'PAYMENT_PLANS_INFO'; #[Props('paymentMethods', type: Type::STR_LIST, validationRegex: V::PAY_METHODS)] case PAYMENT_METHODS = 'PAYMENT_METHODS'; diff --git a/symfony/src/Data/Fixer/Fixer.php b/symfony/src/Data/Fixer/Fixer.php index 1b207d2e4..39b99e6d6 100644 --- a/symfony/src/Data/Fixer/Fixer.php +++ b/symfony/src/Data/Fixer/Fixer.php @@ -18,7 +18,6 @@ use App\Data\Fixer\StrList\LanguagesFixer; use App\Data\Fixer\StrList\NoopStrListFixer; use App\Data\Fixer\StrList\PayMethodFixer; -use App\Data\Fixer\StrList\PayPlanFixer; use App\Data\Fixer\StrList\SpeciesListFixer; use App\Data\Fixer\StrList\UrlListStringFixer; use App\Utils\Creator\SmartAccessDecorator as Creator; @@ -38,7 +37,6 @@ public function __construct( private readonly NoopStringFixer $noopFixer, private readonly NoopStrListFixer $noopListFixer, private readonly StateFixerConfigurable $stateFixer, - private readonly PayPlanFixer $payPlanFixer, private readonly CurrencyFixer $currencyFixer, private readonly PayMethodFixer $payMethodFixer, ) { @@ -97,7 +95,6 @@ private function getStrListFixer(F $field): StrListFixerInterface F::URL_COMMISSIONS, F::URL_PRICES, F::URL_PHOTOS => $this->urlListFixer, F::LANGUAGES => $this->languagesFixer, - F::PAYMENT_PLANS => $this->payPlanFixer, F::PAYMENT_METHODS => $this->payMethodFixer, F::CURRENCIES_ACCEPTED => $this->currencyFixer, diff --git a/symfony/src/Data/Fixer/StrList/PayPlanFixer.php b/symfony/src/Data/Fixer/StrList/PayPlanFixer.php deleted file mode 100644 index 54e0f9a51..000000000 --- a/symfony/src/Data/Fixer/StrList/PayPlanFixer.php +++ /dev/null @@ -1,37 +0,0 @@ -fixer = new ConfigurableStringFixer($noPayPlans); - } - - #[Override] - protected function getSeparatorRegexp(): ?string - { - return null; - } - - #[Override] - protected function fixItem(string $subject): string - { - return $this->fixer->fix($this->genericStringFixer->fix($subject)); - } -} diff --git a/symfony/src/Entity/Creator.php b/symfony/src/Entity/Creator.php index becefc85f..27217de7a 100644 --- a/symfony/src/Entity/Creator.php +++ b/symfony/src/Entity/Creator.php @@ -4,6 +4,7 @@ namespace App\Entity; +use App\Data\Definitions\Ages; use App\Data\Definitions\ContactPermit; use App\Repository\CreatorRepository; use App\Utils\Creator\SmartAccessDecorator; @@ -54,6 +55,9 @@ class Creator implements Stringable #[ORM\Column(type: Types::TEXT)] private string $city = ''; + #[ORM\Column(type: Types::TEXT, nullable: true, enumType: Ages::class)] + private ?Ages $ages = null; + #[ORM\Column(type: Types::TEXT)] private string $productionModelsComment = ''; @@ -66,8 +70,17 @@ class Creator implements Stringable #[ORM\Column(type: Types::TEXT)] private string $featuresComment = ''; + #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + private ?bool $hasAllergyWarning = null; + + #[ORM\Column(type: Types::TEXT)] + private string $allergyWarningInfo = ''; + + #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + private ?bool $offersPaymentPlans = null; + #[ORM\Column(type: Types::TEXT)] - private string $paymentPlans = ''; + private string $paymentPlansInfo = ''; #[ORM\Column(type: Types::TEXT)] private string $paymentMethods = ''; @@ -295,6 +308,18 @@ public function setCity(string $city): self return $this; } + public function getAges(): ?Ages + { + return $this->ages; + } + + public function setAges(?Ages $ages): self + { + $this->ages = $ages; + + return $this; + } + public function getProductionModelsComment(): string { return $this->productionModelsComment; @@ -343,14 +368,50 @@ public function setFeaturesComment(string $featuresComment): self return $this; } - public function getPaymentPlans(): string + public function getHasAllergyWarning(): ?bool + { + return $this->hasAllergyWarning; + } + + public function setHasAllergyWarning(?bool $hasAllergyWarning): self + { + $this->hasAllergyWarning = $hasAllergyWarning; + + return $this; + } + + public function getAllergyWarningInfo(): string + { + return $this->allergyWarningInfo; + } + + public function setAllergyWarningInfo(string $allergyWarningInfo): self + { + $this->allergyWarningInfo = $allergyWarningInfo; + + return $this; + } + + public function getOffersPaymentPlans(): ?bool + { + return $this->offersPaymentPlans; + } + + public function setOffersPaymentPlans(?bool $offersPaymentPlans): self + { + $this->offersPaymentPlans = $offersPaymentPlans; + + return $this; + } + + public function getPaymentPlansInfo(): string { - return $this->paymentPlans; + return $this->paymentPlansInfo; } - public function setPaymentPlans(string $paymentPlans): self + public function setPaymentPlansInfo(string $paymentPlansInfo): self { - $this->paymentPlans = $paymentPlans; + $this->paymentPlansInfo = $paymentPlansInfo; return $this; } diff --git a/symfony/src/Filtering/DataRequests/Consts.php b/symfony/src/Filtering/Consts.php similarity index 87% rename from symfony/src/Filtering/DataRequests/Consts.php rename to symfony/src/Filtering/Consts.php index a84398fc2..6098926ec 100644 --- a/symfony/src/Filtering/DataRequests/Consts.php +++ b/symfony/src/Filtering/Consts.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering; use App\Utils\Traits\UtilityClass; @@ -18,9 +18,7 @@ final class Consts public const string FILTER_VALUE_INCLUDE_INACTIVE = '.'; // FIXME: All below around payment plans https://github.com/veelkoov/fuzzrake/issues/305 - public const string DATA_PAYPLANS_NONE = 'None'; // grep-payment-plans-none - - public const string FILTER_LABEL_PAYPLANS_NONE = 'Not supported'; + public const string FILTER_LABEL_PAYPLANS_NONE = 'Not supported'; // FIXME: How about "Not offered"? public const string FILTER_LABEL_PAYPLANS_SUPPORTED = 'Supported'; public const string FILTER_VALUE_PAYPLANS_SUPPORTED = self::FILTER_LABEL_PAYPLANS_SUPPORTED; diff --git a/symfony/src/Filtering/DataRequests/Filters/SpecialItemsExtractor.php b/symfony/src/Filtering/DataRequests/Filters/SpecialItemsExtractor.php deleted file mode 100644 index 69fe877e6..000000000 --- a/symfony/src/Filtering/DataRequests/Filters/SpecialItemsExtractor.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - private array $special = []; - - public readonly StringSet $common; - - public function __construct(StringSet $items, string ...$allowedSpecialItems) - { - foreach ($allowedSpecialItems as $specialItem) { - $this->special[$specialItem] = $items->contains($specialItem); - } - - $this->common = $items->minusAll($allowedSpecialItems)->freeze(); - } - - public function hasSpecial(string $item): bool - { - return $this->special[$item] ?? throw new InvalidArgumentException("Special choice '$item' was not declared"); - } -} diff --git a/symfony/src/Filtering/FiltersData/Builder/SpecialItems.php b/symfony/src/Filtering/FiltersData/Builder/SpecialItems.php index 6c1195beb..ac85be0f5 100644 --- a/symfony/src/Filtering/FiltersData/Builder/SpecialItems.php +++ b/symfony/src/Filtering/FiltersData/Builder/SpecialItems.php @@ -4,7 +4,7 @@ namespace App\Filtering\FiltersData\Builder; -use App\Filtering\DataRequests\Consts; +use App\Filtering\Consts; use App\Utils\Traits\UtilityClass; use InvalidArgumentException; diff --git a/symfony/src/Filtering/FiltersData/FiltersData.php b/symfony/src/Filtering/FiltersData/FiltersData.php index 4fc8ceb47..1b4008505 100644 --- a/symfony/src/Filtering/FiltersData/FiltersData.php +++ b/symfony/src/Filtering/FiltersData/FiltersData.php @@ -17,6 +17,7 @@ public function __construct( public FilterData $countries, public FilterData $states, public FilterData $species, + public FilterData $ages, public FilterData $inactive, ) { } diff --git a/symfony/src/Filtering/FiltersData/FiltersService.php b/symfony/src/Filtering/FiltersData/FiltersService.php index f95afdff0..11a71f320 100644 --- a/symfony/src/Filtering/FiltersData/FiltersService.php +++ b/symfony/src/Filtering/FiltersData/FiltersService.php @@ -4,8 +4,9 @@ namespace App\Filtering\FiltersData; +use App\Data\Definitions\Ages; use App\Data\Definitions\Fields\Field; -use App\Filtering\DataRequests\Consts; +use App\Filtering\Consts; use App\Filtering\FiltersData\Builder\MutableFilterData; use App\Filtering\FiltersData\Builder\SpecialItems; use App\Filtering\FiltersData\Data\ItemList; @@ -49,6 +50,7 @@ public function getCachedFiltersTplData(): FiltersData $this->getCountriesFilterData(), $this->getStatesFilterData(), $this->speciesFilterService->getFilterData(), + $this->getAgesData(), $this->getInactiveFilterData(), ), CacheTags::CREATORS, __METHOD__); } @@ -126,17 +128,22 @@ private function getOpenFor(): FilterData private function getPaymentPlans(): FilterData { - $unknown = SpecialItems::newUnknown(); - $result = new MutableFilterData($unknown); + $stats = $this->creatorRepository->getActiveOffersPaymentPlansStats(); - foreach ($this->creatorRepository->getPaymentPlans() as $paymentPlan) { - if (Consts::DATA_VALUE_UNKNOWN === $paymentPlan) { - $unknown->incCount(); - } elseif (Consts::DATA_PAYPLANS_NONE === $paymentPlan) { - $result->items->addOrIncItem(Consts::FILTER_VALUE_PAYPLANS_NONE); - } else { - $result->items->addOrIncItem(Consts::FILTER_VALUE_PAYPLANS_SUPPORTED); - } + $result = new MutableFilterData(SpecialItems::newUnknown($stats->getOrDefaultOf(null, 0))); + $result->items->addOrIncItem(Consts::FILTER_VALUE_PAYPLANS_NONE, $stats->getOrDefaultOf(false, 0)); + $result->items->addOrIncItem(Consts::FILTER_VALUE_PAYPLANS_SUPPORTED, $stats->getOrDefaultOf(true, 0)); + + return FilterData::from($result); + } + + private function getAgesData(): FilterData + { + $stats = $this->creatorRepository->getActiveAgesStats(); + + $result = new MutableFilterData(SpecialItems::newUnknown($stats->getOrDefaultOf(null, 0))); + foreach (Ages::getFormChoices(false) as $label => $value) { + $result->items->addOrIncItem($label, $stats->getOrDefaultOf(Ages::get($value), 0)); } return FilterData::from($result); diff --git a/symfony/src/Filtering/DataRequests/Choices.php b/symfony/src/Filtering/RequestsHandling/Choices.php similarity index 85% rename from symfony/src/Filtering/DataRequests/Choices.php rename to symfony/src/Filtering/RequestsHandling/Choices.php index b1fc7df0a..6ee9ed32d 100644 --- a/symfony/src/Filtering/DataRequests/Choices.php +++ b/symfony/src/Filtering/RequestsHandling/Choices.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering\RequestsHandling; use App\Utils\Json; use App\Utils\Pagination\Pagination; @@ -24,9 +24,8 @@ public function __construct( public StringSet $productionModels, public StringSet $openFor, public StringSet $species, - public bool $wantsUnknownPaymentPlans, - public bool $wantsAnyPaymentPlans, - public bool $wantsNoPaymentPlans, + public StringSet $paymentPlans, + public StringSet $ages, public bool $isAdult, public bool $wantsSfw, public bool $wantsInactive, @@ -50,9 +49,8 @@ public function changePage(int $newPageNumber): self $this->productionModels, $this->openFor, $this->species, - $this->wantsUnknownPaymentPlans, - $this->wantsAnyPaymentPlans, - $this->wantsNoPaymentPlans, + $this->paymentPlans, + $this->ages, $this->isAdult, $this->wantsSfw, $this->wantsInactive, diff --git a/symfony/src/Filtering/DataRequests/FilteredDataProvider.php b/symfony/src/Filtering/RequestsHandling/FilteredDataProvider.php similarity index 98% rename from symfony/src/Filtering/DataRequests/FilteredDataProvider.php rename to symfony/src/Filtering/RequestsHandling/FilteredDataProvider.php index 0dd4fe28b..3a47d3add 100644 --- a/symfony/src/Filtering/DataRequests/FilteredDataProvider.php +++ b/symfony/src/Filtering/RequestsHandling/FilteredDataProvider.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering\RequestsHandling; use App\Repository\CreatorRepository; use App\Service\Cache; diff --git a/symfony/src/Filtering/DataRequests/FiltersValidChoicesFilter.php b/symfony/src/Filtering/RequestsHandling/FiltersValidChoicesFilter.php similarity index 83% rename from symfony/src/Filtering/DataRequests/FiltersValidChoicesFilter.php rename to symfony/src/Filtering/RequestsHandling/FiltersValidChoicesFilter.php index 5ca77720c..231f9bd6b 100644 --- a/symfony/src/Filtering/DataRequests/FiltersValidChoicesFilter.php +++ b/symfony/src/Filtering/RequestsHandling/FiltersValidChoicesFilter.php @@ -2,12 +2,14 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering\RequestsHandling; +use App\Data\Definitions\Ages; use App\Data\Definitions\Features; use App\Data\Definitions\OrderTypes; use App\Data\Definitions\ProductionModels; use App\Data\Definitions\Styles; +use App\Filtering\Consts; use App\Service\DataService; use App\Species\SpeciesService; use Veelkoov\Debris\Sets\StringSet; @@ -44,6 +46,12 @@ public function getOnlyValidChoices(Choices $choices): Choices $openFor = self::onlyValidValues($choices->openFor, $this->dataService->getOpenFor(), Consts::FILTER_VALUE_NOT_TRACKED, Consts::FILTER_VALUE_TRACKING_ISSUES); + $paymentPlans = self::onlyValidValues($choices->paymentPlans, + StringSet::of(Consts::FILTER_VALUE_PAYPLANS_SUPPORTED, Consts::FILTER_VALUE_PAYPLANS_NONE, Consts::FILTER_VALUE_UNKNOWN)); + + $ages = self::onlyValidValues($choices->ages, StringSet::mapFrom(Ages::cases(), static fn (Ages $ages) => $ages->value) + ->plus(Consts::FILTER_VALUE_UNKNOWN)); + return new Choices( $choices->creatorId, $choices->textSearch, @@ -56,9 +64,8 @@ public function getOnlyValidChoices(Choices $choices): Choices $productionModels, $openFor, $species, - $choices->wantsUnknownPaymentPlans, - $choices->wantsAnyPaymentPlans, - $choices->wantsNoPaymentPlans, + $paymentPlans, + $ages, $choices->isAdult, $choices->wantsSfw, $choices->wantsInactive, diff --git a/symfony/src/Filtering/RequestsHandling/QueryBuilderUtils.php b/symfony/src/Filtering/RequestsHandling/QueryBuilderUtils.php new file mode 100644 index 000000000..f180ee371 --- /dev/null +++ b/symfony/src/Filtering/RequestsHandling/QueryBuilderUtils.php @@ -0,0 +1,38 @@ + $conditions + */ + public static function andWhere(QueryBuilder $builder, array $conditions): void + { + if ([] === $conditions) { + return; + } elseif (1 === count($conditions)) { + $condition = array_first($conditions); + } else { + $condition = $builder->expr()->orX(...$conditions); + } + + $builder->andWhere($condition); + } +} diff --git a/symfony/src/Filtering/DataRequests/QueryChoicesAppender.php b/symfony/src/Filtering/RequestsHandling/QueryChoicesAppender.php similarity index 70% rename from symfony/src/Filtering/DataRequests/QueryChoicesAppender.php rename to symfony/src/Filtering/RequestsHandling/QueryChoicesAppender.php index 39a96aaeb..1751fd89e 100644 --- a/symfony/src/Filtering/DataRequests/QueryChoicesAppender.php +++ b/symfony/src/Filtering/RequestsHandling/QueryChoicesAppender.php @@ -2,20 +2,17 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering\RequestsHandling; use App\Data\Definitions\Fields\Field; use App\Entity\Creator; use App\Entity\CreatorOfferStatus; use App\Entity\CreatorSpecie; use App\Entity\CreatorUrl; -use App\Filtering\DataRequests\Filters\SpecialItemsExtractor; -use App\Utils\Collections\Arrays; +use App\Filtering\Consts; use App\Utils\Pagination\Pagination; use App\Utils\StrUtils; use Doctrine\DBAL\ParameterType; -use Doctrine\ORM\Query\Expr\Comparison; -use Doctrine\ORM\Query\Expr\Func; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use InvalidArgumentException; @@ -23,8 +20,6 @@ class QueryChoicesAppender { - private int $uniqueIdIndex = 1; - public function __construct( private readonly Choices $choices, ) { @@ -50,6 +45,7 @@ private function applyFilters(QueryBuilder $builder): void $this->applyStates($builder); $this->applyOpenFor($builder); $this->applyPaymentPlans($builder); + $this->applyAges($builder); $this->applySpecies($builder); $this->applyWantsSfw($builder); $this->applyWorksWithMinors($builder); @@ -63,11 +59,11 @@ private function applyFilters(QueryBuilder $builder): void private function applyOrder(QueryBuilder $builder): void { - $addedDateTime = $this->getUniqueId(); - $updatedDateTime = $this->getUniqueId(); - $addedDateTimeValue = $this->getUniqueId(); - $updatedDateTimeValue = $this->getUniqueId(); - $beforeDateTimesValue = $this->getUniqueId(); + $addedDateTime = QueryBuilderUtils::getUniqueId(); + $updatedDateTime = QueryBuilderUtils::getUniqueId(); + $addedDateTimeValue = QueryBuilderUtils::getUniqueId(); + $updatedDateTimeValue = QueryBuilderUtils::getUniqueId(); + $beforeDateTimesValue = QueryBuilderUtils::getUniqueId(); $builder // Retrieve datetime added for sorting by the last update time @@ -116,9 +112,9 @@ private function createSubqueryBuilder(QueryBuilder $builder, string $alias): Qu private function applyCreatorId(QueryBuilder $builder): void // TODO: Test https://github.com/veelkoov/fuzzrake/issues/183 { if ('' !== $this->choices->creatorId) { - $creator = $this->getUniqueId(); - $creatorId = $this->getUniqueId(); - $creatorIdValue = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorId = QueryBuilderUtils::getUniqueId(); + $creatorIdValue = QueryBuilderUtils::getUniqueId(); $builder->andWhere($builder->expr()->exists( $this->createSubqueryBuilder($builder, $creator) @@ -144,9 +140,9 @@ private function applyTextSearch(QueryBuilder $builder): void $searchedText = '%'.mb_strtoupper($searchedText).'%'; - $searchedTextValue = $this->getUniqueId(); - $creator = $this->getUniqueId(); - $creatorId = $this->getUniqueId(); + $searchedTextValue = QueryBuilderUtils::getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorId = QueryBuilderUtils::getUniqueId(); $builder->andWhere($builder->expr()->orX( "UPPER(d_c.name) LIKE :$searchedTextValue", @@ -164,35 +160,25 @@ private function applyTextSearch(QueryBuilder $builder): void private function applyCountries(QueryBuilder $builder): void { - if ($this->choices->countries->isNotEmpty()) { - $countries = $this->choices->countries->map(static fn ($value) => Consts::FILTER_VALUE_UNKNOWN === $value ? Consts::DATA_VALUE_UNKNOWN : $value); - - $countriesValue = $this->getUniqueId(); - - $builder->andWhere("d_c.country IN (:$countriesValue)")->setParameter($countriesValue, $countries); - } + new SingleColumnSingleValueFilter('d_c.country', nullable: false) + ->applyChoicesTo($this->choices->countries, $builder); } private function applyStates(QueryBuilder $builder): void { - if ($this->choices->states->isNotEmpty()) { - $states = $this->choices->states->map(static fn ($value) => Consts::FILTER_VALUE_UNKNOWN === $value ? Consts::DATA_VALUE_UNKNOWN : $value); - - $statesValue = $this->getUniqueId(); - - $builder->andWhere("d_c.state IN (:$statesValue)")->setParameter($statesValue, $states); - } + new SingleColumnSingleValueFilter('d_c.state', nullable: false) + ->applyChoicesTo($this->choices->states, $builder); } private function applyWantsSfw(QueryBuilder $builder): void { if (true !== $this->choices->isAdult || false !== $this->choices->wantsSfw) { - $creator = $this->getUniqueId(); - $creatorValue1 = $this->getUniqueId(); - $creatorValue2 = $this->getUniqueId(); - $cvFieldName1 = $this->getUniqueId(); - $cvFieldName2 = $this->getUniqueId(); - $cvValueFalse = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorValue1 = QueryBuilderUtils::getUniqueId(); + $creatorValue2 = QueryBuilderUtils::getUniqueId(); + $cvFieldName1 = QueryBuilderUtils::getUniqueId(); + $cvFieldName2 = QueryBuilderUtils::getUniqueId(); + $cvValueFalse = QueryBuilderUtils::getUniqueId(); $builder->andWhere($builder->expr()->exists( $this->createSubqueryBuilder($builder, $creator) @@ -214,10 +200,10 @@ private function applyWantsSfw(QueryBuilder $builder): void private function applyWorksWithMinors(QueryBuilder $builder): void { if (true !== $this->choices->isAdult) { - $creator = $this->getUniqueId(); - $creatorValue = $this->getUniqueId(); - $cvFieldName = $this->getUniqueId(); - $cvValueTrue = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorValue = QueryBuilderUtils::getUniqueId(); + $cvFieldName = QueryBuilderUtils::getUniqueId(); + $cvValueTrue = QueryBuilderUtils::getUniqueId(); $builder->andWhere($builder->expr()->exists( $this->createSubqueryBuilder($builder, $creator) @@ -232,68 +218,55 @@ private function applyWorksWithMinors(QueryBuilder $builder): void } } - /** - * FIXME: https://github.com/veelkoov/fuzzrake/issues/305. - * - * This is absolute garbage. - */ private function applyPaymentPlans(QueryBuilder $builder): void { - $paymentPlansValue = $this->getUniqueId(); - - if ($this->choices->wantsUnknownPaymentPlans) { - if ($this->choices->wantsAnyPaymentPlans) { - if ($this->choices->wantsNoPaymentPlans) { - // Unknown + ANY + None - return; - } else { - // Unknown + ANY - $andWhere = "d_c.paymentPlans <> :$paymentPlansValue"; - $parameter = Consts::DATA_PAYPLANS_NONE; - } - } else { - if ($this->choices->wantsNoPaymentPlans) { - // Unknown + None - $andWhere = "d_c.paymentPlans IN (:$paymentPlansValue)"; - $parameter = [Consts::DATA_VALUE_UNKNOWN, Consts::DATA_PAYPLANS_NONE]; - } else { - // Unknown - $andWhere = "d_c.paymentPlans = :$paymentPlansValue"; - $parameter = Consts::DATA_VALUE_UNKNOWN; - } - } - } else { - if ($this->choices->wantsAnyPaymentPlans) { - if ($this->choices->wantsNoPaymentPlans) { - // ANY + None - $andWhere = "d_c.paymentPlans <> :$paymentPlansValue"; - $parameter = Consts::DATA_VALUE_UNKNOWN; - } else { - // ANY - $andWhere = "d_c.paymentPlans NOT IN (:$paymentPlansValue)"; - $parameter = [Consts::DATA_PAYPLANS_NONE, Consts::DATA_VALUE_UNKNOWN]; - } - } else { - if ($this->choices->wantsNoPaymentPlans) { - // None - $andWhere = "d_c.paymentPlans = :$paymentPlansValue"; - $parameter = Consts::DATA_PAYPLANS_NONE; - } else { - // Nothing selected - return; - } - } + if ($this->choices->paymentPlans->isNotEmpty()) { + $this->applyOptionalBoolean($builder, 'd_c.offersPaymentPlans', + $this->choices->paymentPlans->contains(Consts::FILTER_VALUE_PAYPLANS_SUPPORTED), + $this->choices->paymentPlans->contains(Consts::FILTER_VALUE_PAYPLANS_NONE), + $this->choices->paymentPlans->contains(Consts::FILTER_VALUE_UNKNOWN), + ); } + } - $builder - ->andWhere($andWhere) - ->setParameter($paymentPlansValue, $parameter); + private function applyAges(QueryBuilder $builder): void + { + new SingleColumnSingleValueFilter('d_c.ages', nullable: true) + ->applyChoicesTo($this->choices->ages, $builder); + } + + private function applyOptionalBoolean(QueryBuilder $builder, string $fieldReference, bool $wantsTrue, + bool $wantsFalse, bool $wantsNull): void + { + if ($wantsFalse && $wantsTrue && $wantsNull) { + return; + } + + $conditions = []; + + if ($wantsNull) { + $conditions[] = $builder->expr()->isNull($fieldReference); + } + + if ($wantsTrue) { + $aTrue = QueryBuilderUtils::getUniqueId(); + $conditions[] = $builder->expr()->eq($fieldReference, ":$aTrue"); + $builder->setParameter($aTrue, true, ParameterType::BOOLEAN); + } + + if ($wantsFalse) { + $aFalse = QueryBuilderUtils::getUniqueId(); + $conditions[] = $builder->expr()->eq($fieldReference, ":$aFalse"); + $builder->setParameter($aFalse, false, ParameterType::BOOLEAN); + } + + QueryBuilderUtils::andWhere($builder, $conditions); } private function applyWantsInactive(QueryBuilder $builder): void { if (!$this->choices->wantsInactive) { - $inactiveReasonValue = $this->getUniqueId(); + $inactiveReasonValue = QueryBuilderUtils::getUniqueId(); $builder ->andWhere("d_c.inactiveReason = :$inactiveReasonValue") @@ -312,22 +285,22 @@ private function applySpecies(QueryBuilder $builder): void $items = new SpecialItemsExtractor($this->choices->species, Consts::FILTER_VALUE_UNKNOWN); if ($items->hasSpecial(Consts::FILTER_VALUE_UNKNOWN)) { - $creatorSpecie = $this->getUniqueId(); + $creatorSpecie = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->not($builder->expr()->exists( $builder->getEntityManager() ->getRepository(CreatorSpecie::class) ->createQueryBuilder($creatorSpecie) ->select('1') - ->join("$creatorSpecie.specie", $this->getUniqueId()) + ->join("$creatorSpecie.specie", QueryBuilderUtils::getUniqueId()) ->where("$creatorSpecie.creator = d_c") )); } if ($items->common->isNotEmpty()) { - $creatorSpecie = $this->getUniqueId(); - $specie = $this->getUniqueId(); - $sNameValues = $this->getUniqueId(); + $creatorSpecie = QueryBuilderUtils::getUniqueId(); + $specie = QueryBuilderUtils::getUniqueId(); + $sNameValues = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->exists( $builder->getEntityManager() @@ -342,7 +315,7 @@ private function applySpecies(QueryBuilder $builder): void $builder->setParameter($sNameValues, $items->common); } - $this->addWheres($builder, $conditions); + QueryBuilderUtils::andWhere($builder, $conditions); } private function applyOpenFor(QueryBuilder $builder): void @@ -353,7 +326,7 @@ private function applyOpenFor(QueryBuilder $builder): void Consts::FILTER_VALUE_TRACKING_ISSUES, Consts::FILTER_VALUE_NOT_TRACKED); if ($items->hasSpecial(Consts::FILTER_VALUE_TRACKING_ISSUES)) { - $cvdCsTrackerIssueValueTrue = $this->getUniqueId(); + $cvdCsTrackerIssueValueTrue = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->eq('d_cvd.csTrackerIssue', ":$cvdCsTrackerIssueValueTrue"); @@ -361,8 +334,8 @@ private function applyOpenFor(QueryBuilder $builder): void } if ($items->hasSpecial(Consts::FILTER_VALUE_NOT_TRACKED)) { - $creatorUrl = $this->getUniqueId(); - $cuTypeValue = $this->getUniqueId(); + $creatorUrl = QueryBuilderUtils::getUniqueId(); + $cuTypeValue = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->not($builder->expr()->exists( $builder->getEntityManager() @@ -377,9 +350,9 @@ private function applyOpenFor(QueryBuilder $builder): void } if ($items->common->isNotEmpty()) { - $creatorOfferStatus = $this->getUniqueId(); - $cosIsOpenValueTrue = $this->getUniqueId(); - $cosOfferValues = $this->getUniqueId(); + $creatorOfferStatus = QueryBuilderUtils::getUniqueId(); + $cosIsOpenValueTrue = QueryBuilderUtils::getUniqueId(); + $cosOfferValues = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->exists( $builder->getEntityManager() @@ -396,23 +369,7 @@ private function applyOpenFor(QueryBuilder $builder): void ->setParameter($cosIsOpenValueTrue, true, ParameterType::BOOLEAN); } - $this->addWheres($builder, $conditions); - } - - /** - * @param list $conditions - */ - private function addWheres(QueryBuilder $builder, array $conditions): void - { - if ([] === $conditions) { - return; - } elseif (1 === count($conditions)) { - $condition = Arrays::single($conditions); - } else { - $condition = $builder->expr()->orX(...$conditions); - } - - $builder->andWhere($condition); + QueryBuilderUtils::andWhere($builder, $conditions); } private function applyCreatorValuesCount(QueryBuilder $builder, StringSet $selectedItems, Field $primaryField, @@ -427,9 +384,9 @@ private function applyCreatorValuesCount(QueryBuilder $builder, StringSet $selec throw new InvalidArgumentException('Other field not selected'); } - $creator = $this->getUniqueId(); - $creatorValue = $this->getUniqueId(); - $cvFieldNameValue = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorValue = QueryBuilderUtils::getUniqueId(); + $cvFieldNameValue = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->exists( $this->createSubqueryBuilder($builder, $creator) @@ -443,9 +400,9 @@ private function applyCreatorValuesCount(QueryBuilder $builder, StringSet $selec } if ($items->hasSpecial(Consts::FILTER_VALUE_UNKNOWN)) { - $creator = $this->getUniqueId(); - $creatorValue = $this->getUniqueId(); - $cvFieldNameValue = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorValue = QueryBuilderUtils::getUniqueId(); + $cvFieldNameValue = QueryBuilderUtils::getUniqueId(); $conditions[] = $builder->expr()->not($builder->expr()->exists( $this->createSubqueryBuilder($builder, $creator) @@ -459,10 +416,10 @@ private function applyCreatorValuesCount(QueryBuilder $builder, StringSet $selec } if ($items->common->isNotEmpty()) { - $creator = $this->getUniqueId(); - $creatorValue = $this->getUniqueId(); - $cvFieldName = $this->getUniqueId(); - $cvValueValues = $this->getUniqueId(); + $creator = QueryBuilderUtils::getUniqueId(); + $creatorValue = QueryBuilderUtils::getUniqueId(); + $cvFieldName = QueryBuilderUtils::getUniqueId(); + $cvValueValues = QueryBuilderUtils::getUniqueId(); $having = $allInsteadOfAny ? $builder->expr()->eq("COUNT($creator)", $items->common->count()) @@ -483,11 +440,6 @@ private function applyCreatorValuesCount(QueryBuilder $builder, StringSet $selec $builder->setParameter($cvValueValues, $items->common); } - $this->addWheres($builder, $conditions); - } - - private function getUniqueId(): string - { - return 'd_uid'.((string) $this->uniqueIdIndex++); + QueryBuilderUtils::andWhere($builder, $conditions); } } diff --git a/symfony/src/Filtering/DataRequests/RequestParser.php b/symfony/src/Filtering/RequestsHandling/RequestParser.php similarity index 90% rename from symfony/src/Filtering/DataRequests/RequestParser.php rename to symfony/src/Filtering/RequestsHandling/RequestParser.php index 1c1a1d4f7..fb11c13b3 100644 --- a/symfony/src/Filtering/DataRequests/RequestParser.php +++ b/symfony/src/Filtering/RequestsHandling/RequestParser.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace App\Filtering\DataRequests; +namespace App\Filtering\RequestsHandling; +use App\Filtering\Consts; use Symfony\Component\HttpFoundation\Request; use Veelkoov\Debris\Lists\StringList; use Veelkoov\Debris\Maps\StringToBool; @@ -24,6 +25,7 @@ class RequestParser 'openFor', 'species', 'paymentPlans', + 'ages', 'inactive', ]; @@ -63,9 +65,8 @@ public function getChoices(Request $request): Choices new StringSet($strArrays->get('productionModels')), new StringSet($strArrays->get('openFor')), new StringSet($strArrays->get('species')), - $strArrays->get('paymentPlans')->contains(Consts::FILTER_VALUE_UNKNOWN), - $strArrays->get('paymentPlans')->contains(Consts::FILTER_VALUE_PAYPLANS_SUPPORTED), - $strArrays->get('paymentPlans')->contains(Consts::FILTER_VALUE_PAYPLANS_NONE), + new StringSet($strArrays->get('paymentPlans')), + new StringSet($strArrays->get('ages')), $booleans->get('isAdult'), $booleans->get('wantsSfw'), $strArrays->get('inactive')->contains(Consts::FILTER_VALUE_INCLUDE_INACTIVE), diff --git a/symfony/src/Filtering/RequestsHandling/SingleColumnSingleValueFilter.php b/symfony/src/Filtering/RequestsHandling/SingleColumnSingleValueFilter.php new file mode 100644 index 000000000..c1824e470 --- /dev/null +++ b/symfony/src/Filtering/RequestsHandling/SingleColumnSingleValueFilter.php @@ -0,0 +1,38 @@ +isEmpty()) { + return; + } + + $values = QueryBuilderUtils::getUniqueId(); + $conditions = ["$this->columnRef IN (:$values)"]; + + if (!$this->nullable) { + $selected = $selected->map(static fn ($value) => Consts::FILTER_VALUE_UNKNOWN === $value ? Consts::DATA_VALUE_UNKNOWN : $value); + } elseif ($selected->contains(Consts::FILTER_VALUE_UNKNOWN)) { + $conditions[] = "$this->columnRef IS NULL"; + $selected = $selected->minus(Consts::FILTER_VALUE_UNKNOWN); + } + + $builder->setParameter($values, $selected); + QueryBuilderUtils::andWhere($builder, $conditions); + } +} diff --git a/symfony/src/Filtering/RequestsHandling/SpecialItemsExtractor.php b/symfony/src/Filtering/RequestsHandling/SpecialItemsExtractor.php new file mode 100644 index 000000000..9a0d9a0c4 --- /dev/null +++ b/symfony/src/Filtering/RequestsHandling/SpecialItemsExtractor.php @@ -0,0 +1,27 @@ +special = StringToBool::fromKeys($allowedSpecialItems, + static fn (string $specialItem): bool => $items->contains($specialItem))->freeze(); + + $this->common = $items->minusAll($allowedSpecialItems)->freeze(); + } + + public function hasSpecial(string $item): bool + { + return $this->special->get($item); + } +} diff --git a/symfony/src/Form/InclusionUpdate/Data.php b/symfony/src/Form/InclusionUpdate/Data.php index 49c808387..29822c9bd 100644 --- a/symfony/src/Form/InclusionUpdate/Data.php +++ b/symfony/src/Form/InclusionUpdate/Data.php @@ -109,7 +109,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('ages', ChoiceType::class, [ 'label' => 'What is your age?', 'required' => true, - 'choices' => Ages::getChoices(false), + 'choices' => Ages::getFormChoices(false), 'expanded' => true, ]) ->add('nsfwWebsite', ChoiceType::class, [ @@ -136,9 +136,25 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choices' => ['Yes' => 'YES', 'No' => 'NO'], 'expanded' => true, ]) - ->add('paymentPlans', TextareaType::class, [ - 'label' => 'What payment plans do you support?', - 'help' => 'Please provide a precise description. If you leave this empty, getfursu.it will treat this information as missing! (see the first example). Examples: None/100% upfront, 40% upfront to reserve a slot, 40% after 2 months, 20% after next 2 months, 50% upfront to reserve a slot, 10% each next month, 50% upfront for slot reservation, 100$ each next month until fully paid.', + ->add('hasAllergyWarning', ChoiceType::class, [ + 'label' => 'Do you own animals? Do you want to add allergy warning?', + 'choices' => ['Not specified' => '', 'Yes (allergy warning)' => 'YES', 'No (safe)' => 'NO'], + 'expanded' => true, + 'required' => true, + ]) + ->add('allergyWarningInfo', TextareaType::class, [ + 'label' => 'Allergy warning - additional information', + 'required' => false, + 'empty_data' => '', + ]) + ->add('offersPaymentPlans', ChoiceType::class, [ + 'label' => 'Do you offer payment plans?', + 'choices' => ['Not specified' => '', 'Yes' => 'YES', 'No' => 'NO'], + 'expanded' => true, + 'required' => false, + ]) + ->add('paymentPlansInfo', TextareaType::class, [ + 'label' => 'Payment plans - additional information', 'help_html' => true, 'required' => false, 'empty_data' => '', @@ -466,7 +482,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add(self::FLD_CONTACT_ALLOWED, ChoiceType::class, [ 'label' => 'When is contact allowed?', 'required' => true, - 'choices' => ContactPermit::getChoices(false), + 'choices' => ContactPermit::getFormChoices(false), 'expanded' => true, ]) ->add('emailAddress', TextType::class, [ @@ -506,8 +522,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void foreach ([ 'commissionsUrls', 'currenciesAccepted', 'formerly', 'languages', 'otherFeatures', 'otherOrderTypes', - 'otherStyles', 'otherUrls', 'paymentMethods', 'paymentPlans', 'photoUrls', 'pricesUrls', 'speciesDoes', - 'speciesDoesnt', + 'otherStyles', 'otherUrls', 'paymentMethods', 'photoUrls', 'pricesUrls', 'speciesDoes', 'speciesDoesnt', ] as $fieldName) { $builder->get($fieldName)->addModelTransformer(new StringListAsTextareaTransformer()); } @@ -516,7 +531,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->get('ages')->addModelTransformer(new AgesTransformer()); $builder->get(self::FLD_CONTACT_ALLOWED)->addModelTransformer(new ContactPermitTransformer()); - foreach (['nsfwWebsite', 'nsfwSocial', 'doesNsfw', 'worksWithMinors'] as $field) { + foreach (['nsfwWebsite', 'nsfwSocial', 'doesNsfw', 'worksWithMinors', 'offersPaymentPlans', 'hasAllergyWarning'] as $field) { $builder->get($field)->addModelTransformer(new BooleanTransformer()); } } diff --git a/symfony/src/Form/Mx/CreatorType.php b/symfony/src/Form/Mx/CreatorType.php index 209768d58..5e3bfdb1d 100644 --- a/symfony/src/Form/Mx/CreatorType.php +++ b/symfony/src/Form/Mx/CreatorType.php @@ -59,7 +59,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('ages', ChoiceType::class, [ 'label' => 'Age', 'required' => true, - 'choices' => Ages::getChoices(true), + 'choices' => Ages::getFormChoices(true), 'expanded' => true, ]) ->add('worksWithMinors', ChoiceType::class, [ @@ -68,7 +68,24 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choices' => ['Yes' => 'YES', 'No' => 'NO', 'Unknown' => null], 'expanded' => true, ]) - ->add('paymentPlans', TextareaType::class, [ + ->add('hasAllergyWarning', ChoiceType::class, [ + 'choices' => ['Yes' => 'YES', 'No' => 'NO', 'Unknown' => null], + 'expanded' => true, + 'label' => 'Allergy warning', + 'required' => true, + ]) + ->add('allergyWarningInfo', TextareaType::class, [ + 'empty_data' => '', + 'label' => 'Allergy warning - additional information', + 'required' => false, + ]) + ->add('offersPaymentPlans', ChoiceType::class, [ + 'choices' => ['Yes' => 'YES', 'No' => 'NO', 'Unknown' => null], + 'expanded' => true, + 'label' => 'Offers payment plans?', + 'required' => true, + ]) + ->add('paymentPlansInfo', TextareaType::class, [ 'required' => false, 'empty_data' => '', ]) @@ -326,7 +343,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ->add('contactAllowed', ChoiceType::class, [ 'label' => 'Contact allowed?', - 'choices' => ContactPermit::getChoices(true), + 'choices' => ContactPermit::getFormChoices(true), ]) ->add('emailAddress', TextType::class, [ 'label' => 'Email address', @@ -349,13 +366,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void foreach ([ 'commissionsUrls', 'currenciesAccepted', 'formerly', 'languages', 'otherFeatures', 'otherOrderTypes', - 'otherStyles', 'otherUrls', 'paymentMethods', 'paymentPlans', 'photoUrls', 'pricesUrls', 'speciesDoes', - 'speciesDoesnt', 'formerCreatorIds', 'miniatureUrls', + 'otherStyles', 'otherUrls', 'paymentMethods', 'photoUrls', 'pricesUrls', 'speciesDoes', 'speciesDoesnt', + 'formerCreatorIds', 'miniatureUrls', ] as $fieldName) { $builder->get($fieldName)->addModelTransformer(new StringListAsTextareaTransformer()); } - $builder->get('worksWithMinors')->addModelTransformer(new BooleanTransformer()); + foreach (['worksWithMinors', 'offersPaymentPlans', 'hasAllergyWarning'] as $fieldName) { + $builder->get($fieldName)->addModelTransformer(new BooleanTransformer()); + } + $builder->get('ages')->addModelTransformer(new AgesTransformer()); $builder->get('contactAllowed')->addModelTransformer(new ContactPermitTransformer()); } diff --git a/symfony/src/IuHandling/SchemaFixer.php b/symfony/src/IuHandling/SchemaFixer.php index 92f2a5414..639d8200b 100644 --- a/symfony/src/IuHandling/SchemaFixer.php +++ b/symfony/src/IuHandling/SchemaFixer.php @@ -14,7 +14,7 @@ final class SchemaFixer use UtilityClass; private const string SCHEMA_VERSION = 'SCHEMA_VERSION'; - private const int CURRENT_SCHEMA_VERSION = 18; + private const int CURRENT_SCHEMA_VERSION = 19; /** * @param psJsonFieldsData $data @@ -54,6 +54,16 @@ public static function fix(array $data): array $data[Field::URL_DONATIONS->value] = ''; $data[Field::URL_TELEGRAM_CHANNEL->value] = ''; $data[Field::URL_TIKTOK->value] = ''; + // no break + + case 18: + $paymentPlans = implode("\n", Enforce::strList($data['PAYMENT_PLANS'])); + // Better handle all cases manually + $data[Field::PAYMENT_PLANS_INFO->value] = '' === $paymentPlans ? '' : "(FIXME: supports payment plans?) $paymentPlans"; + $data[Field::OFFERS_PAYMENT_PLANS->value] = null; // See above + + $data[Field::HAS_ALLERGY_WARNING->value] = null; + $data[Field::ALLERGY_WARNING_INFO->value] = ''; } return $data; diff --git a/symfony/src/Repository/CreatorRepository.php b/symfony/src/Repository/CreatorRepository.php index c6f544a40..240c8dd24 100644 --- a/symfony/src/Repository/CreatorRepository.php +++ b/symfony/src/Repository/CreatorRepository.php @@ -8,7 +8,7 @@ use App\Data\Definitions\NewCreator; use App\Entity\Creator; use App\Entity\CreatorValue; -use App\Filtering\DataRequests\QueryChoicesAppender; +use App\Filtering\RequestsHandling\QueryChoicesAppender; use App\Utils\Creator\CreatorId; use App\Utils\Creator\CreatorList; use App\Utils\Creator\SmartAccessDecorator as CreatorSAD; @@ -25,7 +25,8 @@ use Doctrine\Persistence\ManagerRegistry; use Generator; use Veelkoov\Debris\Lists\IntList; -use Veelkoov\Debris\Lists\StringList; +use Veelkoov\Debris\Maps\AnyToInt; +use Veelkoov\Debris\Maps\NullBoolToInt; use Veelkoov\Debris\Maps\StringToInt; use Veelkoov\Debris\Maps\StringToString; use Veelkoov\Debris\Sets\StringSet; @@ -208,16 +209,30 @@ public function getDistinctStates(): StringSet return new StringSet($result); // @phpstan-ignore argument.type (Lack of skill to fix this) } - public function getPaymentPlans(): StringList + public function getActiveOffersPaymentPlansStats(): NullBoolToInt { $result = $this->createQueryBuilder('d_c') - ->select('d_c.paymentPlans AS paymentPlans') + ->select('d_c.offersPaymentPlans AS offers', 'COUNT(d_c) AS count') ->where('d_c.inactiveReason = :empty') + ->groupBy('d_c.offersPaymentPlans') ->setParameter('empty', '') ->getQuery() - ->getSingleColumnResult(); + ->getArrayResult(); + + return NullBoolToInt::fromRows($result, 'offers', 'count'); + } + + public function getActiveAgesStats(): AnyToInt + { + $result = $this->createQueryBuilder('d_c') + ->select('d_c.ages AS ages', 'COUNT(d_c) AS count') + ->where('d_c.inactiveReason = :empty') + ->groupBy('d_c.ages') + ->setParameter('empty', '') + ->getQuery() + ->getArrayResult(); - return new StringList($result); // @phpstan-ignore argument.type (Lack of skill to fix this) + return AnyToInt::fromRows($result, 'ages', 'count'); } /** diff --git a/symfony/src/Species/SpeciesService.php b/symfony/src/Species/SpeciesService.php index ce9f57cf1..60165b840 100644 --- a/symfony/src/Species/SpeciesService.php +++ b/symfony/src/Species/SpeciesService.php @@ -16,7 +16,7 @@ * @phpstan-type TSubspecies null|array * @phpstan-type TNextLevelSubspecies null|array */ -final class SpeciesService +class SpeciesService { public readonly Species $species; private readonly Replacements $fixerReplacements; diff --git a/symfony/src/Twig/AppExtensions.php b/symfony/src/Twig/AppExtensions.php index 75a0858a4..96fd2eb26 100644 --- a/symfony/src/Twig/AppExtensions.php +++ b/symfony/src/Twig/AppExtensions.php @@ -88,15 +88,10 @@ public function agesDescription(Creator $creator, bool $addText): string $result = ''; if ($addText) { - $result .= match ($creator->getAges()) { - Ages::MINORS => 'Everyone is under 18', - Ages::MIXED => 'There is a mix of people over and under 18', - Ages::ADULTS => 'Everyone is over 18', - default => '', - }; - if (null === $creator->getAges()) { $result .= $this->unknownValue(); + } else { + $result .= $creator->getAges()->getLabel(); } } diff --git a/symfony/src/Utils/Creator/SmartAccessDecorator.php b/symfony/src/Utils/Creator/SmartAccessDecorator.php index 51ee80383..e19867069 100644 --- a/symfony/src/Utils/Creator/SmartAccessDecorator.php +++ b/symfony/src/Utils/Creator/SmartAccessDecorator.php @@ -187,12 +187,14 @@ public function getAllCreatorIds(): array #[NotNull(message: 'You must answer this question.', groups: [Validation::GRP_DATA])] public function getAges(): ?Ages { - return Ages::get($this->getStringValue(Field::AGES)); + return $this->entity->getAges(); } public function setAges(?Ages $ages): self { - return $this->setStringValue(Field::AGES, $ages?->value); + $this->entity->setAges($ages); + + return $this; } #[NotNull(message: 'You must answer this question.', groups: [Validation::GRP_DATA])] @@ -1247,21 +1249,52 @@ public function setOtherFeatures(array $otherFeatures): self return $this; } - /** - * @return list - */ - #[StrListLength(max: 1024)] - public function getPaymentPlans(): array + public function getHasAllergyWarning(): ?bool { - return PackedStringList::unpack($this->entity->getPaymentPlans()); + return $this->entity->getHasAllergyWarning(); } - /** - * @param list $paymentPlans - */ - public function setPaymentPlans(array $paymentPlans): self + public function setHasAllergyWarning(?bool $hasAllergyWarning): self + { + $this->entity->setHasAllergyWarning($hasAllergyWarning); + + return $this; + } + + #[Length(max: 4096)] + public function getAllergyWarningInfo(): string + { + return $this->entity->getAllergyWarningInfo(); + } + + public function setAllergyWarningInfo(string $allergyWarningInfo): self + { + $this->entity->setAllergyWarningInfo($allergyWarningInfo); + + return $this; + } + + public function getOffersPaymentPlans(): ?bool + { + return $this->entity->getOffersPaymentPlans(); + } + + public function setOffersPaymentPlans(?bool $offersPaymentPlans): self + { + $this->entity->setOffersPaymentPlans($offersPaymentPlans); + + return $this; + } + + #[Length(max: 4096)] + public function getPaymentPlansInfo(): string + { + return $this->entity->getPaymentPlansInfo(); + } + + public function setPaymentPlansInfo(string $paymentPlansInfo): self { - $this->entity->setPaymentPlans(PackedStringList::pack($paymentPlans)); + $this->entity->setPaymentPlansInfo($paymentPlansInfo); return $this; } diff --git a/symfony/src/Utils/Traits/EnumUtils.php b/symfony/src/Utils/Traits/EnumUtils.php index c696c7e7a..467bec595 100644 --- a/symfony/src/Utils/Traits/EnumUtils.php +++ b/symfony/src/Utils/Traits/EnumUtils.php @@ -4,21 +4,20 @@ namespace App\Utils\Traits; +use Veelkoov\Debris\Maps\StringToNullString; + trait EnumUtils { - /** - * @return array - */ - public static function getChoices(bool $includeUnknown): array + public static function getFormChoices(bool $includeUnknown): StringToNullString { - $result = []; + $result = new StringToNullString(); if ($includeUnknown) { - $result['Unknown'] = null; + $result->set('Unknown', null); } foreach (static::cases() as $case) { - $result[$case->getLabel()] = $case->value; + $result->set($case->getLabel(), $case->value); } return $result; diff --git a/symfony/templates/iu_form/data.html.twig b/symfony/templates/iu_form/data.html.twig index a21a4746f..d6910030f 100644 --- a/symfony/templates/iu_form/data.html.twig +++ b/symfony/templates/iu_form/data.html.twig @@ -304,14 +304,27 @@ +
+

Buyer safety

+ +
+
+ {{ form_row(form.hasAllergyWarning) }} +
+
+ {{ form_row(form.allergyWarningInfo) }} +
+
+

Payments

+ {{ form_row(form.paymentPlansInfo) }} {{ form_row(form.paymentMethods) }}
diff --git a/symfony/templates/main/htmx/creator_card.html.twig b/symfony/templates/main/htmx/creator_card.html.twig index 08d89b514..bce3cca76 100644 --- a/symfony/templates/main/htmx/creator_card.html.twig +++ b/symfony/templates/main/htmx/creator_card.html.twig @@ -136,6 +136,20 @@
{% endif %} + {% if creator.hasAllergyWarning == true %} + + {% elseif creator.hasAllergyWarning is null %} + {# Do not show "unknown sign" at this stage. Not enough info. #} + {% endif %} +

Ages of studio members: {{ ages_description(creator, true) }} @@ -205,7 +219,13 @@

Payment plans
- {{ _self.list_with_others_comment_opt(creator.paymentPlans) }} + {% if creator.offersPaymentPlans is null %} + {{ unknown_value() }} + {% elseif creator.offersPaymentPlans %} + {{ creator.paymentPlansInfo }}
+ {% else %} + Not available + {% endif %}
Currencies
diff --git a/symfony/templates/main/parts/filters.html.twig b/symfony/templates/main/parts/filters.html.twig index fbc6b0bae..36c8357de 100644 --- a/symfony/templates/main/parts/filters.html.twig +++ b/symfony/templates/main/parts/filters.html.twig @@ -23,6 +23,7 @@ species: {label: 'Species', data: filters.species}, paymentPlans: {label: 'Payment plans', data: filters.paymentPlans}, inactive: {label: 'Hidden', data: filters.inactive}, + ages: {label: 'Ages', data: filters.ages}, } %}
diff --git a/symfony/tests/Data/Fixer/FixerTest.php b/symfony/tests/Data/Fixer/FixerTest.php index d421cc88d..ba5699abb 100644 --- a/symfony/tests/Data/Fixer/FixerTest.php +++ b/symfony/tests/Data/Fixer/FixerTest.php @@ -85,9 +85,6 @@ public static function getFixedDataProvider(): array ], [Field::FEATURES, ['Follow-me eyes', 'Attached tail'], ['Attached tail', 'Follow-me eyes']], [Field::ORDER_TYPES, ['Aaaaa'], ['Aaaaa']], - // FIXME: https://github.com/veelkoov/fuzzrake/issues/305 - // [Field::PAYMENT_PLANS, ['100% upfront'], ['None']], - [Field::PAYMENT_PLANS, ['30% upfront, rest in 100 Eur/mth until fully paid'], ['30% upfront, rest in 100 Eur/mth until fully paid']], [Field::URL_MINIATURES, ['https://example.com/'], ['https://example.com/']], [Field::URL_COMMISSIONS, ['https://example.com/'], ['https://example.com/']], [Field::SPECIES_DOES, ['Dogs and cats'], ['Dogs and cats']], diff --git a/symfony/tests/E2E/IuSubmissions/ExtendedTest.php b/symfony/tests/E2E/IuSubmissions/ExtendedTest.php index eeff18e65..b9566dbe3 100644 --- a/symfony/tests/E2E/IuSubmissions/ExtendedTest.php +++ b/symfony/tests/E2E/IuSubmissions/ExtendedTest.php @@ -46,13 +46,18 @@ class ExtendedTest extends IuSubmissionsTestCase Field::AGES, ]; - private const array BOOLEAN = [ // These fields are in the form of radios with "YES" or "NO" values + private const array BOOLEAN_REQUIRED = [ // These fields are in the form of radios with "YES" or "NO" values Field::DOES_NSFW, Field::NSFW_SOCIAL, Field::NSFW_WEBSITE, Field::WORKS_WITH_MINORS, ]; + private const array BOOLEAN_OPTIONAL = [ // These fields are in the form of radios with "YES" or "NO" or "Not specified" values + Field::HAS_ALLERGY_WARNING, + Field::OFFERS_PAYMENT_PLANS, + ]; + /** * Purpose of this test is to make sure: * - all fields, which should be updatable by I/U form, are available and get updated after, @@ -176,8 +181,10 @@ private static function assertFieldIsPresentWithValue(mixed $value, Field $field self::assertExpandedFieldIsPresentWithValue($value, $field, $htmlBody); } elseif (Field::SINCE === $field) { self::assertSinceFieldIsPresentWithValue(Enforce::string($value), $htmlBody); - } elseif (arr_contains(self::BOOLEAN, $field)) { - self::assertYesNoFieldIsPresentWithValue(Enforce::nBool($value), $field, $htmlBody); + } elseif (arr_contains(self::BOOLEAN_REQUIRED, $field)) { + self::assertYesNoFieldIsPresentWithValue(Enforce::nBool($value), $field, false, $htmlBody); + } elseif (arr_contains(self::BOOLEAN_OPTIONAL, $field)) { + self::assertYesNoFieldIsPresentWithValue(Enforce::nBool($value), $field, true, $htmlBody); } elseif (Field::CONTACT_ALLOWED === $field) { self::assertContactValueFieldIsPresentWithValue(Enforce::nString($value), $field, $htmlBody); } else { @@ -242,13 +249,16 @@ private static function assertSinceFieldIsPresentWithValue(string $value, string } } - private static function assertYesNoFieldIsPresentWithValue(?bool $value, Field $field, string $htmlBody): void + private static function assertYesNoFieldIsPresentWithValue(?bool $value, Field $field, bool $optional, string $htmlBody): void { - if (null !== $value) { - $value = $value ? 'YES' : 'NO'; - } + $value = match ($value) { + true => 'YES', + false => 'NO', + null => $optional ? '' : $value, + }; - self::assertRadioFieldIsPresentWithValue($value, ['YES', 'NO'], $field, $htmlBody); + $choices = $optional ? ['YES', 'NO', ''] : ['YES', 'NO']; + self::assertRadioFieldIsPresentWithValue($value, $choices, $field, $htmlBody); } private static function assertContactValueFieldIsPresentWithValue(?string $value, Field $field, string $htmlBody): void @@ -305,6 +315,8 @@ private function setValuesInForm(Form $form, Creator $data, bool $solveCaptcha = $value = $data->get($field); if (is_bool($value)) { $value = $value ? 'YES' : 'NO'; + } elseif (arr_contains(self::BOOLEAN_OPTIONAL, $field) && null === $value) { + $value = ''; } elseif ($value instanceof BackedEnum) { $value = $value->value; } diff --git a/symfony/tests/Entity/CreatorSmallTest.php b/symfony/tests/Entity/CreatorSmallTest.php index ffe901ac0..1f166d493 100644 --- a/symfony/tests/Entity/CreatorSmallTest.php +++ b/symfony/tests/Entity/CreatorSmallTest.php @@ -4,6 +4,7 @@ namespace App\Tests\Entity; +use App\Data\Definitions\Ages; use App\Data\Definitions\ContactPermit; use App\Entity\Creator; use App\Entity\CreatorId; @@ -38,7 +39,12 @@ class CreatorSmallTest extends TestCase */ public function testDeepCloningIsComplete(): void { - $subject = new Creator()->setContactAllowed(ContactPermit::CORRECTIONS); + $subject = new Creator() + ->setContactAllowed(ContactPermit::CORRECTIONS) + ->setAges(Ages::ADULTS) + ->setHasAllergyWarning(true) + ->setOffersPaymentPlans(false) + ; $subject->addCreatorId(new CreatorId()); $subject->setPrivateData(new CreatorPrivateData()); $subject->setVolatileData(new CreatorVolatileData()); diff --git a/symfony/tests/Filtering/DataRequests/FiltersValidChoicesFilterTest.php b/symfony/tests/Filtering/DataRequests/FiltersValidChoicesFilterTest.php deleted file mode 100644 index 591ea6563..000000000 --- a/symfony/tests/Filtering/DataRequests/FiltersValidChoicesFilterTest.php +++ /dev/null @@ -1,59 +0,0 @@ -setLanguages(['Czech', 'Finnish']) - ->setOpenFor(['Pancakes', 'Waffles']) - ->setCountry('FI') - ->setState('Liquid'); - - self::persistAndFlush($creator); - - $subject = self::getContainerService(FiltersValidChoicesFilter::class); - - $choices = new Choices( - '', - '', - StringSet::of('FI', '?', 'UK', '*'), - StringSet::of('Liquid', '?', 'Solid', '*'), - StringSet::of('Finnish', 'Czech', '?', 'English', '*'), - StringSet::of('Toony', '?', '*', 'Yellow', '!'), - StringSet::of('LED eyes', '?', '*', 'Oven', '!'), - StringSet::of('Full plantigrade', '?', '*', 'Pancakes', '!'), - StringSet::of('Standard commissions', '?', 'Waffles', '*'), - StringSet::of('Pancakes', '!', '-', 'Kettles', '*'), - StringSet::of('Birds', '?', 'Furniture', '*'), - false, false, false, false, false, false, false, 1); - - $result = $subject->getOnlyValidChoices($choices); - - self::assertEquals(['FI', '?'], $result->countries->getValuesArray()); - self::assertEquals(['Liquid', '?'], $result->states->getValuesArray()); - self::assertEquals(['Finnish', 'Czech', '?'], $result->languages->getValuesArray()); - self::assertEquals(['Toony', '?', '*'], $result->styles->getValuesArray()); - self::assertEquals(['LED eyes', '?', '*'], $result->features->getValuesArray()); - self::assertEquals(['Full plantigrade', '?', '*'], $result->orderTypes->getValuesArray()); - self::assertEquals(['Standard commissions', '?'], $result->productionModels->getValuesArray()); - self::assertEquals(['Pancakes', '!', '-'], $result->openFor->getValuesArray()); - self::assertEquals(['Birds', '?'], $result->species->getValuesArray()); - } -} diff --git a/symfony/tests/Filtering/DataRequests/FilteredDataProviderTest.php b/symfony/tests/Filtering/RequestsHandling/FilteredDataProviderTest.php similarity index 71% rename from symfony/tests/Filtering/DataRequests/FilteredDataProviderTest.php rename to symfony/tests/Filtering/RequestsHandling/FilteredDataProviderTest.php index 1f3fe1281..fa4ea6cae 100644 --- a/symfony/tests/Filtering/DataRequests/FilteredDataProviderTest.php +++ b/symfony/tests/Filtering/RequestsHandling/FilteredDataProviderTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace App\Tests\Filtering\DataRequests; +namespace App\Tests\Filtering\RequestsHandling; -use App\Filtering\DataRequests\Choices; -use App\Filtering\DataRequests\FilteredDataProvider; +use App\Filtering\RequestsHandling\Choices; +use App\Filtering\RequestsHandling\FilteredDataProvider; use App\Tests\TestUtils\CacheUtils; use App\Tests\TestUtils\Cases\FuzzrakeKernelTestCase; use App\Utils\Creator\SmartAccessDecorator as Creator; @@ -13,15 +13,11 @@ use App\Utils\Parse; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Medium; -use Psr\Cache\InvalidArgumentException; use Veelkoov\Debris\Sets\StringSet; #[Medium] class FilteredDataProviderTest extends FuzzrakeKernelTestCase { - /** - * @throws InvalidArgumentException - */ public function testWorkingWithMinors(): void { $a1 = new Creator()->setCreatorId('M000001')->setWorksWithMinors(false); @@ -36,10 +32,10 @@ public function testWorkingWithMinors(): void $subject = new FilteredDataProvider(self::getCreatorRepository(), CacheUtils::getArrayBased()); - $result = $subject->getCreatorsPage(new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), false, false, false, false, false, false, false, 1)); + $result = $subject->getCreatorsPage($this->getChoices(isAdult: false, wantsSfw: false, wantsInactive: false, pageNumber: 1)); self::assertSame('M000002', self::creatorsListToCreatorIdList($result)); - $result = $subject->getCreatorsPage(new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), false, false, false, false, true, false, false, 1)); + $result = $subject->getCreatorsPage($this->getChoices(isAdult: false, wantsSfw: true, wantsInactive: false, pageNumber: 1)); self::assertSame('M000002', self::creatorsListToCreatorIdList($result)); } @@ -57,10 +53,10 @@ public function testWantsSfw(): void $subject = new FilteredDataProvider(self::getCreatorRepository(), CacheUtils::getArrayBased()); - $result = $subject->getCreatorsPage(new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), false, false, false, true, true, false, false, 1)); + $result = $subject->getCreatorsPage($this->getChoices(isAdult: true, wantsSfw: true, wantsInactive: false, pageNumber: 1)); self::assertSame('M000001', self::creatorsListToCreatorIdList($result)); - $result = $subject->getCreatorsPage(new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), false, false, false, true, false, false, false, 1)); + $result = $subject->getCreatorsPage($this->getChoices(isAdult: true, wantsSfw: false, wantsInactive: false, pageNumber: 1)); self::assertSame('M000001, M000002, M000003, M000004, M000005, M000006, M000007', self::creatorsListToCreatorIdList($result)); } @@ -88,8 +84,7 @@ public static function paginatedResultsDataProvider(): array } #[DataProvider('paginatedResultsDataProvider')] - public function testPaginatedResults(int $numberOfCreators, int $pageRequested, int $pageReturned, int $pagesCount, - int $expectedFirst, int $expectedLast): void + public function testPaginatedResults(int $numberOfCreators, int $pageRequested, int $pageReturned, int $pagesCount, int $expectedFirst, int $expectedLast): void { for ($i = 1; $i <= $numberOfCreators; ++$i) { self::persist(new Creator() @@ -102,7 +97,7 @@ public function testPaginatedResults(int $numberOfCreators, int $pageRequested, $subject = new FilteredDataProvider(self::getCreatorRepository(), CacheUtils::getArrayBased()); - $input = new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), true, true, true, true, false, true, false, $pageRequested); + $input = $this->getChoices(isAdult: true, wantsSfw: false, wantsInactive: true, pageNumber: $pageRequested); $result = $subject->getCreatorsPage($input); @@ -126,4 +121,9 @@ private static function creatorsListToCreatorIdList(ItemsPage $pageData): string return implode(', ', arr_sortl($creatorIds)); } + + private function getChoices(bool $isAdult, bool $wantsSfw, bool $wantsInactive, int $pageNumber): Choices + { + return new Choices('', '', new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), new StringSet(), $isAdult, $wantsSfw, $wantsInactive, creatorMode: false, pageNumber: $pageNumber); + } } diff --git a/symfony/tests/Filtering/RequestsHandling/FiltersValidChoicesFilterTest.php b/symfony/tests/Filtering/RequestsHandling/FiltersValidChoicesFilterTest.php new file mode 100644 index 000000000..97980f3f6 --- /dev/null +++ b/symfony/tests/Filtering/RequestsHandling/FiltersValidChoicesFilterTest.php @@ -0,0 +1,64 @@ +createMock(DataService::class); + $dataServiceMock->method('getCountries')->willReturn(StringSet::of('FI')); + $dataServiceMock->method('getStates')->willReturn(StringSet::of('Liquid')); + $dataServiceMock->method('getOpenFor')->willReturn(StringSet::of('Pancakes', 'Waffles')); + $dataServiceMock->method('getLanguages')->willReturn(StringSet::of('Czech', 'Finnish')); + $speciesServiceMock = $this->createMock(SpeciesService::class); + $speciesServiceMock->method('getValidNames')->willReturn(StringSet::of('Birds')); + + $subject = new FiltersValidChoicesFilter($dataServiceMock, $speciesServiceMock); + + $choices = new Choices( + '', + '', + StringSet::of('FI', '?', 'UK', '*'), + StringSet::of('Liquid', '?', 'Solid', '*'), + StringSet::of('Finnish', 'Czech', '?', 'English', '*'), + StringSet::of('Toony', '?', '*', 'Yellow', '!'), + StringSet::of('LED eyes', '?', '*', 'Oven', '!'), + StringSet::of('Full plantigrade', '?', '*', 'Pancakes', '!'), + StringSet::of('Standard commissions', '?', 'Waffles', '*'), + StringSet::of('Pancakes', '!', '-', 'Kettles', '*'), + StringSet::of('Birds', '?', 'Furniture', '*'), + StringSet::of('None', 'Not supported', 'Supported', '?', '*', 'Waffles', ''), + StringSet::of('ADULTS', 'MIXED', 'MINORS', '?', '*', 'Zombie', ''), + false, false, false, false, 1); + + $result = $subject->getOnlyValidChoices($choices); + + self::assertSameItems(['FI', '?'], $result->countries); + self::assertSameItems(['Liquid', '?'], $result->states); + self::assertSameItems(['Finnish', 'Czech', '?'], $result->languages); + self::assertSameItems(['Toony', '?', '*'], $result->styles); + self::assertSameItems(['LED eyes', '?', '*'], $result->features); + self::assertSameItems(['Full plantigrade', '?', '*'], $result->orderTypes); + self::assertSameItems(['Standard commissions', '?'], $result->productionModels); + self::assertSameItems(['Pancakes', '!', '-'], $result->openFor); + self::assertSameItems(['Birds', '?'], $result->species); + self::assertSameItems(['Not supported', 'Supported', '?'], $result->paymentPlans); + self::assertSameItems(['ADULTS', 'MIXED', 'MINORS', '?'], $result->ages); + } +} diff --git a/symfony/tests/Filtering/DataRequests/PaginationTest.php b/symfony/tests/Filtering/RequestsHandling/PaginationTest.php similarity index 98% rename from symfony/tests/Filtering/DataRequests/PaginationTest.php rename to symfony/tests/Filtering/RequestsHandling/PaginationTest.php index 17797e00e..e4feebf76 100644 --- a/symfony/tests/Filtering/DataRequests/PaginationTest.php +++ b/symfony/tests/Filtering/RequestsHandling/PaginationTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Tests\Filtering\DataRequests; +namespace App\Tests\Filtering\RequestsHandling; use App\Tests\TestUtils\Cases\FuzzrakeTestCase; use App\Utils\Pagination\Pagination; diff --git a/symfony/tests/Filtering/DataRequests/SpecialItemsExtractorTest.php b/symfony/tests/Filtering/RequestsHandling/SpecialItemsExtractorTest.php similarity index 76% rename from symfony/tests/Filtering/DataRequests/SpecialItemsExtractorTest.php rename to symfony/tests/Filtering/RequestsHandling/SpecialItemsExtractorTest.php index 94a09abd1..61bcedf4d 100644 --- a/symfony/tests/Filtering/DataRequests/SpecialItemsExtractorTest.php +++ b/symfony/tests/Filtering/RequestsHandling/SpecialItemsExtractorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace App\Tests\Filtering\DataRequests; +namespace App\Tests\Filtering\RequestsHandling; -use App\Filtering\DataRequests\Filters\SpecialItemsExtractor; -use InvalidArgumentException; +use App\Filtering\RequestsHandling\SpecialItemsExtractor; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; +use Veelkoov\Debris\Exception\MissingKeyException; use Veelkoov\Debris\Sets\StringSet; #[Small] @@ -23,7 +23,7 @@ public function testExtracting(): void try { $subject->hasSpecial('333'); - } catch (InvalidArgumentException) { + } catch (MissingKeyException) { // Expected } } diff --git a/symfony/tests/TestUtils/Cases/Traits/FiltersTestTrait.php b/symfony/tests/TestUtils/Cases/Traits/FiltersTestTrait.php index a9b74ee6b..3acaccf2d 100644 --- a/symfony/tests/TestUtils/Cases/Traits/FiltersTestTrait.php +++ b/symfony/tests/TestUtils/Cases/Traits/FiltersTestTrait.php @@ -15,18 +15,18 @@ trait FiltersTestTrait private static function getCombinedFiltersTestSet(): array { return [ - self::creator('M000001', 'CZ', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000002', 'FI', 'State2', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000003', 'FI', 'State1', ['Lang2'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000004', 'FI', 'State1', ['Lang1'], ['Realistic'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000005', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes', 'Indoor feet'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000006', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Tails (as parts/separate)'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000007', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Premades'], ['Open1'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000008', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open2'], ['Real life animals'], ['Supported'], false, false), - self::creator('M000009', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Fantasy creatures'], ['Supported'], false, false), - self::creator('M000010', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], [], ['None'], false, false), - self::creator('M000011', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], true, false), - self::creator('M000012', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], ['Supported'], false, true), + self::creator('M000001', 'CZ', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000002', 'FI', 'State2', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000003', 'FI', 'State1', ['Lang2'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000004', 'FI', 'State1', ['Lang1'], ['Realistic'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000005', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes', 'Indoor feet'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000006', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Tails (as parts/separate)'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000007', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Premades'], ['Open1'], ['Real life animals'], true, false, false), + self::creator('M000008', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open2'], ['Real life animals'], true, false, false), + self::creator('M000009', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Fantasy creatures'], true, false, false), + self::creator('M000010', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], [], false, false, false), + self::creator('M000011', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, true, false), + self::creator('M000012', 'FI', 'State1', ['Lang1'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], ['Open1'], ['Real life animals'], true, false, true), ]; } @@ -38,61 +38,61 @@ private static function getSpecialFiltersTestSet(): array return [ self::creator('NOCNTRY', '', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('NOSTATE', 'FI', '', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('NOLANGG', 'FI', 'State', [], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('NOSTLES', 'FI', 'State', ['Language'], [], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('BOTHSTL', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherStyles: ['Other styles']), self::creator('OTHRSTL', 'FI', 'State', ['Language'], [], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherStyles: ['Other styles']), self::creator('NOFTRES', 'FI', 'State', ['Language'], ['Toony'], [], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('BOTHFTR', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherFeatures: ['Other features']), self::creator('OTHRFTR', 'FI', 'State', ['Language'], ['Toony'], [], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherFeatures: ['Other features']), self::creator('NOORTPS', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], [], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), self::creator('BOTHORT', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherOrderTypes: ['Other order types']), self::creator('OTHRORT', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], [], ['Standard commissions'], - ['Open for'], ['Most species'], ['Supported'], false, false, + ['Open for'], ['Most species'], true, false, false, otherOrderTypes: ['Other order types']), self::creator('NOPRDMD', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], [], - ['Open for'], ['Most species'], ['Supported'], false, false), + ['Open for'], ['Most species'], true, false, false), ]; } @@ -104,15 +104,15 @@ private static function getPayPlanFiltersTestSet(): array return [ self::creator('UNKPAYP', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], [], false, false), + ['Open for'], ['Most species'], null, false, false), self::creator('NOPAYPL', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['None'], false, false), + ['Open for'], ['Most species'], false, false, false), self::creator('PAYPLNS', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Most species'], ['Some plan'], false, false), + ['Open for'], ['Most species'], true, false, false), ]; } @@ -124,21 +124,21 @@ private static function getTrackingFiltersTestSet(): array return [ self::creator('NTTRCKD', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - [], ['Most species'], ['Supported'], false, false), + [], ['Most species'], true, false, false), self::creator('TRACKIS', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Offer'], ['Most species'], ['Supported'], false, false) + ['Offer'], ['Most species'], true, false, false) ->setCsTrackerIssue(true)->setCommissionsUrls(['url']), self::creator('TRKFAIL', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - [], ['Most species'], ['Supported'], false, false) + [], ['Most species'], true, false, false) ->setCsTrackerIssue(true)->setCommissionsUrls(['url']), self::creator('TRACKOK', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Offer'], ['Most species'], ['Supported'], false, false) + ['Offer'], ['Most species'], true, false, false) ->setCommissionsUrls(['url']), ]; } @@ -151,11 +151,11 @@ private static function getInactiveFiltersTestSet(): array return [ self::creator('ACTIVE1', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Canines'], ['Supported'], false, false), + ['Open for'], ['Canines'], true, false, false), self::creator('INACTIV', 'FI', 'State', ['Language'], ['Toony'], ['LED eyes'], ['Full plantigrade'], ['Standard commissions'], - ['Open for'], ['Canines'], ['Supported'], false, false, + ['Open for'], ['Canines'], true, false, false, inactiveReason: 'Inactive'), ]; } @@ -263,13 +263,12 @@ public static function filterChoicesDataProvider(): array * @param list $productionModels * @param list $openFor * @param list $speciesDoes - * @param list $paymentPlans * @param list $otherStyles * @param list $otherFeatures * @param list $otherOrderTypes * @param list $speciesDoesnt */ - private static function creator(string $creatorIdAndName, string $country, string $state, array $languages, array $styles, array $features, array $orderTypes, array $productionModels, array $openFor, array $speciesDoes, array $paymentPlans, bool $nsfw, bool $worksWithMinors, array $otherStyles = [], array $otherFeatures = [], array $otherOrderTypes = [], array $speciesDoesnt = [], string $inactiveReason = ''): Creator + private static function creator(string $creatorIdAndName, string $country, string $state, array $languages, array $styles, array $features, array $orderTypes, array $productionModels, array $openFor, array $speciesDoes, ?bool $offersPaymentPlans, bool $nsfw, bool $worksWithMinors, array $otherStyles = [], array $otherFeatures = [], array $otherOrderTypes = [], array $speciesDoesnt = [], string $inactiveReason = ''): Creator { return new Creator() ->setCreatorId($creatorIdAndName) @@ -283,7 +282,7 @@ private static function creator(string $creatorIdAndName, string $country, strin ->setProductionModels($productionModels) ->setOpenFor($openFor) ->setSpeciesDoes($speciesDoes) - ->setPaymentPlans($paymentPlans) + ->setOffersPaymentPlans($offersPaymentPlans) ->setAges(Ages::ADULTS) ->setNsfwSocial($nsfw) ->setNsfwWebsite($nsfw) diff --git a/symfony/tests/test_data/extended_test.yaml b/symfony/tests/test_data/extended_test.yaml index 50da35edf..62b354ad4 100644 --- a/symfony/tests/test_data/extended_test.yaml +++ b/symfony/tests/test_data/extended_test.yaml @@ -285,19 +285,61 @@ test_data: DOES_NSFW: false WORKS_WITH_MINORS: false - - PAYMENT_PLANS: + - OFFERS_PAYMENT_PLANS: max_to_max: - before: ["PP C1 A", "PP C1 B before", "PP C1 C"] - update+after: ["PP C1 B after", "PP C1 C", "PP C1 D"] + before: false + update+after: true new_max: - update+after: ["PP C2 A", "PP C2 B"] + update+after: true min_to_max: - before: [] - update+after: ["PP C3 A", "PP C3 B"] + before: null + update+after: true new_min: - update+after: [] + update+after: null just_id: - before+update+after: [] + before+update+after: null + + - PAYMENT_PLANS_INFO: + max_to_max: + before: "Creator 1 PAYMENT_PLANS_INFO before" + update+after: "Creator 1 PAYMENT_PLANS_INFO after" + new_max: + update+after: "Creator 2 PAYMENT_PLANS_INFO" + min_to_max: + before: "" + update+after: "Creator 3 PAYMENT_PLANS_INFO" + new_min: + update+after: "" + just_id: + before+update+after: "" + + - HAS_ALLERGY_WARNING: + max_to_max: + before: false + update+after: true + new_max: + update+after: true + min_to_max: + before: null + update+after: true + new_min: + update+after: null + just_id: + before+update+after: null + + - ALLERGY_WARNING_INFO: + max_to_max: + before: "Creator 1 ALLERGY_WARNING_INFO before" + update+after: "Creator 1 ALLERGY_WARNING_INFO after" + new_max: + update+after: "Creator 2 ALLERGY_WARNING_INFO" + min_to_max: + before: "" + update+after: "Creator 3 ALLERGY_WARNING_INFO" + new_min: + update+after: "" + just_id: + before+update+after: "" - PAYMENT_METHODS: max_to_max: