Merge pull request #7 from goteo/develop
chore: sync main to open-core develop
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20261015093422 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add User, Person and Organization profile fields';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE user_category (user_id INT NOT NULL, category_id INT NOT NULL, INDEX IDX_E6C1FDC1A76ED395 (user_id), INDEX IDX_E6C1FDC112469DE2 (category_id), PRIMARY KEY(user_id, category_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE user_category ADD CONSTRAINT FK_E6C1FDC1A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE user_category ADD CONSTRAINT FK_E6C1FDC112469DE2 FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE CASCADE');
|
||||
$this->addSql('CREATE TABLE user_theme (user_id INT NOT NULL, theme_id INT NOT NULL, INDEX IDX_75B71C50A76ED395 (user_id), INDEX IDX_75B71C5059027487 (theme_id), PRIMARY KEY(user_id, theme_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE user_theme ADD CONSTRAINT FK_75B71C50A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE user_theme ADD CONSTRAINT FK_75B71C5059027487 FOREIGN KEY (theme_id) REFERENCES theme (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE user ADD share_location TINYINT(1) DEFAULT 0 NOT NULL, ADD is_anonymous TINYINT(1) DEFAULT 0 NOT NULL, ADD referral_source VARCHAR(255) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE user_organization ADD tax_id_type VARCHAR(255) DEFAULT NULL, ADD legal_form VARCHAR(255) DEFAULT NULL, ADD public_entity TINYINT(1) DEFAULT 0 NOT NULL');
|
||||
$this->addSql('ALTER TABLE user_person ADD tax_id_type VARCHAR(255) DEFAULT NULL, ADD birth_year INT DEFAULT NULL, ADD gender VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE user_category DROP FOREIGN KEY FK_E6C1FDC1A76ED395');
|
||||
$this->addSql('ALTER TABLE user_category DROP FOREIGN KEY FK_E6C1FDC112469DE2');
|
||||
$this->addSql('DROP TABLE user_category');
|
||||
$this->addSql('ALTER TABLE user_theme DROP FOREIGN KEY FK_75B71C50A76ED395');
|
||||
$this->addSql('ALTER TABLE user_theme DROP FOREIGN KEY FK_75B71C5059027487');
|
||||
$this->addSql('DROP TABLE user_theme');
|
||||
$this->addSql('ALTER TABLE user_person DROP tax_id_type, DROP birth_year, DROP gender');
|
||||
$this->addSql('ALTER TABLE user DROP share_location, DROP is_anonymous, DROP referral_source');
|
||||
$this->addSql('ALTER TABLE user_organization DROP tax_id_type, DROP legal_form, DROP public_entity');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace App\ApiResource\User;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\User\Organization;
|
||||
use App\Entity\User\OrganizationLegalForm;
|
||||
use App\Entity\User\TaxIdType;
|
||||
use App\State\ApiResourceStateProcessor;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -73,4 +75,28 @@ class OrganizationApiResource
|
||||
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
|
||||
)]
|
||||
public string $businessName;
|
||||
|
||||
/**
|
||||
* The kind of document the tax ID belongs to.
|
||||
*/
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
|
||||
)]
|
||||
public ?TaxIdType $taxIdType = null;
|
||||
|
||||
/**
|
||||
* Legal form under which the organization is constituted.
|
||||
*/
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
|
||||
)]
|
||||
public ?OrganizationLegalForm $legalForm = null;
|
||||
|
||||
/**
|
||||
* Is the organization a public entity? e.g: city councils, public universities, etc.
|
||||
*/
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
|
||||
)]
|
||||
public bool $publicEntity = false;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ namespace App\ApiResource\User;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\User\Person;
|
||||
use App\Entity\User\PersonGender;
|
||||
use App\Entity\User\TaxIdType;
|
||||
use App\State\ApiResourceStateProcessor;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* Most times a real person is behind a User account,
|
||||
@@ -69,4 +72,40 @@ class PersonApiResource
|
||||
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
|
||||
)]
|
||||
public string $lastName;
|
||||
|
||||
/**
|
||||
* The kind of document the tax ID belongs to.
|
||||
*/
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
|
||||
)]
|
||||
public ?TaxIdType $taxIdType = null;
|
||||
|
||||
/**
|
||||
* Year of birth of the person. e.g: 1990.
|
||||
*/
|
||||
#[Assert\GreaterThanOrEqual(1900)]
|
||||
#[Assert\Callback([self::class, 'validateBirthYear'])]
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
|
||||
)]
|
||||
public ?int $birthYear = null;
|
||||
|
||||
/**
|
||||
* Gender of the person.
|
||||
*/
|
||||
#[API\ApiProperty(
|
||||
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
|
||||
)]
|
||||
public ?PersonGender $gender = null;
|
||||
|
||||
/**
|
||||
* `Range` only resolves relative time strings (e.g. 'now') against DateTime values, not int years.
|
||||
*/
|
||||
public static function validateBirthYear(?int $birthYear, ExecutionContextInterface $context): void
|
||||
{
|
||||
if ($birthYear !== null && $birthYear > (int) date('Y')) {
|
||||
$context->buildViolation('The birth year cannot be in the future.')->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@ use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\CategoryApiResource;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\ApiResource\TimestampedCreationApiResource;
|
||||
use App\ApiResource\TimestampedUpdationApiResource;
|
||||
use App\Dto\User\UserSignupDto;
|
||||
use App\Dto\User\UserUpdationDto;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\User\User;
|
||||
use App\Entity\User\UserReferralSource;
|
||||
use App\Entity\User\UserType;
|
||||
use App\Filter\EncryptedSearchFilter;
|
||||
use App\Filter\InArrayFilter;
|
||||
@@ -191,4 +194,42 @@ class UserApiResource
|
||||
* Free-form rich text description for the User.
|
||||
*/
|
||||
public string $description;
|
||||
|
||||
/**
|
||||
* Has the User agreed to share their location with the platform?
|
||||
* Used to generate anonymous statistics and impact indicators, never disclosed to third parties.\
|
||||
* Only available to themselves and platform administrators.
|
||||
*/
|
||||
#[API\ApiProperty(security: 'is_granted("USER_EDIT", object)')]
|
||||
public bool $shareLocation = false;
|
||||
|
||||
/**
|
||||
* Does this User wish to have their personal data to remain undisclosed to 3rd parties?\
|
||||
* Only available to themselves and platform administrators.
|
||||
*/
|
||||
#[API\ApiProperty(security: 'is_granted("USER_EDIT", object)')]
|
||||
public bool $isAnonymous = false;
|
||||
|
||||
/**
|
||||
* How the User got to know about the platform.\
|
||||
* Only available to themselves and platform administrators.
|
||||
*/
|
||||
#[API\ApiProperty(security: 'is_granted("USER_EDIT", object)')]
|
||||
public ?UserReferralSource $referralSource = null;
|
||||
|
||||
/**
|
||||
* The Categories the User is interested in.
|
||||
*
|
||||
* @var CategoryApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writableLink: false)]
|
||||
public array $preferredCategories = [];
|
||||
|
||||
/**
|
||||
* The Themes the User is interested in.
|
||||
*
|
||||
* @var ThemeApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writableLink: false)]
|
||||
public array $preferredThemes = [];
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ final class OAuthClientsController extends AbstractController
|
||||
|
||||
private function getBaseUri(string|array $url): string
|
||||
{
|
||||
if (empty($url)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (is_array($url)) {
|
||||
$url = $url[0];
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
namespace App\Dto\User;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\CategoryApiResource;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
use App\Dto\CategorizedInputDtoTrait;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\User\User;
|
||||
use App\Entity\User\UserReferralSource;
|
||||
use App\Entity\User\UserType;
|
||||
use App\Mapping\Transformer\RawLinksMapTransformer;
|
||||
use App\Validator\NotExisting;
|
||||
@@ -15,6 +19,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
final class UserUpdationDto
|
||||
{
|
||||
use CategorizedInputDtoTrait;
|
||||
|
||||
/**
|
||||
* A unique, non white space, byte-safe string identifier for this User.
|
||||
*/
|
||||
@@ -75,4 +81,36 @@ final class UserUpdationDto
|
||||
* Free-form rich text description for the User.
|
||||
*/
|
||||
public string $description;
|
||||
|
||||
/**
|
||||
* Has the User agreed to share their location with the platform?
|
||||
* Used to generate anonymous statistics and impact indicators, never disclosed to third parties.
|
||||
*/
|
||||
public bool $shareLocation;
|
||||
|
||||
/**
|
||||
* Does this User wish to have their personal data to remain undisclosed to 3rd parties?
|
||||
*/
|
||||
public bool $isAnonymous;
|
||||
|
||||
/**
|
||||
* How the User got to know about the platform.
|
||||
*/
|
||||
public ?UserReferralSource $referralSource;
|
||||
|
||||
/**
|
||||
* The Categories the User is interested in.
|
||||
*
|
||||
* @var CategoryApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)]
|
||||
public array $preferredCategories;
|
||||
|
||||
/**
|
||||
* The Themes the User is interested in.
|
||||
*
|
||||
* @var ThemeApiResource[]
|
||||
*/
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::THEMES_OPENAPI_CONTEXT)]
|
||||
public array $preferredThemes;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\ThemeRepository;
|
||||
use AutoMapper\Attribute\MapProvider;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
#[MapProvider(EntityMapProvider::class)]
|
||||
#[ORM\Entity(repositoryClass: ThemeRepository::class)]
|
||||
class Theme
|
||||
{
|
||||
|
||||
@@ -30,6 +30,24 @@ class Organization
|
||||
#[BlindIndex(sourceField: 'taxId', normalizer: BlindIndex::NORMALIZE_UPPERCASE)]
|
||||
private ?string $taxIdLookup = null;
|
||||
|
||||
/**
|
||||
* The kind of document the tax ID belongs to.
|
||||
*/
|
||||
#[ORM\Column(nullable: true, enumType: TaxIdType::class)]
|
||||
private ?TaxIdType $taxIdType = null;
|
||||
|
||||
/**
|
||||
* Legal form under which the organization is constituted.
|
||||
*/
|
||||
#[ORM\Column(nullable: true, enumType: OrganizationLegalForm::class)]
|
||||
private ?OrganizationLegalForm $legalForm = null;
|
||||
|
||||
/**
|
||||
* Is the organization a public entity?
|
||||
*/
|
||||
#[ORM\Column(options: ['default' => false])]
|
||||
private bool $publicEntity = false;
|
||||
|
||||
/**
|
||||
* Organization legal name before government,
|
||||
* as it appears on legal documents issued by or for this organization.
|
||||
@@ -110,4 +128,40 @@ class Organization
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTaxIdType(): ?TaxIdType
|
||||
{
|
||||
return $this->taxIdType;
|
||||
}
|
||||
|
||||
public function setTaxIdType(?TaxIdType $taxIdType): static
|
||||
{
|
||||
$this->taxIdType = $taxIdType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLegalForm(): ?OrganizationLegalForm
|
||||
{
|
||||
return $this->legalForm;
|
||||
}
|
||||
|
||||
public function setLegalForm(?OrganizationLegalForm $legalForm): static
|
||||
{
|
||||
$this->legalForm = $legalForm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPublicEntity(): bool
|
||||
{
|
||||
return $this->publicEntity;
|
||||
}
|
||||
|
||||
public function setPublicEntity(bool $publicEntity): static
|
||||
{
|
||||
$this->publicEntity = $publicEntity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\User;
|
||||
|
||||
enum OrganizationLegalForm: string
|
||||
{
|
||||
case Association = 'association';
|
||||
|
||||
case Foundation = 'foundation';
|
||||
|
||||
case Cooperative = 'cooperative';
|
||||
|
||||
/**
|
||||
* For-profit companies of any kind.
|
||||
*/
|
||||
case Company = 'company';
|
||||
|
||||
/**
|
||||
* Self-employed professionals.
|
||||
*/
|
||||
case SelfEmployed = 'self_employed';
|
||||
|
||||
/**
|
||||
* Government bodies and public administrations.
|
||||
*/
|
||||
case PublicAdministration = 'public_administration';
|
||||
|
||||
case Other = 'other';
|
||||
}
|
||||
@@ -37,6 +37,21 @@ class Person
|
||||
#[BlindIndex(sourceField: 'taxId', normalizer: BlindIndex::NORMALIZE_UPPERCASE)]
|
||||
private ?string $taxIdLookup = null;
|
||||
|
||||
/**
|
||||
* The kind of document the tax ID belongs to.
|
||||
*/
|
||||
#[ORM\Column(nullable: true, enumType: TaxIdType::class)]
|
||||
private ?TaxIdType $taxIdType = null;
|
||||
|
||||
/**
|
||||
* Year of birth of the person.
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $birthYear = null;
|
||||
|
||||
#[ORM\Column(nullable: true, enumType: PersonGender::class)]
|
||||
private ?PersonGender $gender = null;
|
||||
|
||||
/**
|
||||
* First-part of the name of the person, usually the given name(s). e.g: John, Juan.
|
||||
*/
|
||||
@@ -116,4 +131,40 @@ class Person
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTaxIdType(): ?TaxIdType
|
||||
{
|
||||
return $this->taxIdType;
|
||||
}
|
||||
|
||||
public function setTaxIdType(?TaxIdType $taxIdType): static
|
||||
{
|
||||
$this->taxIdType = $taxIdType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBirthYear(): ?int
|
||||
{
|
||||
return $this->birthYear;
|
||||
}
|
||||
|
||||
public function setBirthYear(?int $birthYear): static
|
||||
{
|
||||
$this->birthYear = $birthYear;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGender(): ?PersonGender
|
||||
{
|
||||
return $this->gender;
|
||||
}
|
||||
|
||||
public function setGender(?PersonGender $gender): static
|
||||
{
|
||||
$this->gender = $gender;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\User;
|
||||
|
||||
enum PersonGender: string
|
||||
{
|
||||
case Female = 'female';
|
||||
|
||||
case Male = 'male';
|
||||
|
||||
case NonBinary = 'non_binary';
|
||||
|
||||
/**
|
||||
* Any other gender identity, or the person prefers not to say.
|
||||
*/
|
||||
case Other = 'other';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\User;
|
||||
|
||||
enum TaxIdType: string
|
||||
{
|
||||
/**
|
||||
* National identity or tax identification document, for people and organizations.
|
||||
*/
|
||||
case NationalId = 'national_id';
|
||||
|
||||
case Passport = 'passport';
|
||||
|
||||
/**
|
||||
* Any other national or foreign identification document.
|
||||
*/
|
||||
case Other = 'other';
|
||||
}
|
||||
@@ -5,12 +5,14 @@ namespace App\Entity\User;
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Accounting\AccountingOwnerInterface;
|
||||
use App\Entity\Address;
|
||||
use App\Entity\Category;
|
||||
use App\Entity\DateCreatedTrait;
|
||||
use App\Entity\DateUpdatedTrait;
|
||||
use App\Entity\DedupedTrait;
|
||||
use App\Entity\MigratedTrait;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\Theme;
|
||||
use App\Library\Link;
|
||||
use App\Mapping\Provider\EntityMapProvider;
|
||||
use App\Repository\User\UserRepository;
|
||||
@@ -131,6 +133,40 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
#[ORM\OneToMany(targetEntity: Address::class, mappedBy: 'user', cascade: ['persist'])]
|
||||
private Collection $addresses;
|
||||
|
||||
/**
|
||||
* Has the User agreed to share their location with the platform for anonymous statistics?
|
||||
*/
|
||||
#[ORM\Column(options: ['default' => false])]
|
||||
private bool $shareLocation = false;
|
||||
|
||||
/**
|
||||
* Does this User wish to have their personal data to remain undisclosed to 3rd parties?
|
||||
*/
|
||||
#[ORM\Column(options: ['default' => false])]
|
||||
private bool $isAnonymous = false;
|
||||
|
||||
/**
|
||||
* How the User got to know about the platform.
|
||||
*/
|
||||
#[ORM\Column(nullable: true, enumType: UserReferralSource::class)]
|
||||
private ?UserReferralSource $referralSource = null;
|
||||
|
||||
/**
|
||||
* The Categories the User is interested in.
|
||||
*
|
||||
* @var Collection<int, Category>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Category::class)]
|
||||
private Collection $preferredCategories;
|
||||
|
||||
/**
|
||||
* The Themes the User is interested in.
|
||||
*
|
||||
* @var Collection<int, Theme>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Theme::class)]
|
||||
private Collection $preferredThemes;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->accounting = Accounting::of($this);
|
||||
@@ -141,6 +177,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
$this->emailConfirmed = false;
|
||||
$this->active = false;
|
||||
$this->addresses = new ArrayCollection();
|
||||
$this->preferredCategories = new ArrayCollection();
|
||||
$this->preferredThemes = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -431,4 +469,88 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isShareLocation(): bool
|
||||
{
|
||||
return $this->shareLocation;
|
||||
}
|
||||
|
||||
public function setShareLocation(bool $shareLocation): static
|
||||
{
|
||||
$this->shareLocation = $shareLocation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isAnonymous(): bool
|
||||
{
|
||||
return $this->isAnonymous;
|
||||
}
|
||||
|
||||
public function setIsAnonymous(bool $isAnonymous): static
|
||||
{
|
||||
$this->isAnonymous = $isAnonymous;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReferralSource(): ?UserReferralSource
|
||||
{
|
||||
return $this->referralSource;
|
||||
}
|
||||
|
||||
public function setReferralSource(?UserReferralSource $referralSource): static
|
||||
{
|
||||
$this->referralSource = $referralSource;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Category>
|
||||
*/
|
||||
public function getPreferredCategories(): Collection
|
||||
{
|
||||
return $this->preferredCategories;
|
||||
}
|
||||
|
||||
public function addPreferredCategory(Category $category): static
|
||||
{
|
||||
if (!$this->preferredCategories->contains($category)) {
|
||||
$this->preferredCategories->add($category);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePreferredCategory(Category $category): static
|
||||
{
|
||||
$this->preferredCategories->removeElement($category);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Theme>
|
||||
*/
|
||||
public function getPreferredThemes(): Collection
|
||||
{
|
||||
return $this->preferredThemes;
|
||||
}
|
||||
|
||||
public function addPreferredTheme(Theme $theme): static
|
||||
{
|
||||
if (!$this->preferredThemes->contains($theme)) {
|
||||
$this->preferredThemes->add($theme);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePreferredTheme(Theme $theme): static
|
||||
{
|
||||
$this->preferredThemes->removeElement($theme);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity\User;
|
||||
|
||||
enum UserReferralSource: string
|
||||
{
|
||||
case SearchEngine = 'search_engine';
|
||||
|
||||
case SocialMedia = 'social_media';
|
||||
|
||||
/**
|
||||
* Word of mouth: a friend, relative or acquaintance.
|
||||
*/
|
||||
case Friend = 'friend';
|
||||
|
||||
/**
|
||||
* News, press or media coverage.
|
||||
*/
|
||||
case Press = 'press';
|
||||
|
||||
/**
|
||||
* An event, talk or workshop.
|
||||
*/
|
||||
case Event = 'event';
|
||||
|
||||
case Other = 'other';
|
||||
}
|
||||
@@ -6,20 +6,33 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="mx-auto max-w-360 px-10 mb-20">
|
||||
<ul class="mx-auto max-w-122 flex flex-col gap-4">
|
||||
<div class="mx-auto max-w-180 flex flex-col gap-4">
|
||||
<h1 class="text-black text-2xl font-bold">{{ 'clients.title' | trans }}</h1>
|
||||
<p class="text-content">{{ 'clients.description' | trans | raw }}</p>
|
||||
<div class="flex gap-6">
|
||||
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
{% for client in oauth_clients %}
|
||||
<li>
|
||||
<div class="bg-white border border-grey rounded-2xl p-4 shadow-sm">
|
||||
<a href="{{ client.baseUri }}">
|
||||
<img class="max-h-12 h-full" src="/oauth2/clients/{{ client.identifier }}.png" alt={{ client.name }}>
|
||||
</a>
|
||||
<a href="{{ client.baseUri }}" class="block h-full">
|
||||
<div class="h-full bg-white border border-grey rounded-2xl p-4 shadow-sm flex items-center">
|
||||
<div class="flex flex-col gap-4">
|
||||
<img
|
||||
class="max-h-16 h-auto grayscale"
|
||||
src="/oauth2/clients/{{ client.identifier }}.png"
|
||||
alt="{{ client.name }}"
|
||||
>
|
||||
<div>
|
||||
<span class="text-content text-xs">Name</span>
|
||||
<p>{{ client.name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-content text-xs">Site</span>
|
||||
<p class="text-sm">{{ client.baseUri | replace({'https://' : '', 'https://www.' : '', 'http://' : '', '/' : ''}) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<input type="password" placeholder="{{ 'login.password.placeholder' | trans }}" name="_password" id="password" class="form-control peer border-secondary focus:ring-tertiary w-full rounded-md border bg-white p-4 text-base text-gray-700 placeholder-gray-400 focus:ring-1 focus:outline-none" autocomplete="current-password" required>
|
||||
</div>
|
||||
|
||||
<a class="text-secondary" href="{{ path('app_forgot_password_request') }}">
|
||||
{{ 'login.password.forgotten' | trans }}
|
||||
</a>
|
||||
|
||||
<input type="hidden" name="_csrf_token" data-controller="csrf-protection" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<div class="checkbox mb-3 flex flex-row gap-2">
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity\UserApi;
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
|
||||
use App\Entity\Theme;
|
||||
use App\Entity\User\UserType;
|
||||
use App\Factory\CategoryFactory;
|
||||
use App\Factory\User\UserFactory;
|
||||
use App\Tests\Fixtures\TestUser;
|
||||
use App\Tests\Traits\RequestingTestTrait;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
use function Zenstruck\Foundry\Persistence\persist;
|
||||
|
||||
class UpdateTest extends ApiTestCase
|
||||
{
|
||||
use ResetDatabase;
|
||||
use Factories;
|
||||
use RequestingTestTrait;
|
||||
|
||||
public function testUpdateProfileFields(): void
|
||||
{
|
||||
$user = TestUser::get();
|
||||
$category = CategoryFactory::createOne(['name' => 'test']);
|
||||
$theme = persist(Theme::class, ['name' => 'test']);
|
||||
$uri = \sprintf('/v4/users/%d', $user->getId());
|
||||
|
||||
$this->request('PATCH', $uri, [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => [
|
||||
'shareLocation' => true,
|
||||
'isAnonymous' => true,
|
||||
'referralSource' => 'social_media',
|
||||
'preferredCategories' => [\sprintf('/v4/categories/%s', $category->getId())],
|
||||
'preferredThemes' => [\sprintf('/v4/themes/%s', $theme->getId())],
|
||||
],
|
||||
], Response::HTTP_OK);
|
||||
|
||||
$this->assertJsonContains([
|
||||
'shareLocation' => true,
|
||||
'isAnonymous' => true,
|
||||
'referralSource' => 'social_media',
|
||||
]);
|
||||
|
||||
$data = $this->request('GET', $uri, ['headers' => $this->withAuthHeader($user)])->toArray();
|
||||
$this->assertCount(1, $data['preferredCategories']);
|
||||
$this->assertCount(1, $data['preferredThemes']);
|
||||
}
|
||||
|
||||
public function testPrivateFieldsHiddenFromOthers(): void
|
||||
{
|
||||
$user = TestUser::get();
|
||||
$other = UserFactory::createOne(['handle' => 'other_user', 'email' => 'other@example.com']);
|
||||
|
||||
$data = $this->request('GET', \sprintf('/v4/users/%d', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($other),
|
||||
], Response::HTTP_OK)->toArray();
|
||||
|
||||
$this->assertArrayNotHasKey('shareLocation', $data);
|
||||
$this->assertArrayNotHasKey('isAnonymous', $data);
|
||||
$this->assertArrayNotHasKey('referralSource', $data);
|
||||
}
|
||||
|
||||
public function testUpdatePersonFields(): void
|
||||
{
|
||||
$user = TestUser::get();
|
||||
|
||||
$this->request('PATCH', \sprintf('/v4/users/%d/person', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => [
|
||||
'taxIdType' => 'passport',
|
||||
'birthYear' => 1990,
|
||||
'gender' => 'non_binary',
|
||||
],
|
||||
], Response::HTTP_OK);
|
||||
|
||||
$this->assertJsonContains([
|
||||
'taxIdType' => 'passport',
|
||||
'birthYear' => 1990,
|
||||
'gender' => 'non_binary',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdatePersonInvalidBirthYear(): void
|
||||
{
|
||||
$user = TestUser::get();
|
||||
|
||||
$this->request('PATCH', \sprintf('/v4/users/%d/person', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => ['birthYear' => 1800],
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
|
||||
$this->request('PATCH', \sprintf('/v4/users/%d/person', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => ['birthYear' => (int) date('Y') + 1],
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
public function testUpdateOrganizationFields(): void
|
||||
{
|
||||
$user = TestUser::get();
|
||||
|
||||
$this->request('PATCH', \sprintf('/v4/users/%d', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => ['type' => UserType::Organization->value],
|
||||
], Response::HTTP_OK);
|
||||
|
||||
$this->request('PATCH', \sprintf('/v4/users/%d/organization', $user->getId()), [
|
||||
'headers' => $this->withAuthHeader($user),
|
||||
'json' => [
|
||||
'taxId' => 'G12345678',
|
||||
'legalName' => 'Test Association',
|
||||
'taxIdType' => 'national_id',
|
||||
'legalForm' => 'association',
|
||||
'publicEntity' => true,
|
||||
],
|
||||
], Response::HTTP_OK);
|
||||
|
||||
$this->assertJsonContains([
|
||||
'taxIdType' => 'national_id',
|
||||
'legalForm' => 'association',
|
||||
'publicEntity' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ login:
|
||||
password:
|
||||
label: Password
|
||||
placeholder: TrustNo1
|
||||
forgotten: Did you forget your password?
|
||||
rememberme: Remember me
|
||||
submit: Sign in
|
||||
oauth: 'You can also login via:'
|
||||
@@ -49,7 +50,8 @@ password_reset:
|
||||
|
||||
clients:
|
||||
title: Goteo sites
|
||||
description: This site is an instance of <a class="text-primary" href="/v4">the Goteo v4 API</a>, these clients are connected to it.
|
||||
description: This site is an instance of <a class="text-secondary" href="/v4">the Goteo v4 API</a>, these clients are connected to it.
|
||||
noClients: No active clients registered
|
||||
|
||||
footer:
|
||||
legal:
|
||||
|
||||
Reference in New Issue
Block a user