104 Commits
Author SHA1 Message Date
daniel.subiabre 302464e01e feat: update ProjectCreationDto categories and themes input
PHPUnit / phpunit (push) Canceled after 0s
PHP CS Fixer / php-cs-fixer (pull_request) Canceled after 0s
2026-10-19 15:36:25 +02:00
daniel.subiabre 4d348e1db7 Merge branch 'main' into feat/dtf-api 2026-10-08 16:30:31 +02:00
Daniel Subiabre García b4f1281e9c Merge pull request #3 from goteo/develop
chore: sync main to open-core develop
2026-10-08 16:29:14 +02:00
Daniel Subiabre García dc23606b42 Merge branch 'main' into develop 2026-10-08 16:18:49 +02:00
daniel.subiabre ac47aec378 feat: Themes (#257)
* feat: add trait to unify get by id or slug logic

* feat: add Theme resource

* fix: categories and themes

* feat: update api docs for projects to themes relationship

* feat: update doctrine relationship of projects to themes

* feat: add v3 categories to v4 themes pumping during projects pumping
2026-10-08 13:02:07 +02:00
daniel.subiabre 14ee28be4f [feat] Project reviews (#177)
* feat: start project reviews entity

* feat: add review areas entity

* feat: add review comments entity

* fix: code styles improvements by php-cs-fixer

* feat: expose project review resources in the API

* feat: add timestampt to review resources

* feat: add SQL migration

* fix: update ProjectReview API docs and operations

* feat: refactor ProjectsListener to dedicated project status listener

* fix: make default review area risk high

* feat: add ReviewService for creating default project reviews

* fix: renamed timestampt entity traits

* fix: update doctrine migration

* feat: test for release date to be set greater than today's start

* feat: add test for project campaign review

* fix: update person and organization patch validations

* fix: test if review entity is truly persisted

* fix: bad start statuses in project tests

* fix: ProjectStatusListener not persisting side effect data

* fix: centralize project status change side-effects processing in ProjectService

* fix: small review persistence tweaks

* feat: allow only concerned users to see and edit review areas

* feat: apply validations and security for concerned users to review comments
2026-10-07 13:31:44 +02:00
daniel.subiabre 86ba2c4fe4 fix: HTTPS in OAuth2 redirect_uri (#255)
* feat: remove router schema force via config

* fix: configure trusted proxies via env vars

* fix: typo in var description
2026-10-05 15:43:20 +02:00
daniel.subiabre 2dec0d1db5 fix: HTTP (!s) scheme in callback for OAuth2 providers (#254)
* fix: bad path to oauth2 provider logo

* feat: set https scheme on router on prod context

* feat: configure oauth2 jwts ttl via env var

* fix: bad nesting of access_token_ttl yaml option
2026-10-02 16:03:20 +02:00
daniel.subiabre 4761bc4544 Merge branch 'main' into feat/dtf-api
PHPUnit / phpunit (push) Canceled after 0s
PHP CS Fixer / php-cs-fixer (pull_request) Canceled after 0s
2026-10-02 03:27:00 +02:00
Daniel Subiabre García e1b7fec3ba chore: sync main to open-core develop (#2)
* fix: upgrade goteo/benzina to 0.6

* feat: increase Nominatim cache TTL

* fix: allow long addresses for territory data

* fix: build project addresses from goteo 3 territory fields

* fix: handle empty strings for video sources

* fix: update project drafts deadline to 2026

* feat: split project description into 4 fields

* feat: bump doctrine related dependencies

* feat: bump symfony related dependencies

* feat: make categories work on idOrSlug

* fix: allow filtering on project descBrief property

* fix: update constraints on project update to allow optional updates of properties

* fix: update Projects test suite to new project shape

* fix: order of DROP operations in migration Version20260813171902

* fix: move accounting balance and checkout to supports listeners to onFlush events

* fix: bad order of filter safeguards

* chore: code style by php-cs-fixer

* fix: prevent uninitialized fields on api resource from breaking AccountingVoter

* fix: small bug preventing code improvements on Accounting api resource

* feat: add cover property on ProjectReward resource

* feat: add cover property to Project resources

* feat: use video cover for project cover

* feat: add migration for property cover on project and rewards

* fix: small improvements on UserOwned helpers

* feat: add territory to project creation

* feat: mark creation fields as required

* feat: add CollaborationCandidacy resource

* fix: update old validations access of new types

* feat: add validation for charges targetting projects not in campaign

* feat: validate new collaboration candicacies are to open collaborations

* feat: prevent future object access issues in SameProjectRewardChargeValidator

* fix: return void on succesfull delete operations

* fix: force set of isFinite property

* feat: allow to filter ProjectBudgetItem by type and deadline

* feat: remove example values from .env

* feat: add testing encryption keys

* feat: make project in BudgetItem body required

* feat: add testing encryption keys to actions

* feat: add collaborations property to ProjectApiResource

* feat: Update Users PATCH body (#246)

* feat: move UserSignupDto to Dto\User namespace

* fix: bad parsing of INSTR by ApiPlatform pagination layer

* fix: make User resources be processed by dedicated update processor

* fix: specify authentication usage of UserSignupDto::password property

* feat: improve non-crawlable URI detection for ScoutService

* feat: add "org" to Links with known organizations behind links

* feat: map raw uri strings to full Link objects

* fix: bad loading of existing user data in UserStateProcessor

* feat: add NotExisting validator

* feat: use NotExisting constraint on Users handle, email, Projects titles and Tipjar names

* fix: check for existing node item in URI rel detection

* fix: make app:oauth2-server:consent-client true by default

* feat: bump league/oauth2-server-bundle to 1.2.2

* feat: add UserConverter decorator to have league/oauth2-server JWT's sub have User id instead of handle

* fix: specify OAuth2 and  separate server from client utilities

* fix: make app_user_provider use ID instead of handle

* feat: add explaining comment to doctrine_encryption.key_id

* feat: add status to reward claims

* feat: make default RewardClaim entity status in_pending

* feat: add shorter check for ownership of projects

* feat: allow to modify status of reward claims

* feat: add shipping address to reward claims

* fix: remove requirement of status in claim update op

* fix: update filters and remove obsolete attributes from ProjectRewardClaim resource

* feat: update DB layer for project reward claims

* fix: code style issues by PHP-CS-Fixer

* feat: add DoctrineLoggablePumpTrait to remove Gedmo Loggable event listener

* fix: update legacy ref to FileUriException

* feat: pump reward claim status and shipping address from v3 records

* fix: update pumps batch sizes and injected dependencies

* feat: pump user roles

* feat: add true image cover retrieval and leave video as fallback for ProjectsPump

* feat: pump project collaborations

* feat: allow cascading of collaborations

* fix: set isFulfilled of collaboration

* fix: link collaboration to in-pumping project

* fix: add trailing comma to list of fulfilling statuses

* test: update PHPUnit CI to use bin/phpunit instead of action

* feat: set memory_limit of PHPUnit run on CI to 128M

* fix: add user avatar to user pump processing

* fix: set invests batch size to 1 to avoid support duplicates on insert

* Merge pull request #253 from goteo/fix/pumping-bugs

fix: Pumping bugs
2026-10-02 03:26:21 +02:00
daniel.subiabre 8053e8f800 feat: update pumping to DTFs 2026-10-02 03:25:04 +02:00
daniel.subiabre 317dfc4e29 Merge branch 'main' into feat/dtf-api 2026-10-01 16:26:25 +02:00
Daniel Subiabre García 0007c6b9b1 chore: sync main to core develop (#1)
* fix: upgrade goteo/benzina to 0.6

* feat: increase Nominatim cache TTL

* fix: allow long addresses for territory data

* fix: build project addresses from goteo 3 territory fields

* fix: handle empty strings for video sources

* fix: update project drafts deadline to 2026

* feat: split project description into 4 fields

* feat: bump doctrine related dependencies

* feat: bump symfony related dependencies

* feat: make categories work on idOrSlug

* fix: allow filtering on project descBrief property

* fix: update constraints on project update to allow optional updates of properties

* fix: update Projects test suite to new project shape

* fix: order of DROP operations in migration Version20260813171902

* fix: move accounting balance and checkout to supports listeners to onFlush events

* fix: bad order of filter safeguards

* chore: code style by php-cs-fixer

* fix: prevent uninitialized fields on api resource from breaking AccountingVoter

* fix: small bug preventing code improvements on Accounting api resource

* feat: add cover property on ProjectReward resource

* feat: add cover property to Project resources

* feat: use video cover for project cover

* feat: add migration for property cover on project and rewards

* fix: small improvements on UserOwned helpers

* feat: add territory to project creation

* feat: mark creation fields as required

* feat: add CollaborationCandidacy resource

* fix: update old validations access of new types

* feat: add validation for charges targetting projects not in campaign

* feat: validate new collaboration candicacies are to open collaborations

* feat: prevent future object access issues in SameProjectRewardChargeValidator

* fix: return void on succesfull delete operations

* fix: force set of isFinite property

* feat: allow to filter ProjectBudgetItem by type and deadline

* feat: remove example values from .env

* feat: add testing encryption keys

* feat: make project in BudgetItem body required

* feat: add testing encryption keys to actions

* feat: add collaborations property to ProjectApiResource

* feat: Update Users PATCH body (#246)

* feat: move UserSignupDto to Dto\User namespace

* fix: bad parsing of INSTR by ApiPlatform pagination layer

* fix: make User resources be processed by dedicated update processor

* fix: specify authentication usage of UserSignupDto::password property

* feat: improve non-crawlable URI detection for ScoutService

* feat: add "org" to Links with known organizations behind links

* feat: map raw uri strings to full Link objects

* fix: bad loading of existing user data in UserStateProcessor

* feat: add NotExisting validator

* feat: use NotExisting constraint on Users handle, email, Projects titles and Tipjar names

* fix: check for existing node item in URI rel detection

* fix: make app:oauth2-server:consent-client true by default

* feat: bump league/oauth2-server-bundle to 1.2.2

* feat: add UserConverter decorator to have league/oauth2-server JWT's sub have User id instead of handle

* fix: specify OAuth2 and  separate server from client utilities

* fix: make app_user_provider use ID instead of handle

* feat: add explaining comment to doctrine_encryption.key_id

* feat: add status to reward claims

* feat: make default RewardClaim entity status in_pending

* feat: add shorter check for ownership of projects

* feat: allow to modify status of reward claims

* feat: add shipping address to reward claims

* fix: remove requirement of status in claim update op

* fix: update filters and remove obsolete attributes from ProjectRewardClaim resource

* feat: update DB layer for project reward claims

* fix: code style issues by PHP-CS-Fixer

* feat: add DoctrineLoggablePumpTrait to remove Gedmo Loggable event listener

* fix: update legacy ref to FileUriException

* feat: pump reward claim status and shipping address from v3 records

* fix: update pumps batch sizes and injected dependencies

* feat: pump user roles

* feat: add true image cover retrieval and leave video as fallback for ProjectsPump

* feat: pump project collaborations

* feat: allow cascading of collaborations

* fix: set isFulfilled of collaboration

* fix: link collaboration to in-pumping project

* fix: add trailing comma to list of fulfilling statuses

* test: update PHPUnit CI to use bin/phpunit instead of action

* feat: set memory_limit of PHPUnit run on CI to 128M

* fix: add user avatar to user pump processing

* fix: set invests batch size to 1 to avoid support duplicates on insert

* Merge pull request #253 from goteo/fix/pumping-bugs

fix: Pumping bugs
2026-10-01 16:26:01 +02:00
daniel.subiabre 5d878f2c4c Merge pull request #253 from goteo/fix/pumping-bugs
fix: Pumping bugs
2026-10-01 16:22:42 +02:00
daniel.subiabre 07b0b47e92 Merge pull request #252 from goteo/fix/missing-user-avatar-pumping
fix: missing User's avatar pumping
2026-09-30 17:40:38 +02:00
daniel.subiabre 7732352bb7 fix: set invests batch size to 1 to avoid support duplicates on insert 2026-09-30 17:36:42 +02:00
daniel.subiabre c3b4299097 fix: add user avatar to user pump processing 2026-09-30 17:36:27 +02:00
daniel.subiabre a2371240c2 Merge pull request #251 from goteo/feat/pump-user-roles
feat: update Pumps
2026-09-30 16:51:29 +02:00
daniel.subiabre deef8ad171 feat: set memory_limit of PHPUnit run on CI to 128M 2026-09-30 16:42:48 +02:00
daniel.subiabre 3d0925ca0f test: update PHPUnit CI to use bin/phpunit instead of action 2026-09-30 16:33:26 +02:00
daniel.subiabre 727ddaee15 fix: add trailing comma to list of fulfilling statuses 2026-09-30 15:33:30 +02:00
daniel.subiabre e1e1604f31 fix: link collaboration to in-pumping project 2026-09-30 15:32:45 +02:00
daniel.subiabre 1bcb1f3daa fix: set isFulfilled of collaboration 2026-09-30 15:03:18 +02:00
daniel.subiabre 574b6d3ccf feat: allow cascading of collaborations 2026-09-30 14:55:13 +02:00
daniel.subiabre 10c7806ef6 feat: pump project collaborations 2026-09-30 14:32:50 +02:00
daniel.subiabre 87ccc073df feat: add true image cover retrieval and leave video as fallback for ProjectsPump 2026-09-30 14:15:09 +02:00
daniel.subiabre 74db679146 feat: pump user roles 2026-09-30 13:06:51 +02:00
daniel.subiabre dd5361c925 Merge pull request #250 from goteo/feat/update-project-rewardclaim-data
feat: Update ProjectRewardClaim data
2026-09-30 10:26:36 +02:00
daniel.subiabre 9b8911eefb fix: update pumps batch sizes and injected dependencies 2026-09-30 10:24:27 +02:00
daniel.subiabre 1f6eb4fb4d feat: pump reward claim status and shipping address from v3 records 2026-09-30 10:16:52 +02:00
daniel.subiabre 80178bb7f0 fix: update legacy ref to FileUriException 2026-09-30 09:56:26 +02:00
daniel.subiabre a328073b0f feat: add DoctrineLoggablePumpTrait to remove Gedmo Loggable event listener 2026-09-30 09:55:04 +02:00
daniel.subiabre 995fd339b7 fix: code style issues by PHP-CS-Fixer 2026-09-29 19:07:38 +02:00
daniel.subiabre 6f88f746f3 feat: update DB layer for project reward claims 2026-09-29 19:07:06 +02:00
daniel.subiabre 242755e01e fix: update filters and remove obsolete attributes from ProjectRewardClaim resource 2026-09-29 19:01:18 +02:00
daniel.subiabre 738f2ada84 fix: remove requirement of status in claim update op 2026-09-29 18:55:07 +02:00
daniel.subiabre 81a264a241 feat: add shipping address to reward claims 2026-09-29 18:54:25 +02:00
daniel.subiabre bb297c5fac feat: allow to modify status of reward claims 2026-09-29 18:13:43 +02:00
daniel.subiabre 4fedf2657b feat: add shorter check for ownership of projects 2026-09-29 17:41:31 +02:00
daniel.subiabre 412cf4daf6 feat: make default RewardClaim entity status in_pending 2026-09-29 17:17:09 +02:00
daniel.subiabre 8743c79603 feat: add status to reward claims 2026-09-29 17:01:59 +02:00
daniel.subiabre b0bc61f2f4 feat: add explaining comment to doctrine_encryption.key_id 2026-09-29 14:26:59 +02:00
daniel.subiabre ee369bc699 Merge pull request #248 from goteo/fix/update-jwt-subject
fix: Update JWT subject
2026-09-25 18:42:37 +02:00
daniel.subiabre 1699a43f97 fix: make app_user_provider use ID instead of handle 2026-09-25 18:25:29 +02:00
daniel.subiabre b5e03e285a fix: specify OAuth2 and separate server from client utilities 2026-09-25 18:04:20 +02:00
daniel.subiabre 03ff6f71e8 feat: add UserConverter decorator to have league/oauth2-server JWT's sub have User id instead of handle 2026-09-25 17:56:24 +02:00
daniel.subiabre 6b9014d73f feat: bump league/oauth2-server-bundle to 1.2.2 2026-09-25 17:47:45 +02:00
daniel.subiabre 6973e9a098 fix: make app:oauth2-server:consent-client true by default 2026-09-25 17:05:28 +02:00
daniel.subiabre 6de849ebf8 Merge branch 'main' into develop 2026-09-24 13:23:02 +02:00
daniel.subiabre 9b5b5a9350 feat: Update Users PATCH body (#246)
* feat: move UserSignupDto to Dto\User namespace

* fix: bad parsing of INSTR by ApiPlatform pagination layer

* fix: make User resources be processed by dedicated update processor

* fix: specify authentication usage of UserSignupDto::password property

* feat: improve non-crawlable URI detection for ScoutService

* feat: add "org" to Links with known organizations behind links

* feat: map raw uri strings to full Link objects

* fix: bad loading of existing user data in UserStateProcessor

* feat: add NotExisting validator

* feat: use NotExisting constraint on Users handle, email, Projects titles and Tipjar names

* fix: check for existing node item in URI rel detection
2026-09-23 18:36:55 +02:00
daniel.subiabre a32cedc177 fix: default localization is english
PHPUnit / phpunit (push) Canceled after 0s
PHP CS Fixer / php-cs-fixer (pull_request) Canceled after 0s
2026-09-22 12:47:06 +02:00
daniel.subiabre 93203d8d56 feat: mark API instance as DTF's 2026-09-22 12:46:50 +02:00
daniel.subiabre 28a67064c2 Merge pull request #244 from goteo/feat/add-collaborations-to-projects
feat: add collaborations property to ProjectApiResource
2026-09-17 14:44:42 +02:00
daniel.subiabre 85aac10b6a feat: add collaborations property to ProjectApiResource 2026-09-17 14:44:04 +02:00
daniel.subiabre 7708e390bd Merge pull request #243 from goteo/fix/update-example-env-values
feat: remove example values from .env
2026-09-11 19:38:28 +02:00
daniel.subiabre 6247fd2c54 feat: add testing encryption keys to actions 2026-09-11 19:35:19 +02:00
daniel.subiabre 8ad3e79c19 feat: make project in BudgetItem body required 2026-09-11 19:30:06 +02:00
daniel.subiabre 565d01254e feat: add testing encryption keys 2026-09-11 19:25:51 +02:00
daniel.subiabre 5e96bc94e6 feat: remove example values from .env 2026-09-10 12:07:22 +02:00
daniel.subiabre 0f3b7f90ab Merge pull request #242 from goteo/feat/budget-items-missing-filters
feat: allow to filter ProjectBudgetItem by type and deadline
2026-09-08 17:01:34 +02:00
daniel.subiabre 1bc97973d5 feat: allow to filter ProjectBudgetItem by type and deadline 2026-09-08 16:54:57 +02:00
daniel.subiabre a785eaf87e Merge pull request #241 from goteo/fix/rewards-not-being-set-to-infinite
fix: force set of isFinite property
2026-09-03 16:17:46 +02:00
daniel.subiabre 4d4e7a7db6 fix: force set of isFinite property 2026-09-03 16:14:46 +02:00
daniel.subiabre 7b08eca22c Merge pull request #240 from goteo/fix/bad-return-in-delete-operations
fix: return void on succesfull delete operations
2026-09-03 15:48:01 +02:00
daniel.subiabre a48fd6d4b7 fix: return void on succesfull delete operations 2026-09-03 15:28:40 +02:00
daniel.subiabre 5527097178 Merge pull request #239 from goteo/feat/project-collaborations-apply
feat: Let Users apply to ProjectCollaborations
2026-08-26 14:30:28 +02:00
daniel.subiabre 0a887e8884 feat: prevent future object access issues in SameProjectRewardChargeValidator 2026-08-26 12:59:12 +02:00
daniel.subiabre 28c27ad4ed feat: validate new collaboration candicacies are to open collaborations 2026-08-26 12:53:39 +02:00
daniel.subiabre 40ff4b5ca4 feat: add validation for charges targetting projects not in campaign 2026-08-26 11:52:03 +02:00
daniel.subiabre 4d4de0fc7b fix: update old validations access of new types 2026-08-26 11:21:46 +02:00
daniel.subiabre 5c54d1bf5b feat: add CollaborationCandidacy resource 2026-08-26 11:03:37 +02:00
daniel.subiabre 9633cb50a6 Merge pull request #238 from goteo/feat/post-project-with-territory
feat: mark creation fields as required
2026-08-25 14:07:58 +02:00
daniel.subiabre 31af0cb72b feat: mark creation fields as required 2026-08-25 13:25:17 +02:00
daniel.subiabre 87f6670cab Merge pull request #237 from goteo/feat/post-project-with-territory
feat: add territory to project creation
2026-08-25 12:44:25 +02:00
daniel.subiabre 6d2389125f feat: add territory to project creation 2026-08-25 12:22:43 +02:00
daniel.subiabre 28e0999a0f Merge pull request #236 from goteo/feat/cover-images
feat: Cover images
2026-08-18 11:35:23 +02:00
daniel.subiabre 80ac9684ab fix: small improvements on UserOwned helpers 2026-08-18 11:14:56 +02:00
daniel.subiabre 7a6a2628d7 feat: add migration for property cover on project and rewards 2026-08-18 11:07:37 +02:00
daniel.subiabre ee61072c6a feat: use video cover for project cover 2026-08-18 11:02:36 +02:00
daniel.subiabre 5965f65c4a feat: add cover property to Project resources 2026-08-18 10:58:45 +02:00
daniel.subiabre 8b7f480e46 feat: add cover property on ProjectReward resource 2026-08-18 10:53:42 +02:00
daniel.subiabre 294c447db9 Merge pull request #235 from goteo/hotfix/bad-order-of-filter-safeguards
fix: bad order of filter safeguards
2026-08-17 18:50:06 +02:00
daniel.subiabre 4b7648dc99 fix: small bug preventing code improvements on Accounting api resource 2026-08-17 17:08:42 +02:00
daniel.subiabre 492a211d02 fix: prevent uninitialized fields on api resource from breaking AccountingVoter 2026-08-17 16:12:03 +02:00
daniel.subiabre e1c9b29593 chore: code style by php-cs-fixer 2026-08-17 15:05:18 +02:00
daniel.subiabre 4e60b980dc fix: bad order of filter safeguards 2026-08-17 15:04:10 +02:00
daniel.subiabre 5bd40405d4 Merge pull request #234 from goteo/fix/update-checkout-listeners-to-onflush-event
fix: move accounting balance and checkout to supports listeners to on…
2026-08-17 12:57:52 +02:00
daniel.subiabre 799112a3c2 fix: move accounting balance and checkout to supports listeners to onFlush events 2026-08-14 20:40:44 +02:00
daniel.subiabre 111e4ce72d Merge pull request #233 from goteo/hotfix/doctrine-migration-order
fix: order of DROP operations in migration Version20260813171902
2026-08-14 18:16:11 +02:00
daniel.subiabre 4f8b0a1361 fix: order of DROP operations in migration Version20260813171902 2026-08-14 18:13:18 +02:00
daniel.subiabre 8b93dc6e2a Merge pull request #232 from goteo/feat/bump-benzina
feat: Bump goteo/benzina
2026-08-14 16:06:49 +02:00
daniel.subiabre e9bb6aa28f fix: update Projects test suite to new project shape 2026-08-14 15:15:22 +02:00
daniel.subiabre c1d05ce47b fix: update constraints on project update to allow optional updates of properties 2026-08-14 15:14:59 +02:00
daniel.subiabre e3ef9ea80d fix: allow filtering on project descBrief property 2026-08-14 15:11:24 +02:00
daniel.subiabre 3d800b6326 feat: make categories work on idOrSlug 2026-08-13 19:35:36 +02:00
daniel.subiabre e95383774f feat: bump symfony related dependencies 2026-08-13 18:11:58 +02:00
daniel.subiabre d0e4895208 feat: bump doctrine related dependencies 2026-08-13 18:10:20 +02:00
daniel.subiabre c0fab8a098 feat: split project description into 4 fields 2026-08-13 16:47:15 +02:00
daniel.subiabre 79ef243d11 fix: update project drafts deadline to 2026 2026-08-13 15:18:26 +02:00
daniel.subiabre c6dad06cf5 fix: handle empty strings for video sources 2026-08-13 15:18:09 +02:00
daniel.subiabre 72a04d412b fix: build project addresses from goteo 3 territory fields 2026-08-12 14:44:41 +02:00
daniel.subiabre 3b3fa996f8 fix: allow long addresses for territory data 2026-08-12 14:42:00 +02:00
daniel.subiabre 995eef64fe feat: increase Nominatim cache TTL 2026-08-10 16:12:11 +02:00
daniel.subiabre 5803affb2f fix: upgrade goteo/benzina to 0.6 2026-08-10 16:12:11 +02:00
96 changed files with 3041 additions and 409 deletions
+5 -1
View File
@@ -17,7 +17,10 @@
###> symfony/framework-bundle ### ###> symfony/framework-bundle ###
APP_ENV=dev APP_ENV=dev
APP_SECRET=b8ec44a08e5f4ba917879713a4a8e63c APP_SECRET=b8ec44a08e5f4ba917879713a4a8e63c
APP_LOCALE='es' APP_LOCALE='en'
# DO NOT LEAVE OPEN UNLESS YOU'VE GUARANTEED THAT TRAFFIC WILL ONLY COME FROM TRUSTED ORIGINS
# https://symfony.com/doc/6.4/deployment/proxies.html
APP_TRUSTED_PROXIES='127.0.0.1,REMOTE_ADDR'
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ### ###> doctrine/doctrine-bundle ###
@@ -69,6 +72,7 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
OAUTH_PASSPHRASE=df7ec1f514c44825926e3dabaf34e3f5 OAUTH_PASSPHRASE=df7ec1f514c44825926e3dabaf34e3f5
OAUTH_ENCRYPTION_KEY=325b6ce98c009bb83e885657ec08d142 OAUTH_ENCRYPTION_KEY=325b6ce98c009bb83e885657ec08d142
OAUTH_ACCESS_TOKEN_TTL=PT1H
###< league/oauth2-server-bundle ### ###< league/oauth2-server-bundle ###
###> knpuniversity/oauth2-client-bundle ### ###> knpuniversity/oauth2-client-bundle ###
+7 -5
View File
@@ -34,11 +34,13 @@ jobs:
php_version: ${{ env.PHP_VERSION }} php_version: ${{ env.PHP_VERSION }}
php_extensions: ${{ env.PHP_EXTENSIONS }} php_extensions: ${{ env.PHP_EXTENSIONS }}
- name: PHP diagnostics
run: |
php -v
php --ini
php -r 'echo "PHP_VERSION=", PHP_VERSION, PHP_EOL;'
- name: PHPUnit - name: PHPUnit
uses: php-actions/phpunit@v4 run: php -d memory_limit=128M bin/phpunit -c phpunit.xml.dist
env: env:
DATABASE_URL: mysql://root:goteo@127.0.0.1:${{ job.services.mariadb.ports['3306'] }}/goteo DATABASE_URL: mysql://root:goteo@127.0.0.1:${{ job.services.mariadb.ports['3306'] }}/goteo
with:
php_version: ${{ env.PHP_VERSION }}
php_extensions: ${{ env.PHP_EXTENSIONS }}
configuration: phpunit.xml.dist
Generated
+153 -154
View File
@@ -3916,16 +3916,16 @@
}, },
{ {
"name": "league/oauth2-server-bundle", "name": "league/oauth2-server-bundle",
"version": "v1.1.1", "version": "v1.2.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/oauth2-server-bundle.git", "url": "https://github.com/thephpleague/oauth2-server-bundle.git",
"reference": "b0094bef19cb0dfa526ea9eec6d8a863b91a3f7d" "reference": "db8d6a267b3bb76451816751539d50aa56588247"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-server-bundle/zipball/b0094bef19cb0dfa526ea9eec6d8a863b91a3f7d", "url": "https://api.github.com/repos/thephpleague/oauth2-server-bundle/zipball/db8d6a267b3bb76451816751539d50aa56588247",
"reference": "b0094bef19cb0dfa526ea9eec6d8a863b91a3f7d", "reference": "db8d6a267b3bb76451816751539d50aa56588247",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3934,16 +3934,18 @@
"nyholm/psr7": "^1.4", "nyholm/psr7": "^1.4",
"php": "^8.1", "php": "^8.1",
"psr/http-factory": "^1.0", "psr/http-factory": "^1.0",
"symfony/deprecation-contracts": "^3", "symfony/deprecation-contracts": "^3.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0", "symfony/event-dispatcher": "^6.4|^7.4|^8.0",
"symfony/filesystem": "^6.4|^7.0|^8.0", "symfony/filesystem": "^6.4|^7.4|^8.0",
"symfony/framework-bundle": "^6.4|^7.0|^8.0", "symfony/framework-bundle": "^6.4|^7.4|^8.0",
"symfony/psr-http-message-bridge": "^6.4|^7.0|^8.0", "symfony/password-hasher": "^6.4|^7.4|^8.0",
"symfony/security-bundle": "^6.4|^7.0|^8.0" "symfony/psr-http-message-bridge": "^6.4|^7.4|^8.0",
"symfony/security-bundle": "^6.4|^7.4|^8.0"
}, },
"conflict": { "conflict": {
"doctrine/doctrine-bundle": "<2.8.0", "doctrine/doctrine-bundle": "<2.8.0",
"doctrine/orm": "<2.14" "doctrine/orm": "<2.14",
"symfony/console": "<6.1"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-bundle": "^2.8|^3.0", "doctrine/doctrine-bundle": "^2.8|^3.0",
@@ -3953,19 +3955,15 @@
"php-cs-fixer/shim": "^3.38", "php-cs-fixer/shim": "^3.38",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpstan/phpstan-symfony": "^2.0", "phpstan/phpstan-symfony": "^2.0",
"symfony/browser-kit": "^6.4|^7.0|^8.0", "rector/rector": "^2.5",
"symfony/phpunit-bridge": "^7.3" "symfony/browser-kit": "^6.4|^7.4|^8.0",
"symfony/phpunit-bridge": "^8.1"
}, },
"suggest": { "suggest": {
"doctrine/doctrine-bundle": "Allow usage of doctrine as persistence layer", "doctrine/doctrine-bundle": "Allow usage of doctrine as persistence layer",
"doctrine/orm": "Allow usage of doctrine as persistence layer" "doctrine/orm": "Allow usage of doctrine as persistence layer"
}, },
"type": "symfony-bundle", "type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"League\\Bundle\\OAuth2ServerBundle\\": "src/" "League\\Bundle\\OAuth2ServerBundle\\": "src/"
@@ -3996,9 +3994,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/oauth2-server-bundle/issues", "issues": "https://github.com/thephpleague/oauth2-server-bundle/issues",
"source": "https://github.com/thephpleague/oauth2-server-bundle/tree/v1.1.1" "source": "https://github.com/thephpleague/oauth2-server-bundle/tree/v1.2.2"
}, },
"time": "2026-02-06T11:56:53+00:00" "time": "2026-07-26T12:46:32+00:00"
}, },
{ {
"name": "league/uri", "name": "league/uri",
@@ -5934,16 +5932,16 @@
}, },
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/cache.git", "url": "https://github.com/symfony/cache.git",
"reference": "8f0b5de5d9f5131aa99fdab78d7802efef9322dc" "reference": "b001ef092224de1358462ceabfbdf4d4c2d4c9d2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/8f0b5de5d9f5131aa99fdab78d7802efef9322dc", "url": "https://api.github.com/repos/symfony/cache/zipball/b001ef092224de1358462ceabfbdf4d4c2d4c9d2",
"reference": "8f0b5de5d9f5131aa99fdab78d7802efef9322dc", "reference": "b001ef092224de1358462ceabfbdf4d4c2d4c9d2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -5966,7 +5964,7 @@
"symfony/cache-implementation": "1.1|2.0|3.0" "symfony/cache-implementation": "1.1|2.0|3.0"
}, },
"require-dev": { "require-dev": {
"cache/integration-tests": "dev-master", "cache/integration-tests": "^0.18",
"doctrine/dbal": "^2.13.1|^3|^4", "doctrine/dbal": "^2.13.1|^3|^4",
"predis/predis": "^1.1|^2.0", "predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0",
@@ -6010,7 +6008,7 @@
"psr6" "psr6"
], ],
"support": { "support": {
"source": "https://github.com/symfony/cache/tree/v6.4.43" "source": "https://github.com/symfony/cache/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -6030,7 +6028,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-28T21:33:22+00:00" "time": "2026-09-08T06:25:25+00:00"
}, },
{ {
"name": "symfony/cache-contracts", "name": "symfony/cache-contracts",
@@ -6192,16 +6190,16 @@
}, },
{ {
"name": "symfony/config", "name": "symfony/config",
"version": "v6.4.43", "version": "v6.4.44",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/config.git", "url": "https://github.com/symfony/config.git",
"reference": "e39cdd91b2adf67a117bea29660b73d896937d86" "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/e39cdd91b2adf67a117bea29660b73d896937d86", "url": "https://api.github.com/repos/symfony/config/zipball/6a5abf67fd5b138df96984fe6f8fd736fb7d6b29",
"reference": "e39cdd91b2adf67a117bea29660b73d896937d86", "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6247,7 +6245,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/config/tree/v6.4.43" "source": "https://github.com/symfony/config/tree/v6.4.44"
}, },
"funding": [ "funding": [
{ {
@@ -6267,7 +6265,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-21T09:46:53+00:00" "time": "2026-08-20T09:38:57+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
@@ -6369,16 +6367,16 @@
}, },
{ {
"name": "symfony/dependency-injection", "name": "symfony/dependency-injection",
"version": "v6.4.43", "version": "v6.4.44",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/dependency-injection.git", "url": "https://github.com/symfony/dependency-injection.git",
"reference": "f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905" "reference": "675b2dfaf70bdca37d13816582023e3bac5fc2d1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905", "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/675b2dfaf70bdca37d13816582023e3bac5fc2d1",
"reference": "f11815fd2f5f80ce94bf527b5ecfd4bd0b1cc905", "reference": "675b2dfaf70bdca37d13816582023e3bac5fc2d1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6430,7 +6428,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application", "description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/dependency-injection/tree/v6.4.43" "source": "https://github.com/symfony/dependency-injection/tree/v6.4.44"
}, },
"funding": [ "funding": [
{ {
@@ -6450,7 +6448,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-20T15:13:12+00:00" "time": "2026-08-21T15:20:39+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@@ -6715,16 +6713,16 @@
}, },
{ {
"name": "symfony/error-handler", "name": "symfony/error-handler",
"version": "v6.4.43", "version": "v6.4.44",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/error-handler.git", "url": "https://github.com/symfony/error-handler.git",
"reference": "ecec4c500623ee0ac0c925c3426bcb3e7f429b14" "reference": "8fa59eb915a14b2881993565a122c9dd1b0f8a19"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/ecec4c500623ee0ac0c925c3426bcb3e7f429b14", "url": "https://api.github.com/repos/symfony/error-handler/zipball/8fa59eb915a14b2881993565a122c9dd1b0f8a19",
"reference": "ecec4c500623ee0ac0c925c3426bcb3e7f429b14", "reference": "8fa59eb915a14b2881993565a122c9dd1b0f8a19",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6770,7 +6768,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code", "description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/error-handler/tree/v6.4.43" "source": "https://github.com/symfony/error-handler/tree/v6.4.44"
}, },
"funding": [ "funding": [
{ {
@@ -6790,20 +6788,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-21T10:11:50+00:00" "time": "2026-08-20T17:31:15+00:00"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v6.4.43", "version": "v6.4.44",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "ac405d324c10ebbbde6a6e58379bf81db10f1dbf" "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ac405d324c10ebbbde6a6e58379bf81db10f1dbf", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7110a878e7e7cbd8aaebe9f55da8af885b8c0af",
"reference": "ac405d324c10ebbbde6a6e58379bf81db10f1dbf", "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6854,7 +6852,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v6.4.43" "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.44"
}, },
"funding": [ "funding": [
{ {
@@ -6874,7 +6872,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-21T14:00:19+00:00" "time": "2026-08-21T14:01:40+00:00"
}, },
{ {
"name": "symfony/event-dispatcher-contracts", "name": "symfony/event-dispatcher-contracts",
@@ -7026,16 +7024,16 @@
}, },
{ {
"name": "symfony/filesystem", "name": "symfony/filesystem",
"version": "v6.4.43", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/filesystem.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "9ff03da12d67649fbd1f34ca95951554624d0a16" "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/9ff03da12d67649fbd1f34ca95951554624d0a16", "url": "https://api.github.com/repos/symfony/filesystem/zipball/6dabf41c21957a2060f101bd7bf7e322989866bd",
"reference": "9ff03da12d67649fbd1f34ca95951554624d0a16", "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7072,7 +7070,7 @@
"description": "Provides basic utilities for the filesystem", "description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/filesystem/tree/v6.4.43" "source": "https://github.com/symfony/filesystem/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -7092,20 +7090,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-27T10:13:35+00:00" "time": "2026-08-23T07:59:31+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v6.4.42", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "0b73dac42493acbadbba644207a715b254e9b029" "reference": "04db08efd85558162ef43fba10c299871b60d60b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/0b73dac42493acbadbba644207a715b254e9b029", "url": "https://api.github.com/repos/symfony/finder/zipball/04db08efd85558162ef43fba10c299871b60d60b",
"reference": "0b73dac42493acbadbba644207a715b254e9b029", "reference": "04db08efd85558162ef43fba10c299871b60d60b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7140,7 +7138,7 @@
"description": "Finds files and directories via an intuitive fluent interface", "description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/finder/tree/v6.4.42" "source": "https://github.com/symfony/finder/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -7160,7 +7158,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-26T15:18:24+00:00" "time": "2026-09-10T14:17:24+00:00"
}, },
{ {
"name": "symfony/flex", "name": "symfony/flex",
@@ -7237,16 +7235,16 @@
}, },
{ {
"name": "symfony/framework-bundle", "name": "symfony/framework-bundle",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/framework-bundle.git", "url": "https://github.com/symfony/framework-bundle.git",
"reference": "dcec940410874561f58edb75e2f3574c8a1340d3" "reference": "407e77216969a366fd2bbf6d96a84c6b4a6500a2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/framework-bundle/zipball/dcec940410874561f58edb75e2f3574c8a1340d3", "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/407e77216969a366fd2bbf6d96a84c6b4a6500a2",
"reference": "dcec940410874561f58edb75e2f3574c8a1340d3", "reference": "407e77216969a366fd2bbf6d96a84c6b4a6500a2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7280,7 +7278,7 @@
"symfony/form": "<5.4", "symfony/form": "<5.4",
"symfony/http-client": "<6.3", "symfony/http-client": "<6.3",
"symfony/lock": "<5.4", "symfony/lock": "<5.4",
"symfony/mailer": "<5.4", "symfony/mailer": "<6.4.44|>=7.0,<7.4.17|>=8.0,<8.1.5",
"symfony/messenger": "<6.3", "symfony/messenger": "<6.3",
"symfony/mime": "<6.4.37|>=7.0,<7.4.9", "symfony/mime": "<6.4.37|>=7.0,<7.4.9",
"symfony/property-access": "<5.4", "symfony/property-access": "<5.4",
@@ -7366,7 +7364,7 @@
"description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/framework-bundle/tree/v6.4.43" "source": "https://github.com/symfony/framework-bundle/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -7386,7 +7384,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-26T09:00:38+00:00" "time": "2026-09-13T09:15:59+00:00"
}, },
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
@@ -7570,16 +7568,16 @@
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-foundation.git", "url": "https://github.com/symfony/http-foundation.git",
"reference": "ea0c801ec34e9017a8c9363e55c8ef5f1717216e" "reference": "08bbdd5cf09248785442c41929aabe42f8ef5cfd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/ea0c801ec34e9017a8c9363e55c8ef5f1717216e", "url": "https://api.github.com/repos/symfony/http-foundation/zipball/08bbdd5cf09248785442c41929aabe42f8ef5cfd",
"reference": "ea0c801ec34e9017a8c9363e55c8ef5f1717216e", "reference": "08bbdd5cf09248785442c41929aabe42f8ef5cfd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7627,7 +7625,7 @@
"description": "Defines an object-oriented layer for the HTTP specification", "description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-foundation/tree/v6.4.43" "source": "https://github.com/symfony/http-foundation/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -7647,20 +7645,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-29T06:55:26+00:00" "time": "2026-09-14T17:40:49+00:00"
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "cc35556a9bc9e9a6a35fb1114609d1e3c9a63738" "reference": "c662aa16f2a6b078800ef5302444043c38932213"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/cc35556a9bc9e9a6a35fb1114609d1e3c9a63738", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c662aa16f2a6b078800ef5302444043c38932213",
"reference": "cc35556a9bc9e9a6a35fb1114609d1e3c9a63738", "reference": "c662aa16f2a6b078800ef5302444043c38932213",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7745,7 +7743,7 @@
"description": "Provides a structured process for converting a Request into a Response", "description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-kernel/tree/v6.4.43" "source": "https://github.com/symfony/http-kernel/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -7765,7 +7763,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-29T11:30:30+00:00" "time": "2026-09-15T07:06:31+00:00"
}, },
{ {
"name": "symfony/intl", "name": "symfony/intl",
@@ -8359,16 +8357,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.38.0", "version": "v1.42.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502",
"reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8420,7 +8418,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0"
}, },
"funding": [ "funding": [
{ {
@@ -8440,7 +8438,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-05-25T13:48:31+00:00" "time": "2026-08-07T06:33:24+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
@@ -8772,16 +8770,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v6.4.41", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "c8fc09bdfe9fde9aaa89b415a4477feaccec16a7" "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c8fc09bdfe9fde9aaa89b415a4477feaccec16a7", "url": "https://api.github.com/repos/symfony/process/zipball/b9f3d9ddf67543b92dbfa6d210c407d7a7982781",
"reference": "c8fc09bdfe9fde9aaa89b415a4477feaccec16a7", "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8813,7 +8811,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v6.4.41" "source": "https://github.com/symfony/process/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -8833,20 +8831,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-05-23T13:47:21+00:00" "time": "2026-09-02T12:35:55+00:00"
}, },
{ {
"name": "symfony/property-access", "name": "symfony/property-access",
"version": "v6.4.32", "version": "v6.4.44",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/property-access.git", "url": "https://github.com/symfony/property-access.git",
"reference": "6dfa655ac9e9860c05cabb287f34da86b18c237e" "reference": "57793e8d1e3ede2d4a65604e8f213a3cfa8d1721"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/property-access/zipball/6dfa655ac9e9860c05cabb287f34da86b18c237e", "url": "https://api.github.com/repos/symfony/property-access/zipball/57793e8d1e3ede2d4a65604e8f213a3cfa8d1721",
"reference": "6dfa655ac9e9860c05cabb287f34da86b18c237e", "reference": "57793e8d1e3ede2d4a65604e8f213a3cfa8d1721",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8894,7 +8892,7 @@
"reflection" "reflection"
], ],
"support": { "support": {
"source": "https://github.com/symfony/property-access/tree/v6.4.32" "source": "https://github.com/symfony/property-access/tree/v6.4.44"
}, },
"funding": [ "funding": [
{ {
@@ -8914,20 +8912,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-05T08:25:17+00:00" "time": "2026-07-30T11:04:25+00:00"
}, },
{ {
"name": "symfony/property-info", "name": "symfony/property-info",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/property-info.git", "url": "https://github.com/symfony/property-info.git",
"reference": "ff32a0d7885f6f8997ee0aca499fd008852ad5bd" "reference": "4789c9a2650f9285da6db0363ef40e5a21b42ce9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/property-info/zipball/ff32a0d7885f6f8997ee0aca499fd008852ad5bd", "url": "https://api.github.com/repos/symfony/property-info/zipball/4789c9a2650f9285da6db0363ef40e5a21b42ce9",
"reference": "ff32a0d7885f6f8997ee0aca499fd008852ad5bd", "reference": "4789c9a2650f9285da6db0363ef40e5a21b42ce9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8984,7 +8982,7 @@
"validator" "validator"
], ],
"support": { "support": {
"source": "https://github.com/symfony/property-info/tree/v6.4.43" "source": "https://github.com/symfony/property-info/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -9004,7 +9002,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-22T12:35:29+00:00" "time": "2026-09-03T19:31:46+00:00"
}, },
{ {
"name": "symfony/psr-http-message-bridge", "name": "symfony/psr-http-message-bridge",
@@ -9095,16 +9093,16 @@
}, },
{ {
"name": "symfony/routing", "name": "symfony/routing",
"version": "v6.4.43", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/routing.git", "url": "https://github.com/symfony/routing.git",
"reference": "f8dedd42e7a511906aa54be6cebffbbe826ff20e" "reference": "6fe2ed5b356beec41d5720dd73db72887c3f9d6d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/f8dedd42e7a511906aa54be6cebffbbe826ff20e", "url": "https://api.github.com/repos/symfony/routing/zipball/6fe2ed5b356beec41d5720dd73db72887c3f9d6d",
"reference": "f8dedd42e7a511906aa54be6cebffbbe826ff20e", "reference": "6fe2ed5b356beec41d5720dd73db72887c3f9d6d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9158,7 +9156,7 @@
"url" "url"
], ],
"support": { "support": {
"source": "https://github.com/symfony/routing/tree/v6.4.43" "source": "https://github.com/symfony/routing/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -9178,7 +9176,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-21T13:51:56+00:00" "time": "2026-08-16T17:07:07+00:00"
}, },
{ {
"name": "symfony/runtime", "name": "symfony/runtime",
@@ -9265,16 +9263,16 @@
}, },
{ {
"name": "symfony/security-bundle", "name": "symfony/security-bundle",
"version": "v6.4.43", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/security-bundle.git", "url": "https://github.com/symfony/security-bundle.git",
"reference": "c77d9d6076e6bb8ba706b8616519e3f52f6b2660" "reference": "de06cd0f756ca8fe4894c7e95bac71d90bb94e64"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/security-bundle/zipball/c77d9d6076e6bb8ba706b8616519e3f52f6b2660", "url": "https://api.github.com/repos/symfony/security-bundle/zipball/de06cd0f756ca8fe4894c7e95bac71d90bb94e64",
"reference": "c77d9d6076e6bb8ba706b8616519e3f52f6b2660", "reference": "de06cd0f756ca8fe4894c7e95bac71d90bb94e64",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9357,7 +9355,7 @@
"description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "description": "Provides a tight integration of the Security component into the Symfony full-stack framework",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/security-bundle/tree/v6.4.43" "source": "https://github.com/symfony/security-bundle/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -9377,20 +9375,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-29T06:43:01+00:00" "time": "2026-08-30T06:17:28+00:00"
}, },
{ {
"name": "symfony/security-core", "name": "symfony/security-core",
"version": "v6.4.43", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/security-core.git", "url": "https://github.com/symfony/security-core.git",
"reference": "9d2741948b0eb1fc08fd5431909dac8316ba49c2" "reference": "8f74c37460002b97c1318007629c8be37681498a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/security-core/zipball/9d2741948b0eb1fc08fd5431909dac8316ba49c2", "url": "https://api.github.com/repos/symfony/security-core/zipball/8f74c37460002b97c1318007629c8be37681498a",
"reference": "9d2741948b0eb1fc08fd5431909dac8316ba49c2", "reference": "8f74c37460002b97c1318007629c8be37681498a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9417,6 +9415,7 @@
"symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/ldap": "^5.4|^6.0|^7.0", "symfony/ldap": "^5.4|^6.0|^7.0",
"symfony/property-access": "^5.4|^6.0|^7.0",
"symfony/string": "^5.4|^6.0|^7.0", "symfony/string": "^5.4|^6.0|^7.0",
"symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3",
"symfony/validator": "^6.4|^7.0" "symfony/validator": "^6.4|^7.0"
@@ -9447,7 +9446,7 @@
"description": "Symfony Security Component - Core Library", "description": "Symfony Security Component - Core Library",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/security-core/tree/v6.4.43" "source": "https://github.com/symfony/security-core/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -9467,7 +9466,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-13T05:00:30+00:00" "time": "2026-08-30T00:30:12+00:00"
}, },
{ {
"name": "symfony/security-csrf", "name": "symfony/security-csrf",
@@ -9543,16 +9542,16 @@
}, },
{ {
"name": "symfony/security-http", "name": "symfony/security-http",
"version": "v6.4.42", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/security-http.git", "url": "https://github.com/symfony/security-http.git",
"reference": "c6a3836e6c331503a841f439912939c4e1be480f" "reference": "a8ad51e48f20b515e589b5927ee7cde7e1e37453"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/security-http/zipball/c6a3836e6c331503a841f439912939c4e1be480f", "url": "https://api.github.com/repos/symfony/security-http/zipball/a8ad51e48f20b515e589b5927ee7cde7e1e37453",
"reference": "c6a3836e6c331503a841f439912939c4e1be480f", "reference": "a8ad51e48f20b515e589b5927ee7cde7e1e37453",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9611,7 +9610,7 @@
"description": "Symfony Security Component - HTTP Integration", "description": "Symfony Security Component - HTTP Integration",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/security-http/tree/v6.4.42" "source": "https://github.com/symfony/security-http/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -9631,7 +9630,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-16T12:40:39+00:00" "time": "2026-09-15T05:55:29+00:00"
}, },
{ {
"name": "symfony/serializer", "name": "symfony/serializer",
@@ -9737,16 +9736,16 @@
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
"version": "v3.7.1", "version": "v3.7.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/service-contracts.git", "url": "https://github.com/symfony/service-contracts.git",
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9800,7 +9799,7 @@
"standards" "standards"
], ],
"support": { "support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.7.1" "source": "https://github.com/symfony/service-contracts/tree/v3.7.3"
}, },
"funding": [ "funding": [
{ {
@@ -9820,7 +9819,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-16T09:55:08+00:00" "time": "2026-07-27T15:39:01+00:00"
}, },
{ {
"name": "symfony/stimulus-bundle", "name": "symfony/stimulus-bundle",
@@ -9963,16 +9962,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v6.4.43", "version": "v6.4.46",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "2a8d515c3eaa5d33cf76d5fa277cdadd0a4e5b49" "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/2a8d515c3eaa5d33cf76d5fa277cdadd0a4e5b49", "url": "https://api.github.com/repos/symfony/string/zipball/4513ab2b0ad2d7845a2f920e2f30b61633c9e606",
"reference": "2a8d515c3eaa5d33cf76d5fa277cdadd0a4e5b49", "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10028,7 +10027,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v6.4.43" "source": "https://github.com/symfony/string/tree/v6.4.46"
}, },
"funding": [ "funding": [
{ {
@@ -10048,7 +10047,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-28T07:28:15+00:00" "time": "2026-09-11T09:03:57+00:00"
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
@@ -10696,16 +10695,16 @@
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v6.4.43", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "6638cfe00907b2b980b749fcf2cd4cd6b2b1396e" "reference": "86a9b2f1bd81c9780a809632238ab6ba10292166"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/6638cfe00907b2b980b749fcf2cd4cd6b2b1396e", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/86a9b2f1bd81c9780a809632238ab6ba10292166",
"reference": "6638cfe00907b2b980b749fcf2cd4cd6b2b1396e", "reference": "86a9b2f1bd81c9780a809632238ab6ba10292166",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10760,7 +10759,7 @@
"dump" "dump"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-dumper/tree/v6.4.43" "source": "https://github.com/symfony/var-dumper/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -10780,20 +10779,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-06T08:03:42+00:00" "time": "2026-08-30T20:10:39+00:00"
}, },
{ {
"name": "symfony/var-exporter", "name": "symfony/var-exporter",
"version": "v6.4.42", "version": "v6.4.45",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-exporter.git", "url": "https://github.com/symfony/var-exporter.git",
"reference": "15e670e0218bfe4efbc0556b8a12ad1304f4c7f4" "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/15e670e0218bfe4efbc0556b8a12ad1304f4c7f4", "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a4e458999ee554a667cd19227f0b97a751bd61e6",
"reference": "15e670e0218bfe4efbc0556b8a12ad1304f4c7f4", "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10841,7 +10840,7 @@
"serialize" "serialize"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-exporter/tree/v6.4.42" "source": "https://github.com/symfony/var-exporter/tree/v6.4.45"
}, },
"funding": [ "funding": [
{ {
@@ -10861,7 +10860,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-06-27T08:12:01+00:00" "time": "2026-08-23T09:39:49+00:00"
}, },
{ {
"name": "symfony/web-link", "name": "symfony/web-link",
+4 -4
View File
@@ -1,14 +1,14 @@
api_platform: api_platform:
title: The v4 API title: The Goteo v4 API
version: 4.0.0 version: 4.0.0
show_webby: false show_webby: false
collection: collection:
order: "DESC" order: "DESC"
openapi: openapi:
contact: contact:
name: Fundación Platoniq name: Democratic Tech Fund
url: http://platoniq.net/contact/ url: https://democratictech.fund/contact/
email: contacto@platoniq.net email: democratictech@freeknowledge.eu
termsOfService: termsOfService:
license: license:
name: AGPL name: AGPL
+2
View File
@@ -2,4 +2,6 @@
doctrine_encryption: doctrine_encryption:
encrypt_key: "%env(DOCTRINE_ENCRYPTION_ENCRYPT_KEY)%" encrypt_key: "%env(DOCTRINE_ENCRYPTION_ENCRYPT_KEY)%"
blind_index_key: "%env(DOCTRINE_ENCRYPTION_BLIND_INDEX_KEY)%" blind_index_key: "%env(DOCTRINE_ENCRYPTION_BLIND_INDEX_KEY)%"
# Necessary for key rotation
# https://github.com/Kyzegs/doctrine-encryption-bundle#key-rotation
key_id: "2026" key_id: "2026"
+3 -1
View File
@@ -26,7 +26,9 @@ framework:
http_client: http_client:
default_options: default_options:
headers: headers:
'User-Agent': 'Platoniq Goteo-v4-API' 'User-Agent': 'DTF Goteo v4 API'
trusted_proxies: '%env(APP_TRUSTED_PROXIES)%'
when@test: when@test:
framework: framework:
@@ -2,6 +2,7 @@ league_oauth2_server:
client: client:
classname: App\Entity\OAuth2Client classname: App\Entity\OAuth2Client
authorization_server: authorization_server:
access_token_ttl: '%env(OAUTH_ACCESS_TOKEN_TTL)%'
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%' private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%'
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%' encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
+1 -1
View File
@@ -13,7 +13,7 @@ security:
app_user_provider: app_user_provider:
entity: entity:
class: App\Entity\User\User class: App\Entity\User\User
property: handle property: id
login_user_provider: login_user_provider:
id: App\Security\LoginUserProvider id: App\Security\LoginUserProvider
firewalls: firewalls:
+1
View File
@@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed # Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters: parameters:
env(OAUTH_ACCESS_TOKEN_TTL): PT1H
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260929170504 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add to project_reward_claim a status and shipping address.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE project_reward_claim ADD status VARCHAR(255) NOT NULL, ADD shipping_address_first_name VARCHAR(255) NOT NULL, ADD shipping_address_last_name VARCHAR(255) NOT NULL, ADD shipping_address_address_line1 VARCHAR(255) NOT NULL, ADD shipping_address_address_line2 VARCHAR(255) DEFAULT NULL, ADD shipping_address_city VARCHAR(255) NOT NULL, ADD shipping_address_post_code VARCHAR(255) NOT NULL, ADD shipping_address_country VARCHAR(2) NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE project_reward_claim DROP status, DROP shipping_address_first_name, DROP shipping_address_last_name, DROP shipping_address_address_line1, DROP shipping_address_address_line2, DROP shipping_address_city, DROP shipping_address_post_code, DROP shipping_address_country');
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261001120621 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add address table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE address (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, line1 VARCHAR(255) NOT NULL, line2 VARCHAR(255) DEFAULT NULL, city VARCHAR(255) NOT NULL, post_code VARCHAR(255) NOT NULL, country VARCHAR(2) NOT NULL, INDEX IDX_D4E6F81A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE address ADD CONSTRAINT FK_D4E6F81A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE project_reward_claim ADD address_id INT DEFAULT NULL, DROP shipping_address_first_name, DROP shipping_address_last_name, DROP shipping_address_address_line1, DROP shipping_address_address_line2, DROP shipping_address_city, DROP shipping_address_post_code, DROP shipping_address_country');
$this->addSql('ALTER TABLE project_reward_claim ADD CONSTRAINT FK_EA126118F5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id)');
$this->addSql('CREATE INDEX IDX_EA126118F5B7AF75 ON project_reward_claim (address_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE project_reward_claim DROP FOREIGN KEY FK_EA126118F5B7AF75');
$this->addSql('ALTER TABLE address DROP FOREIGN KEY FK_D4E6F81A76ED395');
$this->addSql('DROP TABLE address');
$this->addSql('DROP INDEX IDX_EA126118F5B7AF75 ON project_reward_claim');
$this->addSql('ALTER TABLE project_reward_claim ADD shipping_address_first_name VARCHAR(255) DEFAULT NULL, ADD shipping_address_last_name VARCHAR(255) DEFAULT NULL, ADD shipping_address_address_line1 VARCHAR(255) DEFAULT NULL, ADD shipping_address_address_line2 VARCHAR(255) DEFAULT NULL, ADD shipping_address_city VARCHAR(255) DEFAULT NULL, ADD shipping_address_post_code VARCHAR(255) DEFAULT NULL, ADD shipping_address_country VARCHAR(2) DEFAULT NULL, DROP address_id');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261006114714 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add review, review_area and review_comment tables';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE review (id INT AUTO_INCREMENT NOT NULL, project_id INT NOT NULL, reviewer_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, date_created DATETIME NOT NULL, date_updated DATETIME NOT NULL, INDEX IDX_794381C6166D1F9C (project_id), INDEX IDX_794381C670574616 (reviewer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE review_area (id INT AUTO_INCREMENT NOT NULL, review_id INT NOT NULL, title VARCHAR(255) NOT NULL, summary LONGTEXT DEFAULT NULL, risk VARCHAR(255) NOT NULL, date_created DATETIME NOT NULL, date_updated DATETIME NOT NULL, INDEX IDX_352070703E2E969B (review_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE review_comment (id INT AUTO_INCREMENT NOT NULL, author_id INT DEFAULT NULL, area_id INT NOT NULL, body LONGTEXT NOT NULL, date_created DATETIME NOT NULL, date_updated DATETIME NOT NULL, INDEX IDX_F9AE69BF675F31B (author_id), INDEX IDX_F9AE69BBD0F409C (area_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C6166D1F9C FOREIGN KEY (project_id) REFERENCES project (id)');
$this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C670574616 FOREIGN KEY (reviewer_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE review_area ADD CONSTRAINT FK_352070703E2E969B FOREIGN KEY (review_id) REFERENCES review (id)');
$this->addSql('ALTER TABLE review_comment ADD CONSTRAINT FK_F9AE69BF675F31B FOREIGN KEY (author_id) REFERENCES user (id)');
$this->addSql('ALTER TABLE review_comment ADD CONSTRAINT FK_F9AE69BBD0F409C FOREIGN KEY (area_id) REFERENCES review_area (id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE review DROP FOREIGN KEY FK_794381C6166D1F9C');
$this->addSql('ALTER TABLE review DROP FOREIGN KEY FK_794381C670574616');
$this->addSql('ALTER TABLE review_area DROP FOREIGN KEY FK_352070703E2E969B');
$this->addSql('ALTER TABLE review_comment DROP FOREIGN KEY FK_F9AE69BF675F31B');
$this->addSql('ALTER TABLE review_comment DROP FOREIGN KEY FK_F9AE69BBD0F409C');
$this->addSql('DROP TABLE review');
$this->addSql('DROP TABLE review_area');
$this->addSql('DROP TABLE review_comment');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261007154315 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add theme tables';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE project_theme (project_id INT NOT NULL, theme_id INT NOT NULL, INDEX IDX_8420EE96166D1F9C (project_id), INDEX IDX_8420EE9659027487 (theme_id), PRIMARY KEY(project_id, theme_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE theme (id INT AUTO_INCREMENT NOT NULL, slug VARCHAR(56) NOT NULL, name VARCHAR(255) NOT NULL, locales JSON NOT NULL COMMENT \'(DC2Type:json)\', UNIQUE INDEX UNIQ_9775E708989D9B62 (slug), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE project_theme ADD CONSTRAINT FK_8420EE96166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE project_theme ADD CONSTRAINT FK_8420EE9659027487 FOREIGN KEY (theme_id) REFERENCES theme (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE project_theme DROP FOREIGN KEY FK_8420EE96166D1F9C');
$this->addSql('ALTER TABLE project_theme DROP FOREIGN KEY FK_8420EE9659027487');
$this->addSql('DROP TABLE project_theme');
$this->addSql('DROP TABLE theme');
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "org.goteo.api", "name": "dtf-api",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+73
View File
@@ -0,0 +1,73 @@
<?php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\ApiResource\User\UserApiResource;
use App\Entity\Address;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Addresses represent named locations that postal services can reach.
*/
#[API\ApiResource(
shortName: 'Address',
stateOptions: new Options(entityClass: Address::class),
processor: ApiResourceStateProcessor::class,
provider: ApiResourceStateProvider::class,
)]
class AddressApiResource
{
#[API\ApiProperty(identifier: true, writable: false)]
public int $id;
#[Assert\NotBlank()]
#[API\ApiProperty(security: 'is_granted("USER_EDIT", object.user)')]
public UserApiResource $user;
/**
* First name(s) of the person receiving the shipment.
*/
#[Assert\NotBlank()]
public string $firstName;
/**
* Last name(s) of the person receiving the shipment.
*/
#[Assert\NotBlank()]
public string $lastName;
/**
* Line 1: usually street name and number.
*/
#[Assert\NotBlank()]
public string $line1;
/**
* Line 2: additional data like apartment number, door, etc.
*/
public ?string $line2 = null;
/**
* Name of the city, or the lowest-available type of settlement to which the address lines belong.
*/
#[Assert\NotBlank()]
public string $city;
/**
* Postal/PIN/ZIP code to which the address lines belong.
*/
#[Assert\NotBlank()]
public string $postCode;
/**
* ISO 3166-1 alpha-2 two-letter country code.\
* e.g: ES (Spain).
*/
#[Assert\NotBlank()]
#[Assert\Country()]
public string $country;
}
+2 -3
View File
@@ -12,9 +12,8 @@ use App\State\CategoryStateProvider;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
* A Category can be used by other resources as a "topic intent".\ * A Category, like in books, is a way of grouping resources by type, genre, audience, or subject, for example:
* For example. Projects might relate with up to 2 Categories, which are used by the Project * science-fiction, biography, fantasy, etc.\
* as a way to describe itself and can be used to discover similar Projects.\
* \ * \
* Categories can only be modified by users with the role "ROLE_ADMIN", but can usually * Categories can only be modified by users with the role "ROLE_ADMIN", but can usually
* be referenced by non-admin users in their own resources, such as Project owners. * be referenced by non-admin users in their own resources, such as Project owners.
@@ -10,6 +10,7 @@ use App\ApiResource\Accounting\AccountingApiResource;
use App\ApiResource\CategoryApiResource; use App\ApiResource\CategoryApiResource;
use App\ApiResource\LocalizedApiResourceTrait; use App\ApiResource\LocalizedApiResourceTrait;
use App\ApiResource\Matchfunding\MatchCallSubmissionApiResource; use App\ApiResource\Matchfunding\MatchCallSubmissionApiResource;
use App\ApiResource\ThemeApiResource;
use App\ApiResource\TimestampedCreationApiResource; use App\ApiResource\TimestampedCreationApiResource;
use App\ApiResource\TimestampedUpdationApiResource; use App\ApiResource\TimestampedUpdationApiResource;
use App\ApiResource\User\UserApiResource; use App\ApiResource\User\UserApiResource;
@@ -134,6 +135,16 @@ class ProjectApiResource
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')] #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
public array $categories; public array $categories;
/**
* A list of the available Themes of this Project.
*
* @var ThemeApiResource[]
*/
#[Assert\NotBlank()]
#[API\ApiProperty(writableLink: false)]
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
public array $themes;
/** /**
* ISO 3166 data about the Project's territory of interest. * ISO 3166 data about the Project's territory of interest.
*/ */
@@ -0,0 +1,57 @@
<?php
namespace App\ApiResource\Project;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\ApiResource\TimestampedCreationApiResource;
use App\ApiResource\TimestampedUpdationApiResource;
use App\ApiResource\User\UserApiResource;
use App\Entity\Project\Review;
use App\Entity\Project\ReviewType;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
/**
* ProjectReviews are launched as health-checks for Projects.\
* \
* The reviews are focused on certain aspects of a Project's lifecycle. The types `campaign` and `financial` reviews
* are to evaluate the fitness of a Project before being greenlit for campaigning or the legitimacy of their raised funds, respectively.\
* \
* ProjectReview resources cannot be manually created, they are created automatically when a Project moves into a "to review" status:
* - `to_campaign_review`: will launch a related ProjectReview of `campaign` type
* - `to_financial_review`: will launch a related ProjectReview of `financial` type.
*/
#[API\ApiResource(
shortName: 'ProjectReview',
stateOptions: new Options(entityClass: Review::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class
)]
#[API\GetCollection()]
#[API\Get()]
#[API\Delete()]
#[API\Patch()]
class ReviewApiResource
{
use TimestampedCreationApiResource;
use TimestampedUpdationApiResource;
#[API\ApiProperty(identifier: true, writable: false)]
public int $id;
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public ProjectApiResource $project;
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public UserApiResource $reviewer;
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public ReviewType $type;
/**
* @var ReviewAreaApiResource[]
*/
public array $areas;
}
@@ -0,0 +1,54 @@
<?php
namespace App\ApiResource\Project;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\ApiResource\TimestampedCreationApiResource;
use App\ApiResource\TimestampedUpdationApiResource;
use App\Entity\Project\ReviewArea;
use App\Entity\Project\ReviewAreaRisk;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
/**
* A ProjectReviewArea represents one specific topic of evaluation for ProjectReviews.\
* \
* Conversations, feedback and evolution of the ProjectReview must happen around specific areas of review.\
* Each area holds an associated risk, which represents the trust the reviewer has for the reviewed Project's health in that area,
* areas can only be updated by the assigned reviewer or an user with `ROLE_ADMIN`.
*/
#[API\ApiResource(
shortName: 'ProjectReviewArea',
stateOptions: new Options(entityClass: ReviewArea::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class,
)]
#[API\GetCollection()]
#[API\Post(security: 'is_granted("ROLE_ADMIN")')]
#[API\Get(security: 'is_granted("REVIEWAREA_VIEW", object)')]
#[API\Patch(security: 'is_granted("REVIEWAREA_EDIT", object)')]
#[API\Delete(security: 'is_granted("REVIEWAREA_EDIT", object)')]
class ReviewAreaApiResource
{
use TimestampedCreationApiResource;
use TimestampedUpdationApiResource;
#[API\ApiProperty(identifier: true, writable: false)]
public int $id;
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public ReviewApiResource $review;
public string $title;
public string $summary;
public ReviewAreaRisk $risk;
/**
* @var ReviewCommentApiResource[]
*/
public array $comments;
}
@@ -0,0 +1,50 @@
<?php
namespace App\ApiResource\Project;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\ApiResource\TimestampedCreationApiResource;
use App\ApiResource\TimestampedUpdationApiResource;
use App\ApiResource\User\UserApiResource;
use App\Entity\Project\ReviewComment;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use App\Validator\UserOwned;
use Symfony\Component\Validator\Constraints as Assert;
/**
* ProjectReviewComments hold the conversation between the reviewer and the reviewed Project owner.
*/
#[API\ApiResource(
shortName: 'ProjectReviewComment',
stateOptions: new Options(entityClass: ReviewComment::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class,
)]
#[API\GetCollection()]
#[API\Post(securityPostDenormalize: 'is_granted("REVIEWAREA_VIEW", object.area)')]
#[API\Get(security: 'is_granted("REVIEWAREA_VIEW", object.area)')]
#[API\Delete(security: 'is_granted("REVIEWAREA_VIEW", object.area)')]
class ReviewCommentApiResource
{
use TimestampedCreationApiResource;
use TimestampedUpdationApiResource;
#[API\ApiProperty(identifier: true, writable: false)]
public int $id;
#[Assert\NotBlank()]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public ReviewAreaApiResource $area;
#[UserOwned()]
#[Assert\NotBlank()]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public UserApiResource $author;
#[Assert\NotBlank()]
#[Assert\Length(min: 15)]
public string $body;
}
@@ -5,17 +5,21 @@ namespace App\ApiResource\Project;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API; use ApiPlatform\Metadata as API;
use App\ApiResource\AddressApiResource;
use App\ApiResource\Gateway\ChargeApiResource; use App\ApiResource\Gateway\ChargeApiResource;
use App\ApiResource\User\UserApiResource; use App\ApiResource\User\UserApiResource;
use App\Dto\RewardClaimCreationDto; use App\Dto\RewardClaimCreationDto;
use App\Dto\RewardClaimUpdationDto;
use App\Entity\Project\RewardClaim; use App\Entity\Project\RewardClaim;
use App\Entity\Project\RewardClaimStatus;
use App\State\ApiResourceStateProvider; use App\State\ApiResourceStateProvider;
use App\State\Project\RewardClaimStateProcessor; use App\State\Project\RewardClaimStateProcessor;
use App\Validator\AvailableRewardUnits;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* A ProjectRewardClaim represents the will of an User who wishes to obtain one ProjectReward. * A ProjectRewardClaim represents the will of an User who wishes to obtain one ProjectReward.\
* \
* Once created ProjectRewardClaims can only be deleted by the User who owns it,
* while their status can only be updated by the User who owns the Project of the claimed ProjectReward.
*/ */
#[API\ApiResource( #[API\ApiResource(
shortName: 'ProjectRewardClaim', shortName: 'ProjectRewardClaim',
@@ -26,31 +30,43 @@ use Symfony\Component\Validator\Constraints as Assert;
#[API\GetCollection()] #[API\GetCollection()]
#[API\Post(input: RewardClaimCreationDto::class)] #[API\Post(input: RewardClaimCreationDto::class)]
#[API\Get()] #[API\Get()]
#[API\Delete()] #[API\Patch(input: RewardClaimUpdationDto::class)]
#[API\Delete(security: 'is_granted("CLAIM_OWNS", object)')]
class RewardClaimApiResource class RewardClaimApiResource
{ {
#[API\ApiProperty(identifier: true, writable: false)] #[API\ApiProperty(identifier: true, writable: false)]
public int $id; public int $id;
/** /**
* The User claiming the ProjectReward. * The User claiming the ProjectReward. Derived from the GatewayCharge.
*/ */
#[API\ApiProperty(writable: false)]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')] #[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public UserApiResource $owner; public UserApiResource $owner;
/** /**
* The GatewayCharge granting access to the ProjectReward. * The GatewayCharge granting access to the ProjectReward.
*/ */
#[Assert\NotBlank()]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')] #[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public ChargeApiResource $charge; public ChargeApiResource $charge;
/** /**
* The ProjectReward being claimed. * The ProjectReward being claimed.
*/ */
#[Assert\NotBlank()]
#[AvailableRewardUnits()]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')] #[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public RewardApiResource $reward; public RewardApiResource $reward;
/**
* The point at which the claim is in its life-cylce.
*/
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public RewardClaimStatus $status;
/**
* Only used when the reward is a physical object that needs to be shipped.
*/
#[API\ApiProperty(readableLink: true)]
#[API\ApiFilter(SearchFilter::class, properties: ['address.city' => 'partial'])]
#[API\ApiFilter(SearchFilter::class, properties: ['address.postCode' => 'partial'])]
#[API\ApiFilter(SearchFilter::class, properties: ['address.country' => 'partial'])]
public ?AddressApiResource $address;
} }
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\Entity\Theme;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use App\State\ThemeStateProvider;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A Theme, like in books, works as a "topic intent" grouping, for example:
* friendship, good vs. evil, power and corruption, etc.\
* \
* Themes can only be modified by users with the role "ROLE_ADMIN", but can usually
* be referenced by non-admin users in their own resources, such as Project owners.
*/
#[API\ApiResource(
shortName: 'Theme',
stateOptions: new Options(entityClass: Theme::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class,
)]
#[API\GetCollection()]
#[API\Post(security: 'is_granted("ROLE_ADMIN")')]
#[API\Get(
provider: ThemeStateProvider::class,
uriTemplate: '/themes/{idOrSlug}',
uriVariables: [
'idOrSlug' => new API\Link(
description: 'Theme identifier or slug',
),
]
)]
#[API\Patch(security: 'is_granted("ROLE_ADMIN")')]
#[API\Delete(security: 'is_granted("ROLE_ADMIN")')]
class ThemeApiResource
{
use LocalizedApiResourceTrait;
/**
* This value will identify this Theme in relationships with other resources.
*/
#[API\ApiProperty(identifier: true)]
public string $id;
/**
* A unique, non white space, string identifier for this Theme.
*/
#[API\ApiProperty(writable: false)]
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
public string $slug;
/**
* A human-readable self-descriptive string of what this Theme is about.
*/
#[Assert\NotBlank()]
#[API\ApiProperty()]
#[API\ApiFilter(SearchFilter::class, strategy: 'partial')]
public string $name;
}
+2
View File
@@ -9,6 +9,7 @@ use App\ApiResource\Accounting\AccountingApiResource;
use App\Entity\Tipjar; use App\Entity\Tipjar;
use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider; use App\State\ApiResourceStateProvider;
use App\Validator\NotExisting;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
/** /**
@@ -40,6 +41,7 @@ class TipjarApiResource
* Human readable, non white space, unique string. * Human readable, non white space, unique string.
*/ */
#[Assert\NotBlank()] #[Assert\NotBlank()]
#[NotExisting(Tipjar::class, 'name')]
#[API\ApiFilter(SearchFilter::class, strategy: 'partial')] #[API\ApiFilter(SearchFilter::class, strategy: 'partial')]
public string $name; public string $name;
} }
@@ -42,12 +42,13 @@ class OrganizationApiResource
public UserApiResource $user; public UserApiResource $user;
/** /**
* ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc. * Organization ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc.
*/ */
#[Assert\NotBlank()]
#[Assert\Length(min: 2)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
)] )]
#[Assert\NotBlank()]
public string $taxId; public string $taxId;
/** /**
@@ -55,10 +56,11 @@ class OrganizationApiResource
* as it appears on legal documents issued by or for this organization.\ * as it appears on legal documents issued by or for this organization.\
* Will be used as last option for the display name of the User. * Will be used as last option for the display name of the User.
*/ */
#[Assert\NotBlank()]
#[Assert\Length(min: 1)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
)] )]
#[Assert\NotBlank()]
public string $legalName; public string $legalName;
/** /**
@@ -66,6 +68,7 @@ class OrganizationApiResource
* Might be similar to the legal name or completely different.\ * Might be similar to the legal name or completely different.\
* Will be used as display name for the User when present. * Will be used as display name for the User when present.
*/ */
#[Assert\Length(min: 1)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)',
)] )]
+5 -1
View File
@@ -7,6 +7,7 @@ use ApiPlatform\Metadata as API;
use App\Entity\User\Person; use App\Entity\User\Person;
use App\State\ApiResourceStateProcessor; use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider; use App\State\ApiResourceStateProvider;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* Most times a real person is behind a User account, * Most times a real person is behind a User account,
@@ -43,6 +44,7 @@ class PersonApiResource
/** /**
* Personal ID for tax purposes. e.g: NIF, Steuer-ID, SSN, ITIN, etc. * Personal ID for tax purposes. e.g: NIF, Steuer-ID, SSN, ITIN, etc.
*/ */
#[Assert\Length(min: 2)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
)] )]
@@ -52,6 +54,7 @@ class PersonApiResource
* First-part of the name of the person, * First-part of the name of the person,
* in most western conventions this is the given name(s). e.g: John, Juan, etc. * in most western conventions this is the given name(s). e.g: John, Juan, etc.
*/ */
#[Assert\Length(min: 1)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
)] )]
@@ -59,8 +62,9 @@ class PersonApiResource
/** /**
* Last-part of the name of the person, * Last-part of the name of the person,
* in most western conventions this is the family name(s). e.g: Smith, Herrera García, etc. * in most western conventions this is the family name(s). e.g: Smith, Herrera, etc.
*/ */
#[Assert\Length(min: 1)]
#[API\ApiProperty( #[API\ApiProperty(
securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)',
)] )]
+7 -3
View File
@@ -10,7 +10,8 @@ use ApiPlatform\Metadata as API;
use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\Accounting\AccountingApiResource;
use App\ApiResource\TimestampedCreationApiResource; use App\ApiResource\TimestampedCreationApiResource;
use App\ApiResource\TimestampedUpdationApiResource; use App\ApiResource\TimestampedUpdationApiResource;
use App\Dto\UserSignupDto; use App\Dto\User\UserSignupDto;
use App\Dto\User\UserUpdationDto;
use App\Entity\Territory; use App\Entity\Territory;
use App\Entity\User\User; use App\Entity\User\User;
use App\Entity\User\UserType; use App\Entity\User\UserType;
@@ -48,8 +49,11 @@ use Symfony\Component\Validator\Constraints as Assert;
), ),
] ]
)] )]
#[API\Patch(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] #[API\Patch(
#[API\Delete(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] input: UserUpdationDto::class,
securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)'
)]
#[API\Delete(securityPostDenormalize: 'is_granted("ROLE_ADMIN", previous_object)')]
#[API\ApiFilter(QFilter::class, properties: [ #[API\ApiFilter(QFilter::class, properties: [
'q' => [ 'q' => [
'email', 'email',
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\Benzina;
use App\Entity\Theme;
use App\Repository\ThemeRepository;
use Symfony\Contracts\Service\Attribute\Required;
trait CategorizationPumpTrait
{
private ThemeRepository $themeRepository;
private const THEMES = [
'education' => 'Educación',
'culture' => 'Cultura',
'journalism' => 'Periodismo',
'science-and-tech' => 'Ciencia y Tecnología',
'rural-and-ecology' => 'Desarrollo rural y Transición ecológica',
'health-and-cares' => 'Salud y Cuidados',
'sustainability' => 'Sostenibilidad',
'democracy' => 'Derechos y Democracia',
'social-economy' => 'Economía social',
];
#[Required]
public function setThemeRepository(ThemeRepository $themeRepository)
{
$this->themeRepository = $themeRepository;
}
public function getTheme(string $slug): Theme
{
$theme = $this->themeRepository->findOneBy(['slug' => $slug]);
if (!$theme) {
$theme = new Theme();
$theme->setSlug($slug);
$theme->setName(self::THEMES[$slug]);
}
$theme->addLocale('es');
return $theme;
}
private function matchCategories(array $categories, array $names): bool
{
$categories = array_map(fn($c) => $c['name'], $categories);
$matches = 0;
foreach ($names as $name) {
if (in_array($name, $categories)) {
$matches = $matches + 1;
}
}
return $matches === count($names);
}
private function mapOldCategoriesToThemes(string $socialCommitmentName, array $categories): array
{
$themes = [];
if (
$socialCommitmentName === 'Solidario'
&& $this->matchCategories($categories, ['Social'])
) {
$themes[] = $this->getTheme('democracy');
}
if (
$socialCommitmentName === 'Software libre'
&& $this->matchCategories($categories, ['Tecnológico', 'Científico'])
) {
$themes[] = $this->getTheme('science-and-tech');
}
if (
$socialCommitmentName === 'Generar empleo'
&& $this->matchCategories($categories, ['Emprendedor', 'Social'])
) {
$themes[] = $this->getTheme('social-economy');
}
if (
$socialCommitmentName === 'Periodismo independiente'
&& $this->matchCategories($categories, ['Comunicativo'])
) {
$themes[] = $this->getTheme('journalism');
}
if (
$socialCommitmentName === 'Educativo'
&& $this->matchCategories($categories, ['Educativo'])
) {
$themes[] = $this->getTheme('education');
}
if (
$socialCommitmentName === 'Crear cultura'
&& $this->matchCategories($categories, ['Cultural'])
) {
$themes[] = $this->getTheme('culture');
}
if (
$socialCommitmentName === 'Acción por el clima'
&& $this->matchCategories($categories, ['Ecológico'])
) {
$themes[] = $this->getTheme('sustainability');
}
if (
$socialCommitmentName === 'Datos abiertos'
&& $this->matchCategories($categories, ['Científico', 'Comunicativo'])
) {
$themes[] = $this->getTheme('journalism');
}
if (
$socialCommitmentName === 'Reforzar valores democráticos'
&& $this->matchCategories($categories, ['Social'])
) {
$themes[] = $this->getTheme('democracy');
}
if (
$socialCommitmentName === 'Igualdad de Género'
&& $this->matchCategories($categories, ['Social'])
) {
$themes[] = $this->getTheme('democracy');
}
if (
$socialCommitmentName === 'Salud y Cuidados'
&& $this->matchCategories($categories, ['Social'])
) {
$themes[] = $this->getTheme('health-and-cares');
}
if (
$socialCommitmentName === 'Energía y sostenibilidad'
&& $this->matchCategories($categories, ['Científico', 'Ecológico'])
) {
$themes[] = $this->getTheme('science-and-tech');
}
if (
$socialCommitmentName === 'Desarrollo agrorural'
&& $this->matchCategories($categories, ['Ecológico', 'Emprendedor'])
) {
$themes[] = $this->getTheme('rural-and-ecology');
}
return $themes;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Benzina;
use Doctrine\DBAL\Logging\Middleware as LoggingMiddleware;
use Doctrine\ORM\EntityManagerInterface;
use Gedmo\Loggable\LoggableListener;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Symfony\Contracts\Service\Attribute\Required;
trait DoctrineLoggablePumpTrait
{
use DoctrinePumpTrait;
#[Required()]
public function removeLoggable(EntityManagerInterface $entityManager): void
{
$middlewares = $entityManager->getConnection()->getConfiguration()->getMiddlewares();
$middlewares = \array_filter($middlewares, fn($m) => !$m instanceof LoggingMiddleware);
$entityManager->getConnection()->getConfiguration()->setMiddlewares($middlewares);
$evm = $entityManager->getEventManager();
foreach ($evm->getAllListeners() as $event => $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof LoggableListener) {
$evm->removeEventListener([$event], $listener);
}
}
}
$this->setEntityManager($entityManager);
}
}
+42 -20
View File
@@ -2,25 +2,24 @@
namespace App\Benzina; namespace App\Benzina;
use App\Entity\Address;
use App\Entity\Gateway\Charge; use App\Entity\Gateway\Charge;
use App\Entity\Project\Reward; use App\Entity\Project\Reward;
use App\Entity\Project\RewardClaim; use App\Entity\Project\RewardClaim;
use App\Entity\Project\RewardClaimStatus;
use App\Entity\User\User; use App\Entity\User\User;
use App\Gateway\ChargeStatus; use App\Gateway\ChargeStatus;
use App\Money\MoneyService;
use App\Repository\Gateway\ChargeRepository; use App\Repository\Gateway\ChargeRepository;
use App\Repository\Project\RewardRepository; use App\Repository\Project\RewardRepository;
use App\Repository\User\UserRepository; use App\Service\UserService;
use Doctrine\Common\Collections\Criteria;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
class InvestRewardsPump implements PumpInterface class InvestRewardsPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DatabasePumpTrait; use DatabasePumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use InvestsPumpTrait; use InvestsPumpTrait;
/** @var array<string, int> */ /** @var array<string, int> */
@@ -34,10 +33,11 @@ class InvestRewardsPump implements PumpInterface
public function __construct( public function __construct(
private RewardRepository $rewardRepository, private RewardRepository $rewardRepository,
private UserRepository $userRepository, private PumpedUserRepository $userRepository,
private ChargeRepository $chargeRepository, private ChargeRepository $chargeRepository,
private MoneyService $moneyService, ) {
) {} $this->setFlushBatchSize(8);
}
public function supports(mixed $sample): bool public function supports(mixed $sample): bool
{ {
@@ -81,6 +81,16 @@ class InvestRewardsPump implements PumpInterface
$reward->addClaim($claim); $reward->addClaim($claim);
$claim->setReward($reward); $claim->setReward($reward);
$status = match ($record['fulfilled']) {
0 => RewardClaimStatus::InPending,
1 => RewardClaimStatus::Fulfilled,
};
$claim->setStatus($status);
$address = $this->getAddress($claim, $context);
$claim->setAddress($address);
$this->persist($claim, $context); $this->persist($claim, $context);
} }
@@ -139,22 +149,34 @@ class InvestRewardsPump implements PumpInterface
private function getUser(array $record): ?User private function getUser(array $record): ?User
{ {
$id = $record['user']; return $this->userRepository->findPumped($record['user']);
}
if (isset($this->userCache[$id])) { private function getAddress(RewardClaim $claim, array $context): ?Address
return $this->userRepository->find($this->userCache[$id]); {
$query = $this->getDbConnection($context)->prepare(
'SELECT * FROM `invest_address` a WHERE a.invest = :invest'
);
$query->execute(['invest' => $claim->getCharge()->getMigratedId()]);
$result = $query->fetch(\PDO::FETCH_ASSOC);
if (!$result || $result['name'] === null) {
return null;
} }
$criteria = new Criteria(); [$firstName, $lastName] = UserService::guessNames($result['name']);
$criteria
->orWhere($criteria->expr()->eq('migratedId', $id))
->orWhere($criteria->expr()->contains('dedupedIds', $id))
->setMaxResults(1);
$user = $this->userRepository->matching($criteria)->first(); $address = new Address();
$address->setUser($claim->getOwner());
$address->setFirstName($firstName);
$address->setLastName($lastName);
$address->setLine1($result['address']);
$address->setCity($result['location']);
$address->setPostCode($result['zipcode']);
$address->setCountry($result['country']);
$this->userCache[$id] = $user->getId(); return $address;
return $user;
} }
} }
+9 -6
View File
@@ -27,16 +27,14 @@ use App\Repository\Project\ProjectRepository;
use App\Repository\Project\RewardRepository; use App\Repository\Project\RewardRepository;
use App\Repository\Project\SupportRepository; use App\Repository\Project\SupportRepository;
use App\Repository\TipjarRepository; use App\Repository\TipjarRepository;
use App\Service\Gateway\CheckoutService;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
class InvestsPump implements PumpInterface class InvestsPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DatabasePumpTrait; use DatabasePumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use InvestsPumpTrait; use InvestsPumpTrait;
public const TRACKING_TITLE_V3 = 'v3 Invest ID'; public const TRACKING_TITLE_V3 = 'v3 Invest ID';
@@ -71,9 +69,10 @@ class InvestsPump implements PumpInterface
private SupportRepository $supportRepository, private SupportRepository $supportRepository,
private TipjarRepository $tipjarRepository, private TipjarRepository $tipjarRepository,
private RewardRepository $rewardRepository, private RewardRepository $rewardRepository,
private CheckoutService $checkoutService,
private MoneyService $moneyService, private MoneyService $moneyService,
) {} ) {
$this->setFlushBatchSize(1);
}
public function supports(mixed $sample): bool public function supports(mixed $sample): bool
{ {
@@ -86,6 +85,10 @@ class InvestsPump implements PumpInterface
public function pump(mixed $record, array $context): void public function pump(mixed $record, array $context): void
{ {
if (!in_array($record['project'], ProjectsPump::SELECTED_PROJECTS)) {
return;
}
if (!$record['user'] || empty($record['user'])) { if (!$record['user'] || empty($record['user'])) {
return; return;
} }
@@ -261,7 +264,7 @@ class InvestsPump implements PumpInterface
$originId = $origin->getId(); $originId = $origin->getId();
$projectId = $target->getId(); $projectId = $target->getId();
$cacheKey = $projectId.'-'.$originId; $cacheKey = $projectId . '-' . $originId;
if (isset($this->supportCache[$cacheKey])) { if (isset($this->supportCache[$cacheKey])) {
return $this->supportRepository->find($this->supportCache[$cacheKey]); return $this->supportRepository->find($this->supportCache[$cacheKey]);
+5 -2
View File
@@ -9,14 +9,13 @@ use App\Entity\Project\Project;
use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectDeadline;
use App\Repository\Project\ProjectRepository; use App\Repository\Project\ProjectRepository;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
class ProjectsBudgetPump implements PumpInterface class ProjectsBudgetPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DatabasePumpTrait; use DatabasePumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use LocalizedPumpTrait; use LocalizedPumpTrait;
private const MAX_INT = 2147483647; private const MAX_INT = 2147483647;
@@ -49,6 +48,10 @@ class ProjectsBudgetPump implements PumpInterface
public function pump(mixed $record, array $context): void public function pump(mixed $record, array $context): void
{ {
if (!in_array($record['project'], ProjectsPump::SELECTED_PROJECTS)) {
return;
}
if (empty($record['cost']) || empty($record['amount'])) { if (empty($record['cost']) || empty($record['amount'])) {
return; return;
} }
+126 -17
View File
@@ -2,6 +2,7 @@
namespace App\Benzina; namespace App\Benzina;
use App\Entity\Project\Collaboration;
use App\Entity\Project\Project; use App\Entity\Project\Project;
use App\Entity\Project\ProjectCalendar; use App\Entity\Project\ProjectCalendar;
use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectDeadline;
@@ -11,28 +12,49 @@ use App\Entity\Project\Update;
use App\Entity\Territory; use App\Entity\Territory;
use App\Entity\User\User; use App\Entity\User\User;
use App\Repository\Project\ProjectRepository; use App\Repository\Project\ProjectRepository;
use App\Repository\User\UserRepository;
use App\Service\Project\TerritoryService; use App\Service\Project\TerritoryService;
use App\Service\Scout\FileUriException; use App\Service\Scout\NonCrawlableUriException;
use App\Service\Scout\ScoutService; use App\Service\Scout\ScoutService;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
class ProjectsPump implements PumpInterface class ProjectsPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use DatabasePumpTrait; use DatabasePumpTrait;
use ProjectsPumpTrait; use ProjectsPumpTrait;
use LocalizedPumpTrait; use LocalizedPumpTrait;
use TerritoryPumpTrait; use TerritoryPumpTrait;
use CategorizationPumpTrait;
public const SELECTED_PROJECTS = [
'la-nuestra-despega',
'la-nuestra',
'un-netflix-para-el-99-decenas-de-creadores-y-cinea',
'iridia-vs-pegasus',
'impulsa-la-cooperativa-de-informacion-climatica',
'amb-memoria-drets-humans',
'ara-drets-humans',
'amb-memoria-drets-humans',
'quien-cuida-a-las-que-cuidan',
'con-vosotras-si-campana-de-financiacion-duque-de-a',
'top-manta-bcn',
'salvem-el-delta-del-llobregat',
'refugio-una-huida-para-sobrevivir',
'plokte',
'lanzamos-sonda-internacional-y-los-cinco-elementos',
'la-kacharreria',
'portal-de-ressenyes-sobre-pisos',
'indicat-de-llogaters-i-llogateres/',
'jornaleras',
'las-kellys'
];
public function __construct( public function __construct(
private ProjectRepository $projectRepository, private ProjectRepository $projectRepository,
private UserRepository $userRepository, private PumpedUserRepository $userRepository,
private TerritoryService $territoryService, private TerritoryService $territoryService,
private ScoutService $scoutService, private ScoutService $scoutService,
) {} ) {}
@@ -48,7 +70,7 @@ class ProjectsPump implements PumpInterface
public function pump(mixed $record, array $context): void public function pump(mixed $record, array $context): void
{ {
if (empty($record['name'])) { if (empty($record['name']) || !in_array($record['id'], self::SELECTED_PROJECTS)) {
return; return;
} }
@@ -82,11 +104,18 @@ class ProjectsPump implements PumpInterface
$project->setDateUpdated(new \DateTime()); $project->setDateUpdated(new \DateTime());
$project->setTranslatableLocale($record['lang']); $project->setTranslatableLocale($record['lang']);
$project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context))); $project->setUpdates(new ArrayCollection($this->getProjectUpdates($project, $context)));
$project->setCollaborations(new ArrayCollection($this->getProjectCollaborations($project, $context)));
$video = $this->getProjectVideo($record); $video = $this->getProjectVideo($record);
if ($video !== null) {
$project->setVideo($video);
$project->setCover($video->cover);
}
$project->setVideo($video); $cover = $this->getProjectCover($record, $context);
$project->setCover($video->cover); if ($cover !== null) {
$project->setCover($cover);
}
$conf = $this->getProjectConf($project, $context); $conf = $this->getProjectConf($project, $context);
@@ -101,6 +130,11 @@ class ProjectsPump implements PumpInterface
$project->setDescGoal($record['motivation']); $project->setDescGoal($record['motivation']);
$project->setDescTeam($record['related']); $project->setDescTeam($record['related']);
$themes = $this->getProjectThemes($record, $context);
foreach ($themes as $theme) {
$project->addTheme($theme);
}
$this->setPreventFlushAndClear(true); $this->setPreventFlushAndClear(true);
$this->persist($project, $context); $this->persist($project, $context);
@@ -124,13 +158,7 @@ class ProjectsPump implements PumpInterface
private function getProjectOwner(array $record): ?User private function getProjectOwner(array $record): ?User
{ {
$criteria = new Criteria(); return $this->userRepository->findPumped($record['owner']);
$criteria
->orWhere($criteria->expr()->eq('migratedId', $record['owner']))
->orWhere($criteria->expr()->contains('dedupedIds', $record['owner']))
->setMaxResults(1);
return $this->userRepository->matching($criteria)->first() ?? null;
} }
private function getProjectLocalizations(Project $project, array $context): array private function getProjectLocalizations(Project $project, array $context): array
@@ -240,7 +268,7 @@ class ProjectsPump implements PumpInterface
} }
return new ProjectVideo($info->url, $info->cover ?? $info->image, $info->image); return new ProjectVideo($info->url, $info->cover ?? $info->image, $info->image);
} catch (FileUriException $e) { } catch (NonCrawlableUriException $e) {
return new ProjectVideo($e->getUri()); return new ProjectVideo($e->getUri());
} catch (\Exception $e) { } catch (\Exception $e) {
return null; return null;
@@ -346,4 +374,85 @@ class ProjectsPump implements PumpInterface
return ProjectDeadline::Minimum; return ProjectDeadline::Minimum;
} }
private function getImageSource(array $record, array $context): ?string
{
$query = $this->getDbConnection($context)->prepare(
'SELECT * FROM `project_image` WHERE `project` = :project ORDER BY `order` ASC'
);
$query->execute(['project' => $record['id']]);
$images = $query->fetchAll();
foreach ($images as $image) {
if ($image['section'] === 'play-video') {
return $image['image'];
}
}
return $record['image'];
}
private function getProjectCover(array $record, array $context): ?string
{
$image = $this->getImageSource($record, $context);
if ($image === null || $image === '') {
return null;
}
if (!\str_contains($image, '.')) {
return null;
}
return \sprintf('https://s3.eu-west-1.amazonaws.com/goteoassets.org/images/%s', $image);
}
private function getProjectCollaborations(Project $project, array $context): array
{
$collaborations = [];
$query = $this->getDbConnection($context)->prepare(
'SELECT * FROM `support` s WHERE s.project = :project'
);
$query->execute(['project' => $project->getMigratedId()]);
$supports = $query->fetchAll();
foreach ($supports as $support) {
$fulfilled = in_array($project->getStatus(), [
ProjectStatus::FundingPaid,
]);
$collaboration = new Collaboration();
$collaboration->setTitle($support['support']);
$collaboration->setDescription($support['description']);
$collaboration->setFulfilled($fulfilled);
$collaborations[] = $collaboration;
$project->addCollaboration($collaboration);
}
return $collaborations;
}
private function getProjectThemes(array $record, array $context): array
{
$scQuery = $this->getDbConnection($context)->prepare(
'SELECT * FROM `social_commitment` sc WHERE sc.id = :scId'
);
$scQuery->execute(['scId' => $record['social_commitment']]);
$socialCommitment = $scQuery->fetch();
$catsQuery = $this->getDbConnection($context)->prepare(
'SELECT * FROM `category` c INNER JOIN `project_category` pc ON c.id = pc.category WHERE pc.project = :project'
);
$catsQuery->execute(['project' => $record['id']]);
$categories = $catsQuery->fetchAll();
return $this->mapOldCategoriesToThemes($socialCommitment['name'] ?? '', $categories);
}
} }
+5 -2
View File
@@ -8,14 +8,13 @@ use App\Entity\Project\Project;
use App\Entity\Project\Reward; use App\Entity\Project\Reward;
use App\Repository\Project\ProjectRepository; use App\Repository\Project\ProjectRepository;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
class ProjectsRewardsPump implements PumpInterface class ProjectsRewardsPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DatabasePumpTrait; use DatabasePumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use LocalizedPumpTrait; use LocalizedPumpTrait;
private const REWARD_KEYS = [ private const REWARD_KEYS = [
@@ -53,6 +52,10 @@ class ProjectsRewardsPump implements PumpInterface
public function pump(mixed $record, array $context): void public function pump(mixed $record, array $context): void
{ {
if (!in_array($record['project'], ProjectsPump::SELECTED_PROJECTS)) {
return;
}
if (empty($record['reward']) || empty($record['amount'])) { if (empty($record['reward']) || empty($record['amount'])) {
return; return;
} }
+54 -10
View File
@@ -12,7 +12,6 @@ use App\Service\Project\TerritoryService;
use App\Service\UserService; use App\Service\UserService;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\ArrayPumpTrait;
use Goteo\Benzina\Pump\DoctrinePumpTrait;
use Goteo\Benzina\Pump\PumpInterface; use Goteo\Benzina\Pump\PumpInterface;
use Symfony\Component\Validator\Constraints\Url; use Symfony\Component\Validator\Constraints\Url;
use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validation;
@@ -20,14 +19,17 @@ use Symfony\Component\Validator\Validation;
class UsersPump implements PumpInterface class UsersPump implements PumpInterface
{ {
use ArrayPumpTrait; use ArrayPumpTrait;
use DoctrinePumpTrait; use DoctrineLoggablePumpTrait;
use UsersPumpTrait; use UsersPumpTrait;
use TerritoryPumpTrait; use TerritoryPumpTrait;
use DatabasePumpTrait;
public function __construct( public function __construct(
private ManagerRegistry $managerRegistry, private ManagerRegistry $managerRegistry,
private TerritoryService $territoryService, private TerritoryService $territoryService,
) {} ) {
$this->setFlushBatchSize(8);
}
public function supports(mixed $sample): bool public function supports(mixed $sample): bool
{ {
@@ -41,7 +43,7 @@ class UsersPump implements PumpInterface
public function pump(mixed $record, array $context): void public function pump(mixed $record, array $context): void
{ {
$user = new User(); $user = new User();
$user = $this->processUser($user, $record); $user = $this->processUser($user, $record, $context);
try { try {
$this->persist($user, $context); $this->persist($user, $context);
@@ -62,7 +64,7 @@ class UsersPump implements PumpInterface
} }
$user = new User(); $user = new User();
$user = $this->processUser($user, $record); $user = $this->processUser($user, $record, $context);
$user->setHandle(UserService::asHandle($record['id'], 16, 255)); $user->setHandle(UserService::asHandle($record['id'], 16, 255));
$this->persist($user, $context); $this->persist($user, $context);
@@ -71,11 +73,12 @@ class UsersPump implements PumpInterface
} }
} }
private function processUser(User $user, array $record): User private function processUser(User $user, array $record, array $context): User
{ {
$user->setHandle($this->buildHandle($record)); $handle = hash('sha256', $record['email']);
$user->setHandle($handle);
$user->setPassword($record['password'] ?? ''); $user->setPassword($record['password'] ?? '');
$user->setEmail($record['email']); $user->setEmail(\sprintf('%s@goteo.org', $handle));
$user->setEmailConfirmed(false); $user->setEmailConfirmed(false);
$user->setActive(false); $user->setActive(false);
$user->setMigrated(true); $user->setMigrated(true);
@@ -83,9 +86,11 @@ class UsersPump implements PumpInterface
$user->setDateCreated($this->getDateCreated($record)); $user->setDateCreated($this->getDateCreated($record));
$user->setDateUpdated(new \DateTime()); $user->setDateUpdated(new \DateTime());
$user->setType($this->getUserType($record)); $user->setType($this->getUserType($record));
$user->setLinks($this->getLinks($record)); // $user->setLinks($this->getLinks($record));
$user->setTerritory($this->getTerritory($record)); $user->setTerritory($this->getTerritory($record));
$user->setDescription($record['about']); // $user->setDescription($record['about']);
$user->setRoles($this->getRoles($record, $context));
// $user->setAvatar($this->getAvatar($record));
match ($user->getType()) { match ($user->getType()) {
UserType::Individual => $user = $this->setUserPerson($record, $user), UserType::Individual => $user = $this->setUserPerson($record, $user),
@@ -204,4 +209,43 @@ class UsersPump implements PumpInterface
return $this->territoryService->search($cleanAddress); return $this->territoryService->search($cleanAddress);
} }
private function getRoles(array $record, array $context): array
{
$query = $this->getDbConnection($context)->prepare(
'SELECT * FROM `user_role` r WHERE r.user_id = :user'
);
$query->execute(['user' => $record['id']]);
$results = $query->fetchAll(\PDO::FETCH_ASSOC);
if (!$results || empty($results)) {
return [];
}
$roles = [];
foreach ($results as $result) {
if (in_array($result['role_id'], ['superadmin', 'manager'])) {
$roles[] = 'ROLE_ADMIN';
}
}
return $roles;
}
private function getAvatar(array $record): ?string
{
$image = $record['avatar'];
if ($image === null || $image === '') {
return null;
}
if (!\str_contains($image, '.')) {
return null;
}
return \sprintf('https://s3.eu-west-1.amazonaws.com/goteoassets.org/images/%s', $image);
}
} }
+1 -1
View File
@@ -31,7 +31,7 @@ class OAuth2ConsentClientCommand extends Command
$this $this
->setDescription('Consents an OAuth2 client, allowing it to skip the consent screen') ->setDescription('Consents an OAuth2 client, allowing it to skip the consent screen')
->addOption('consent', null, InputOption::VALUE_NEGATABLE, 'Consent the client') ->addOption('consent', null, InputOption::VALUE_NEGATABLE, 'Consent the client', true)
->addArgument('identifier', InputArgument::REQUIRED, 'The client identifier') ->addArgument('identifier', InputArgument::REQUIRED, 'The client identifier')
; ;
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Dto;
trait CategorizedInputDtoTrait
{
public const array CATEGORIES_OPENAPI_CONTEXT = [
'type' => 'array',
'items' => [
'type' => 'string',
'format' => 'iri-reference',
'example' => '/categories/cooperativism',
],
'example' => [
'/categories/open-source-software',
'/categories/cooperativism',
],
];
public const array THEMES_OPENAPI_CONTEXT = [
'type' => 'array',
'items' => [
'type' => 'string',
'format' => 'iri-reference',
'example' => '/themes/music',
],
'example' => [
'/themes/music',
'/themes/art',
],
];
}
-19
View File
@@ -1,19 +0,0 @@
<?php
namespace App\Dto;
trait CategoryInputDtoTrait
{
public const array CATEGORIES_OPENAPI_CONTEXT = [
'type' => 'array',
'items' => [
'type' => 'string',
'format' => 'iri-reference',
'example' => '/categories/music',
],
'example' => [
'/categories/music',
'/categories/art',
],
];
}
+15 -2
View File
@@ -4,14 +4,17 @@ namespace App\Dto;
use ApiPlatform\Metadata as API; use ApiPlatform\Metadata as API;
use App\ApiResource\CategoryApiResource; use App\ApiResource\CategoryApiResource;
use App\ApiResource\ThemeApiResource;
use App\Entity\Project\Project;
use App\Entity\Project\ProjectCalendar; use App\Entity\Project\ProjectCalendar;
use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectStatus;
use App\Entity\Territory; use App\Entity\Territory;
use App\Validator\NotExisting;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
class ProjectCreationDto class ProjectCreationDto
{ {
use CategoryInputDtoTrait; use CategorizedInputDtoTrait;
/** /**
* Main headline for the Project. Must include at least one character between a-Z. * Main headline for the Project. Must include at least one character between a-Z.
@@ -19,6 +22,7 @@ class ProjectCreationDto
#[Assert\NotBlank()] #[Assert\NotBlank()]
#[Assert\Regex('/[a-zA-Z]{1,}/')] #[Assert\Regex('/[a-zA-Z]{1,}/')]
#[Assert\Length(min: 3)] #[Assert\Length(min: 3)]
#[NotExisting(Project::class, 'title')]
public string $title; public string $title;
/** /**
@@ -33,10 +37,19 @@ class ProjectCreationDto
* @var CategoryApiResource[] * @var CategoryApiResource[]
*/ */
#[Assert\NotBlank()] #[Assert\NotBlank()]
#[Assert\Count(min: 1, max: 2)] #[Assert\Count(exactly: 1)]
#[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)] #[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)]
public array $categories; public array $categories;
/**
* List of Themes.
*
* @var ThemeApiResource[]
*/
#[Assert\Count(min: 1, max: 2)]
#[API\ApiProperty(writableLink: false, openapiContext: self::THEMES_OPENAPI_CONTEXT)]
public array $themes;
/** /**
* ISO 3166 data about the Project's territory of interest. * ISO 3166 data about the Project's territory of interest.
*/ */
+11 -1
View File
@@ -4,6 +4,7 @@ namespace App\Dto;
use ApiPlatform\Metadata as API; use ApiPlatform\Metadata as API;
use App\ApiResource\CategoryApiResource; use App\ApiResource\CategoryApiResource;
use App\ApiResource\ThemeApiResource;
use App\Entity\Project\Project; use App\Entity\Project\Project;
use App\Entity\Project\ProjectCalendar; use App\Entity\Project\ProjectCalendar;
use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectDeadline;
@@ -15,7 +16,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class ProjectUpdationDto class ProjectUpdationDto
{ {
use CategoryInputDtoTrait; use CategorizedInputDtoTrait;
#[API\ApiProperty(identifier: true, writable: false)] #[API\ApiProperty(identifier: true, writable: false)]
public int $id; public int $id;
@@ -47,6 +48,15 @@ class ProjectUpdationDto
#[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)] #[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)]
public array $categories; public array $categories;
/**
* List of Themes.
*
* @var ThemeApiResource[]
*/
#[Assert\Count(max: 1)]
#[API\ApiProperty(writableLink: false, openapiContext: self::THEMES_OPENAPI_CONTEXT)]
public array $themes;
/** /**
* ISO 3166 data about the Project's territory of interest. * ISO 3166 data about the Project's territory of interest.
*/ */
+6
View File
@@ -2,6 +2,7 @@
namespace App\Dto; namespace App\Dto;
use App\ApiResource\AddressApiResource;
use App\ApiResource\Gateway\ChargeApiResource; use App\ApiResource\Gateway\ChargeApiResource;
use App\ApiResource\Project\RewardApiResource; use App\ApiResource\Project\RewardApiResource;
use App\Validator\AvailableRewardUnits; use App\Validator\AvailableRewardUnits;
@@ -27,4 +28,9 @@ class RewardClaimCreationDto
#[Assert\NotBlank()] #[Assert\NotBlank()]
#[AvailableRewardUnits()] #[AvailableRewardUnits()]
public RewardApiResource $reward; public RewardApiResource $reward;
/**
* If the reward is a physical object that needs to be delivered to an specific place.
*/
public ?AddressApiResource $address;
} }
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Dto;
use ApiPlatform\Metadata as API;
use App\ApiResource\AddressApiResource;
use App\Entity\Project\RewardClaimStatus;
class RewardClaimUpdationDto
{
/**
* The point at which the claim over the reward is.\
* May only be updated by admins or the User who owns the Project of the ProjectReward.
*/
#[API\ApiProperty(security: 'is_granted("CLAIM_EDIT", object)')]
public RewardClaimStatus $status;
/**
* If the reward is a physical object that needs to be delivered to an specific place.
*/
#[API\ApiProperty(security: 'is_granted("CLAIM_OWNS", object)')]
public ?AddressApiResource $address;
}
@@ -1,8 +1,10 @@
<?php <?php
namespace App\Dto; namespace App\Dto\User;
use App\Entity\User\User;
use App\Entity\User\UserType; use App\Entity\User\UserType;
use App\Validator\NotExisting;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
final class UserSignupDto final class UserSignupDto
@@ -12,10 +14,11 @@ final class UserSignupDto
*/ */
#[Assert\NotBlank()] #[Assert\NotBlank()]
#[Assert\Email()] #[Assert\Email()]
#[NotExisting(User::class, 'email')]
public string $email; public string $email;
/** /**
* The auth password for the new User. Plaintext string, * The authentication password for the new User. Plaintext string,
* will be hashed by the API. * will be hashed by the API.
*/ */
#[Assert\NotBlank()] #[Assert\NotBlank()]
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Dto\User;
use ApiPlatform\Metadata as API;
use App\ApiResource\User\UserApiResource;
use App\Entity\Territory;
use App\Entity\User\User;
use App\Entity\User\UserType;
use App\Mapping\Transformer\RawLinksMapTransformer;
use App\Validator\NotExisting;
use AutoMapper\Attribute\MapTo;
use Symfony\Component\Validator\Constraints as Assert;
final class UserUpdationDto
{
/**
* A unique, non white space, byte-safe string identifier for this User.
*/
#[Assert\Length(min: 4, max: 30)]
#[Assert\Regex('/^[a-z0-9_]+$/')]
#[NotExisting(User::class, 'handle')]
public string $handle;
/**
* The User's given email address. Only available to themselves and platform administrators.
*/
#[Assert\Email()]
#[NotExisting(User::class, 'email')]
public string $email;
/**
* The authentication password for the User. Plaintext string,
* will be hashed by the API.
*/
#[Assert\Length(min: 8)]
public string $password;
/**
* URL to the avatar image of this User.
*/
#[Assert\Url()]
public string $avatar;
/**
* Is this User for an individual acting on their own or a group of individuals?
*/
public UserType $type;
/**
* A list of the roles assigned to this User. Admin scoped property.
*
* @var array<int, string>
*/
#[API\ApiProperty(securityPostDenormalize: 'is_granted("ROLE_ADMIN")')]
public array $roles;
/**
* A list of absolute URLs.\
* e.g: social profiles, personal website.
*
* @var array<int, string>
*/
#[MapTo(User::class, transformer: RawLinksMapTransformer::class)]
#[MapTo(UserApiResource::class, transformer: RawLinksMapTransformer::class)]
public array $links = [];
/**
* ISO 3166 data about the Users's location territory.
*/
#[Assert\Valid()]
public Territory $territory;
/**
* Free-form rich text description for the User.
*/
public string $description;
}
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Entity;
use App\Entity\User\User;
use App\Repository\AddressRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AddressRepository::class)]
class Address
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $firstName = null;
#[ORM\Column(length: 255)]
private ?string $lastName = null;
#[ORM\Column(length: 255)]
private ?string $line1 = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $line2 = null;
#[ORM\Column(length: 255)]
private ?string $city = null;
#[ORM\Column(length: 255)]
private ?string $postCode = null;
#[ORM\Column(length: 2)]
private ?string $country = null;
#[ORM\ManyToOne(inversedBy: 'addresses')]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
public function getId(): ?int
{
return $this->id;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(string $firstName): static
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(string $lastName): static
{
$this->lastName = $lastName;
return $this;
}
public function getLine1(): ?string
{
return $this->line1;
}
public function setLine1(string $line1): static
{
$this->line1 = $line1;
return $this;
}
public function getLine2(): ?string
{
return $this->line2;
}
public function setLine2(?string $line2): static
{
$this->line2 = $line2;
return $this;
}
public function getCity(): ?string
{
return $this->city;
}
public function setCity(string $city): static
{
$this->city = $city;
return $this;
}
public function getPostCode(): ?string
{
return $this->postCode;
}
public function setPostCode(string $postCode): static
{
$this->postCode = $postCode;
return $this;
}
public function getCountry(): ?string
{
return $this->country;
}
public function setCountry(string $country): static
{
$this->country = $country;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
}
+84 -1
View File
@@ -13,6 +13,7 @@ use App\Entity\Matchfunding\MatchCallSubmission;
use App\Entity\Matchfunding\MatchCallSubmissionStatus; use App\Entity\Matchfunding\MatchCallSubmissionStatus;
use App\Entity\MigratedTrait; use App\Entity\MigratedTrait;
use App\Entity\Territory; use App\Entity\Territory;
use App\Entity\Theme;
use App\Entity\User\User; use App\Entity\User\User;
use App\Entity\UserOwnedInterface; use App\Entity\UserOwnedInterface;
use App\Entity\UserOwnedTrait; use App\Entity\UserOwnedTrait;
@@ -157,10 +158,16 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
/** /**
* @var Collection<int, Collaboration> * @var Collection<int, Collaboration>
*/ */
#[ORM\OneToMany(targetEntity: Collaboration::class, mappedBy: 'project')] #[ORM\OneToMany(targetEntity: Collaboration::class, mappedBy: 'project', cascade: ['persist'])]
private Collection $collaborations; private Collection $collaborations;
/** /**
* @var Collection<int, Review>
*/
#[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'project', cascade: ['persist'])]
private Collection $reviews;
/*
* A list of URLs provided by the Project owner.\ * A list of URLs provided by the Project owner.\
* e.g: social profiles, project website. * e.g: social profiles, project website.
* *
@@ -169,6 +176,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?array $links = null; private ?array $links = null;
/**
* @var Collection<int, Theme>
*/
#[ORM\ManyToMany(targetEntity: Theme::class, cascade: ['persist'])]
private Collection $themes;
public function __construct() public function __construct()
{ {
$this->accounting = Accounting::of($this); $this->accounting = Accounting::of($this);
@@ -179,6 +192,8 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
$this->supports = new ArrayCollection(); $this->supports = new ArrayCollection();
$this->categories = new ArrayCollection(); $this->categories = new ArrayCollection();
$this->collaborations = new ArrayCollection(); $this->collaborations = new ArrayCollection();
$this->reviews = new ArrayCollection();
$this->themes = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@@ -378,6 +393,10 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
return $this->status; return $this->status;
} }
/**
* DO NOT call this method unless you know what you are doing:
* Instead use `ProjectService::transition` or you WILL miss side-effects.
*/
public function setStatus(ProjectStatus $status): static public function setStatus(ProjectStatus $status): static
{ {
$this->status = $status; $this->status = $status;
@@ -561,6 +580,16 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
return $this->collaborations; return $this->collaborations;
} }
/**
* @param Collection<int, Collaboration> $collaborations
*/
public function setCollaborations(Collection $collaborations): static
{
$this->collaborations = $collaborations;
return $this;
}
public function addCollaboration(Collaboration $collaboration): static public function addCollaboration(Collaboration $collaboration): static
{ {
if (!$this->collaborations->contains($collaboration)) { if (!$this->collaborations->contains($collaboration)) {
@@ -584,6 +613,36 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
} }
/** /**
* @return Collection<int, Review>
*/
public function getReviews(): Collection
{
return $this->reviews;
}
public function addReview(Review $review): static
{
if (!$this->reviews->contains($review)) {
$this->reviews->add($review);
$review->setProject($this);
}
return $this;
}
public function removeReview(Review $review): static
{
if ($this->reviews->removeElement($review)) {
// set the owning side to null (unless already changed)
if ($review->getProject() === $this) {
$review->setProject(null);
}
}
return $this;
}
/*
* @return Link[] * @return Link[]
*/ */
public function getLinks(): ?array public function getLinks(): ?array
@@ -600,4 +659,28 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
return $this; return $this;
} }
/**
* @return Collection<int, Theme>
*/
public function getThemes(): Collection
{
return $this->themes;
}
public function addTheme(Theme $theme): static
{
if (!$this->themes->contains($theme)) {
$this->themes->add($theme);
}
return $this;
}
public function removeTheme(Theme $theme): static
{
$this->themes->removeElement($theme);
return $this;
}
} }
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace App\Entity\Project;
use App\Entity\DateCreatedTrait;
use App\Entity\DateUpdatedTrait;
use App\Entity\User\User;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\ReviewRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: ReviewRepository::class)]
class Review
{
use DateCreatedTrait;
use DateUpdatedTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'reviews')]
#[ORM\JoinColumn(nullable: false)]
private ?Project $project = null;
#[ORM\ManyToOne]
private ?User $reviewer = null;
#[ORM\Column(enumType: ReviewType::class)]
private ?ReviewType $type = null;
/**
* @var Collection<int, ReviewArea>
*/
#[ORM\OneToMany(targetEntity: ReviewArea::class, mappedBy: 'review', cascade: ['persist'])]
private Collection $areas;
public function __construct()
{
$this->areas = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
public function getReviewer(): ?User
{
return $this->reviewer;
}
public function isReviewedBy(UserInterface $user): bool
{
return $this->reviewer?->getUserIdentifier() === $user->getUserIdentifier();
}
public function setReviewer(?User $reviewer): static
{
$this->reviewer = $reviewer;
return $this;
}
public function getType(): ?ReviewType
{
return $this->type;
}
public function setType(ReviewType $type): static
{
$this->type = $type;
return $this;
}
/**
* @return Collection<int, ReviewArea>
*/
public function getAreas(): Collection
{
return $this->areas;
}
public function addArea(ReviewArea $area): static
{
if (!$this->areas->contains($area)) {
$this->areas->add($area);
$area->setReview($this);
}
return $this;
}
public function removeArea(ReviewArea $area): static
{
if ($this->areas->removeElement($area)) {
// set the owning side to null (unless already changed)
if ($area->getReview() === $this) {
$area->setReview(null);
}
}
return $this;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace App\Entity\Project;
use App\Entity\DateCreatedTrait;
use App\Entity\DateUpdatedTrait;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\ReviewAreaRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: ReviewAreaRepository::class)]
class ReviewArea
{
use DateCreatedTrait;
use DateUpdatedTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'areas')]
#[ORM\JoinColumn(nullable: false)]
private ?Review $review = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $summary = null;
#[ORM\Column(enumType: ReviewAreaRisk::class)]
private ReviewAreaRisk $risk = ReviewAreaRisk::High;
/**
* @var Collection<int, ReviewComment>
*/
#[ORM\OneToMany(targetEntity: ReviewComment::class, mappedBy: 'area')]
private Collection $comments;
public function __construct()
{
$this->comments = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getReview(): ?Review
{
return $this->review;
}
public function setReview(?Review $review): static
{
$this->review = $review;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getSummary(): ?string
{
return $this->summary;
}
public function setSummary(string $summary): static
{
$this->summary = $summary;
return $this;
}
public function getRisk(): ?ReviewAreaRisk
{
return $this->risk;
}
public function setRisk(?ReviewAreaRisk $risk): static
{
$this->risk = $risk;
return $this;
}
/**
* @return Collection<int, ReviewComment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(ReviewComment $comment): static
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setArea($this);
}
return $this;
}
public function removeComment(ReviewComment $comment): static
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getArea() === $this) {
$comment->setArea(null);
}
}
return $this;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Entity\Project;
enum ReviewAreaRisk: string
{
/**
* The risk acceptable or none. Green light.
*/
case Low = 'low';
/**
* The risk is moderate or somewhat worrying. Yellow light.
*/
case Mid = 'mid';
/**
* The risk is severe and/or worrying. Red light.
*/
case High = 'high';
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Entity\Project;
use App\Entity\DateCreatedTrait;
use App\Entity\DateUpdatedTrait;
use App\Entity\User\User;
use App\Mapping\Provider\EntityMapProvider;
use App\Repository\Project\ReviewCommentRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[MapProvider(EntityMapProvider::class)]
#[ORM\Entity(repositoryClass: ReviewCommentRepository::class)]
class ReviewComment
{
use DateCreatedTrait;
use DateUpdatedTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
private ?User $author = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $body = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)]
private ?ReviewArea $area = null;
public function getId(): ?int
{
return $this->id;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(?User $author): static
{
$this->author = $author;
return $this;
}
public function getBody(): ?string
{
return $this->body;
}
public function setBody(string $body): static
{
$this->body = $body;
return $this;
}
public function getArea(): ?ReviewArea
{
return $this->area;
}
public function setArea(?ReviewArea $area): static
{
$this->area = $area;
return $this;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Entity\Project;
enum ReviewType: string
{
/**
* The goal is to determine if a Project is fit for campaigning or not.
*/
case Campaign = 'campaign';
/**
* The goal is to determine if a Project is fit for payment or not.
*/
case Financial = 'financial';
}
+31
View File
@@ -2,6 +2,7 @@
namespace App\Entity\Project; namespace App\Entity\Project;
use App\Entity\Address;
use App\Entity\Gateway\Charge; use App\Entity\Gateway\Charge;
use App\Entity\User\User; use App\Entity\User\User;
use App\Entity\UserOwnedInterface; use App\Entity\UserOwnedInterface;
@@ -35,6 +36,12 @@ class RewardClaim implements UserOwnedInterface
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private ?Charge $charge = null; private ?Charge $charge = null;
#[ORM\Column(enumType: RewardClaimStatus::class)]
private ?RewardClaimStatus $status = RewardClaimStatus::InPending;
#[ORM\ManyToOne]
private ?Address $address = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@@ -63,4 +70,28 @@ class RewardClaim implements UserOwnedInterface
return $this; return $this;
} }
public function getStatus(): ?RewardClaimStatus
{
return $this->status;
}
public function setStatus(RewardClaimStatus $status): static
{
$this->status = $status;
return $this;
}
public function getAddress(): ?Address
{
return $this->address;
}
public function setAddress(?Address $address): static
{
$this->address = $address;
return $this;
}
} }
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Entity\Project;
enum RewardClaimStatus: string
{
/**
* The claim has not been yet processed by the reward provider.
*/
case InPending = 'in_pending';
/**
* The claim has been fulfilled and the user can be considered as rewarded.
*/
case Fulfilled = 'fulfilled';
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Entity;
use App\Repository\ThemeRepository;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
#[ORM\Entity(repositoryClass: ThemeRepository::class)]
class Theme
{
use LocalizedTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 56, unique: true)]
#[Gedmo\Slug(fields: ['name'])]
private ?string $slug = null;
#[ORM\Column(length: 255)]
#[Gedmo\Translatable()]
private ?string $name = null;
public function getId(): ?int
{
return $this->id;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): static
{
$this->slug = $slug;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
}
+38
View File
@@ -4,6 +4,7 @@ namespace App\Entity\User;
use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Accounting;
use App\Entity\Accounting\AccountingOwnerInterface; use App\Entity\Accounting\AccountingOwnerInterface;
use App\Entity\Address;
use App\Entity\DateCreatedTrait; use App\Entity\DateCreatedTrait;
use App\Entity\DateUpdatedTrait; use App\Entity\DateUpdatedTrait;
use App\Entity\DedupedTrait; use App\Entity\DedupedTrait;
@@ -124,6 +125,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
#[ORM\Column(type: Types::TEXT, nullable: true)] #[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $description = null; private ?string $description = null;
/**
* @var Collection<int, Address>
*/
#[ORM\OneToMany(targetEntity: Address::class, mappedBy: 'user')]
private Collection $addresses;
public function __construct() public function __construct()
{ {
$this->accounting = Accounting::of($this); $this->accounting = Accounting::of($this);
@@ -133,6 +140,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
$this->emailConfirmed = false; $this->emailConfirmed = false;
$this->active = false; $this->active = false;
$this->addresses = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@@ -393,4 +401,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account
return $this; return $this;
} }
/**
* @return Collection<int, Address>
*/
public function getAddresses(): Collection
{
return $this->addresses;
}
public function addAddress(Address $address): static
{
if (!$this->addresses->contains($address)) {
$this->addresses->add($address);
$address->setUser($this);
}
return $this;
}
public function removeAddress(Address $address): static
{
if ($this->addresses->removeElement($address)) {
// set the owning side to null (unless already changed)
if ($address->getUser() === $this) {
$address->setUser(null);
}
}
return $this;
}
} }
-37
View File
@@ -1,37 +0,0 @@
<?php
namespace App\EventListener;
use App\Entity\Project\Project;
use App\Entity\Project\ProjectStatus;
use App\Service\Project\CalendarService;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
#[AsEntityListener(
event: Events::preUpdate,
method: 'preUpdate',
entity: Project::class
)]
final class ProjectsListener
{
public function __construct(
private CalendarService $calendarService,
) {}
public function preUpdate(
Project $project,
PreUpdateEventArgs $event,
) {
if (!$event->hasChangedField('status')) {
return;
}
if ($project->getStatus() === ProjectStatus::InCampaign) {
$calendar = $this->calendarService->makeCalendar($project->getDeadline());
$project->setCalendar($calendar);
}
}
}
+10 -1
View File
@@ -33,6 +33,7 @@ final class OrderedLikeFilter extends AbstractFilter
): void { ): void {
if ( if (
!$this->isPropertyEnabled($property, $resourceClass) !$this->isPropertyEnabled($property, $resourceClass)
|| !$this->isPropertyMapped($property, $resourceClass)
) { ) {
return; return;
} }
@@ -40,10 +41,18 @@ final class OrderedLikeFilter extends AbstractFilter
$rootAlias = $queryBuilder->getRootAliases()[0]; $rootAlias = $queryBuilder->getRootAliases()[0];
$likeParameter = $queryNameGenerator->generateParameterName($property); $likeParameter = $queryNameGenerator->generateParameterName($property);
$instrParameter = $queryNameGenerator->generateParameterName(sprintf('%s%s', $property, 'instr')); $instrParameter = $queryNameGenerator->generateParameterName(sprintf('%s%s', $property, 'instr'));
$positionAlias = $queryNameGenerator->generateJoinAlias('position');
$queryBuilder $queryBuilder
->andWhere(sprintf('%s.%s LIKE :%s', $rootAlias, $property, $likeParameter)) ->andWhere(sprintf('%s.%s LIKE :%s', $rootAlias, $property, $likeParameter))
->addOrderBy(sprintf('INSTR(%s.%s, :%s)', $rootAlias, $property, $instrParameter), 'ASC') ->addSelect(sprintf(
'INSTR(%s.%s, :%s) AS HIDDEN %s',
$rootAlias,
$property,
$instrParameter,
$positionAlias
))
->addOrderBy($positionAlias, 'ASC')
->addOrderBy(sprintf('%s.%s', $rootAlias, $property), 'ASC') ->addOrderBy(sprintf('%s.%s', $rootAlias, $property), 'ASC')
->setParameter($likeParameter, sprintf('%%%s%%', $value)) ->setParameter($likeParameter, sprintf('%%%s%%', $value))
->setParameter($instrParameter, $value) ->setParameter($instrParameter, $value)
+18 -3
View File
@@ -16,15 +16,30 @@ class Link
*/ */
public ?string $rel; public ?string $rel;
public static function tryFrom($value): self /**
* The known organization behind this link.
*/
public function getOrg(): LinkOrg
{
return LinkOrg::fromUrl($this->url);
}
public static function tryFrom(mixed $value): self
{ {
if ($value instanceof self) { if ($value instanceof self) {
return $value; return $value;
} }
if (\is_array($value)) {
$link = new self();
$link->url = $value['url'];
$link->rel = $value['rel'];
return $link;
}
$link = new self(); $link = new self();
$link->url = $value['url']; $link->url = $value;
$link->rel = $value['rel'];
return $link; return $link;
} }
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Library;
enum LinkOrg: string
{
case Instagram = 'instagram';
case Facebook = 'facebook';
case GitHub = 'github';
case LinkedIn = 'linkedin';
case Identica = 'identica';
case Unknown = 'unknown';
private static function domains(): array
{
return [
'instagram.com' => self::Instagram,
'facebook.com' => self::Facebook,
'fb.com' => self::Facebook,
'github.com' => self::GitHub,
'linkedin.com' => self::LinkedIn,
'identi.ca' => self::Identica,
];
}
public static function fromUrl(string $url): self
{
$host = \parse_url($url, PHP_URL_HOST) ?? '';
$host = \join('.', \array_slice(\explode('.', $host), -2));
foreach (self::domains() as $domain => $network) {
if ($host === $domain || \str_ends_with($host, '.'.$domain)) {
return $network;
}
}
return self::Unknown;
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Mapping\Transformer;
use App\Library\Link;
use App\Service\Scout\ScoutResult;
use App\Service\Scout\ScoutService;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
class RawLinksMapTransformer implements PropertyTransformerInterface
{
public function __construct(
private ScoutService $scoutService,
) {}
public function transform(mixed $value, object|array $source, array $context): mixed
{
$links = [];
foreach ($value as $rawLink) {
$info = $this->scoutService->get($rawLink);
$link = new Link();
$link->url = $info->getUri();
$link->rel = $this->getRel($info);
$links[] = $link;
}
return $links;
}
private function getRel(ScoutResult $info): ?string
{
$nodes = (new \DOMXPath($info->getDocument()->getDocument()))->query('//a[@rel]');
if ($nodes->length === 0) {
return null;
}
$anchor = $nodes->item(0);
if (!$anchor instanceof \DOMElement) {
return null;
}
return $anchor->getAttribute('rel');
}
}
@@ -1,6 +1,6 @@
<?php <?php
namespace App\OAuth; namespace App\OAuth2\Client;
use App\Service\UserService; use App\Service\UserService;
use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\AbstractProvider;
@@ -12,19 +12,19 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
/** /**
* Provides supports for OAuth Authorization Code flows with Decidim instances. * Provides supports for OAuth2 Authorization Code flows with Decidim instances.
* *
* HOW-TO USE * HOW-TO USE
* 1. Add a .png image in `public/oauth/providers` dir to show the provider's logo in the login page * 1. Add a .png image in `public/oauth2/providers` dir to show the provider's logo in the login page
* 2. Add a new provider under `clients` key in `config/packages/knpu_oauth2_client.yaml`: * 2. Add a new provider under `clients` key in `config/packages/knpu_oauth2_client.yaml`:
* ```yaml * ```yaml
* decidim_<instance name>: * decidim_<instance name>:
* type: generic * type: generic
* provider_class: App\OAuth\DecidimProvider * provider_class: App\OAuth2\Client\DecidimProvider
* provider_options: * provider_options:
* url: <instance web address> * url: <instance web address>
* client_id: "%env(<instance oauth client id>)%" * client_id: "%env(<instance's OAuth2 Client id>)%"
* client_secret: "%env(<instance oauth client secret>)%" * client_secret: "%env(<instance's OAuth2 Client secret>)%"
* redirect_route: oauth_providers_callback * redirect_route: oauth_providers_callback
* redirect_params: * redirect_params:
* provider: decidim_<instance name> * provider: decidim_<instance name>
@@ -1,6 +1,6 @@
<?php <?php
namespace App\OAuth; namespace App\OAuth2\Client;
use League\OAuth2\Client\Provider\ResourceOwnerInterface; use League\OAuth2\Client\Provider\ResourceOwnerInterface;
@@ -1,6 +1,6 @@
<?php <?php
namespace App\OAuth; namespace App\OAuth2\Client;
interface TokenOwnerInterface interface TokenOwnerInterface
{ {
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\OAuth2\Server;
use App\Entity\User\User;
use League\Bundle\OAuth2ServerBundle\Converter\UserConverterInterface;
use League\Bundle\OAuth2ServerBundle\Entity\User as OAuth2User;
use League\OAuth2\Server\Entities\UserEntityInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\Security\Core\User\UserInterface;
#[AsDecorator(UserConverterInterface::class)]
final class UserConverter implements UserConverterInterface
{
public function toLeague(UserInterface $user): UserEntityInterface
{
if (!$user instanceof User) {
throw new \LogicException(sprintf(
'Expected %s, got %s.',
User::class,
$user::class,
));
}
$userEntity = new OAuth2User();
$userEntity->setIdentifier((string) $user->getId());
return $userEntity;
}
}
+3
View File
@@ -1,4 +1,7 @@
# Introduction # Introduction
DTF's Goteo v4 API instance by [Platoniq](https://platoniq.net/).
[Co-op Cloud Git](https://git.coopcloud.tech/DemocraticTech.Fund/crowd-fund)
The v4 API is a multi-capable web API based on the [API Platform](https://api-platform.com/) framework, built to support the future generation of the [Goteo](https://goteo.org) crowdfunding platform, aiming to be it's new underlying engine. The v4 API is a multi-capable web API based on the [API Platform](https://api-platform.com/) framework, built to support the future generation of the [Goteo](https://goteo.org) crowdfunding platform, aiming to be it's new underlying engine.
+9
View File
@@ -31,6 +31,9 @@ trait RedocExtensionsTrait
'name' => 'Projects', 'name' => 'Projects',
'tags' => [ 'tags' => [
'Project', 'Project',
'ProjectReview',
'ProjectReviewArea',
'ProjectReviewComment',
'ProjectReward', 'ProjectReward',
'ProjectRewardClaim', 'ProjectRewardClaim',
'ProjectBudgetItem', 'ProjectBudgetItem',
@@ -38,7 +41,13 @@ trait RedocExtensionsTrait
'ProjectSupport', 'ProjectSupport',
'ProjectCollaboration', 'ProjectCollaboration',
'ProjectCollaborationCandidacy', 'ProjectCollaborationCandidacy',
],
],
[
'name' => 'Categorization',
'tags' => [
'Category', 'Category',
'Theme',
], ],
], ],
[ [
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Address;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Address>
*/
class AddressRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Address::class);
}
// /**
// * @return Address[] Returns an array of Address objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Address
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+2
View File
@@ -11,6 +11,8 @@ use Doctrine\Persistence\ManagerRegistry;
*/ */
class CategoryRepository extends ServiceEntityRepository class CategoryRepository extends ServiceEntityRepository
{ {
use FindByIdOrSlugTrait;
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
parent::__construct($registry, Category::class); parent::__construct($registry, Category::class);
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
/**
* @template T of object
*
* @mixin ServiceEntityRepository
*/
trait FindByIdOrSlugTrait
{
/**
* @return T|null
*/
public function findOneByIdOrSlug(mixed $idOrSlug): ?object
{
if (\ctype_digit((string) $idOrSlug)) {
return $this->find($idOrSlug);
}
return $this->findOneBy(['slug' => $idOrSlug]);
}
public function getByIdOrSlugQuery(mixed $idOrSlug): QueryBuilder
{
/** @var QueryBuilder */
$queryBuilder = $this->createQueryBuilder('o');
$queryBuilder->where(\ctype_digit((string) $idOrSlug) ? 'o.id = :idOrSlugField' : 'o.slug = :idOrSlugField');
$queryBuilder->setParameter('idOrSlugField', $idOrSlug);
return $queryBuilder;
}
}
+3 -9
View File
@@ -4,6 +4,7 @@ namespace App\Repository\Project;
use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Accounting;
use App\Entity\Project\Project; use App\Entity\Project\Project;
use App\Repository\FindByIdOrSlugTrait;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@@ -17,6 +18,8 @@ use Doctrine\Persistence\ManagerRegistry;
*/ */
class ProjectRepository extends ServiceEntityRepository class ProjectRepository extends ServiceEntityRepository
{ {
use FindByIdOrSlugTrait;
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
parent::__construct($registry, Project::class); parent::__construct($registry, Project::class);
@@ -31,15 +34,6 @@ class ProjectRepository extends ServiceEntityRepository
->getOneOrNullResult(); ->getOneOrNullResult();
} }
public function findOneByIdOrSlug(mixed $idOrSlug): ?Project
{
if (\is_numeric($idOrSlug)) {
return $this->find($idOrSlug);
}
return $this->findOneBy(['slug' => $idOrSlug]);
}
// /** // /**
// * @return Project[] Returns an array of Project objects // * @return Project[] Returns an array of Project objects
// */ // */
@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Project;
use App\Entity\Project\ReviewArea;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ReviewArea>
*/
class ReviewAreaRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ReviewArea::class);
}
// /**
// * @return ReviewArea[] Returns an array of ReviewArea objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?ReviewArea
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Project;
use App\Entity\Project\ReviewComment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ReviewComment>
*/
class ReviewCommentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ReviewComment::class);
}
// /**
// * @return ReviewComment[] Returns an array of ReviewComment objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?ReviewComment
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
@@ -0,0 +1,43 @@
<?php
namespace App\Repository\Project;
use App\Entity\Project\Review;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Review>
*/
class ReviewRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Review::class);
}
// /**
// * @return Review[] Returns an array of Review objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Review
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Repository;
use App\Entity\Theme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Theme>
*/
class ThemeRepository extends ServiceEntityRepository
{
use FindByIdOrSlugTrait;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Theme::class);
}
// /**
// * @return Theme[] Returns an array of Theme objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('t.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Theme
// {
// return $this->createQueryBuilder('t')
// ->andWhere('t.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Security\Voter;
use App\ApiResource\Project\ReviewAreaApiResource;
use App\Entity\Project\ReviewArea;
use App\Entity\User\User;
use App\Mapping\AutoMapper;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class ReviewAreaVoter extends Voter
{
use UserOwnedVoterTrait;
public const EDIT = 'REVIEWAREA_EDIT';
public const VIEW = 'REVIEWAREA_VIEW';
public function __construct(
private AutoMapper $mapper,
) {}
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof ReviewAreaApiResource;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($user->hasRoles(['ROLE_ADMIN'])) {
return true;
}
/** @var ReviewArea */
$area = $this->mapper->map($subject, ReviewArea::class);
if (!$area || !$area instanceof ReviewArea) {
return false;
}
$project = $area->getReview()->getProject();
switch ($attribute) {
case self::EDIT:
return $area->getReview()->isReviewedBy($user);
case self::VIEW:
return $this->isOwnerOf($project, $user);
}
return false;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Security\Voter;
use App\ApiResource\Project\RewardClaimApiResource;
use App\Entity\User\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
final class RewardClaimVoter extends Voter
{
use UserOwnedVoterTrait;
public const EDIT = 'CLAIM_EDIT';
public const VIEW = 'CLAIM_VIEW';
public const OWNS = 'CLAIM_OWNS';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::VIEW, self::OWNS])
&& $subject instanceof RewardClaimApiResource;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
switch ($attribute) {
case self::VIEW:
return true;
case self::EDIT:
return $this->voteEdit($subject, $token->getUser());
case self::OWNS:
return $this->voteOwns($subject, $token->getUser());
}
return false;
}
private function voteEdit(RewardClaimApiResource $claim, ?UserInterface $user): bool
{
if (!$user instanceof User) {
return false;
}
if ($user->hasRoles(['ROLE_ADMIN'])) {
return true;
}
return $this->isOwnerOf($claim->reward->project, $user);
}
private function voteOwns(RewardClaimApiResource $claim, ?UserInterface $user): bool
{
if (!$user instanceof User) {
return false;
}
if ($user->hasRoles(['ROLE_ADMIN'])) {
return true;
}
return $this->isOwnerOf($claim->reward, $user);
}
}
+1 -1
View File
@@ -32,6 +32,6 @@ class UserOwnedVoter extends Voter
*/ */
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{ {
return $this->isOwnerOf($subject, $token->getUser()); return $subject !== null && $this->isOwnerOf($subject, $token->getUser());
} }
} }
@@ -3,6 +3,7 @@
namespace App\Security\Voter; namespace App\Security\Voter;
use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\Accounting\AccountingApiResource;
use App\ApiResource\Project\ProjectApiResource;
use App\ApiResource\User\UserApiResource; use App\ApiResource\User\UserApiResource;
use App\Entity\User\User; use App\Entity\User\User;
use App\Entity\UserOwnedInterface; use App\Entity\UserOwnedInterface;
@@ -31,6 +32,10 @@ trait UserOwnedVoterTrait
return $subject->id === $user->getAccounting()->getId(); return $subject->id === $user->getAccounting()->getId();
} }
if ($subject instanceof ProjectApiResource) {
return $subject->owner->id === $user->getId();
}
if ($subject instanceof UserOwnedInterface) { if ($subject instanceof UserOwnedInterface) {
return $subject->isOwnedBy($user); return $subject->isOwnedBy($user);
} }
+36
View File
@@ -4,6 +4,7 @@ namespace App\Service\Project;
use App\Entity\Project\Project; use App\Entity\Project\Project;
use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectStatus;
use App\Entity\Project\ReviewType;
use App\Entity\User\User; use App\Entity\User\User;
use App\Security\Voter\UserOwnedVoterTrait; use App\Security\Voter\UserOwnedVoterTrait;
@@ -19,6 +20,11 @@ class ProjectService
ProjectStatus::ToFunding->value => [ProjectStatus::InFunding], ProjectStatus::ToFunding->value => [ProjectStatus::InFunding],
]; ];
public function __construct(
private ReviewService $reviewService,
private CalendarService $calendarService,
) {}
/** /**
* Check if an User can move a Project to some status. * Check if an User can move a Project to some status.
* *
@@ -47,4 +53,34 @@ class ProjectService
return false; return false;
} }
/**
* Moves a Project to a given ProjectStatus.
*
* @param ProjectStatus $to The new status to where the Project will move
*
* @return Project The resulting Project
*/
public function transition(Project $project, ProjectStatus $to): Project
{
$from = $project->getStatus();
if ($from === $to) {
return $project;
}
$project->setStatus($to);
return match ([$from, $to]) {
[ProjectStatus::InDraft, ProjectStatus::ToCampaignReview] => $project->addReview(
$this->reviewService->makeReview(ReviewType::Campaign)
),
[ProjectStatus::ToCampaign, ProjectStatus::InCampaign] => $project->setCalendar(
$this->calendarService->makeCalendar($project->getDeadline())
),
default => $project,
};
}
} }
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Service\Project;
use App\Entity\Project\Review;
use App\Entity\Project\ReviewArea;
use App\Entity\Project\ReviewType;
class ReviewService
{
public function makeReview(ReviewType $type): Review
{
$review = new Review();
$review->setType($type);
/** @var ReviewArea[] */
$areas = match ($type) {
ReviewType::Campaign => $this->getCampaignReviewAreas(),
ReviewType::Financial => $this->getFinancialReviewAreas(),
};
foreach ($areas as $area) {
$review->addArea($area);
}
return $review;
}
/**
* @return ReviewArea[]
*/
private function getCampaignReviewAreas(): array
{
$config = new ReviewArea();
$config->setTitle('configuration');
$info = new ReviewArea();
$info->setTitle('info');
$rewards = new ReviewArea();
$rewards->setTitle('rewards');
$collabs = new ReviewArea();
$collabs->setTitle('collaborations');
$budget = new ReviewArea();
$budget->setTitle('budget');
$about = new ReviewArea();
$about->setTitle('about');
return [
$config,
$info,
$rewards,
$collabs,
$budget,
$about,
];
}
/**
* @return ReviewArea[]
*
* @todo Add financial review areas
*/
private function getFinancialReviewAreas(): array
{
return [];
}
}
@@ -4,13 +4,13 @@ namespace App\Service\Scout;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
class FileUriException extends \Exception class NonCrawlableUriException extends \Exception
{ {
private UriInterface $uri; private UriInterface $uri;
public function __construct( public function __construct(
UriInterface $uri, UriInterface $uri,
string $message = 'Cannot scout URLs to files (%s)', string $message = 'Cannot scout %s: it is not a Web page',
int $code = 0, int $code = 0,
?\Throwable $previous = null, ?\Throwable $previous = null,
) { ) {
@@ -19,9 +19,6 @@ class FileUriException extends \Exception
$this->uri = $uri; $this->uri = $uri;
} }
/**
* The URI to the file.
*/
public function getUri(): UriInterface public function getUri(): UriInterface
{ {
return $this->uri; return $this->uri;
+24 -4
View File
@@ -5,6 +5,7 @@ namespace App\Service\Scout;
use Embed\Embed; use Embed\Embed;
use Embed\Http\Crawler; use Embed\Http\Crawler;
use Psr\Http\Client\ClientInterface; use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Url; use Symfony\Component\Validator\Constraints\Url;
use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validation;
@@ -39,8 +40,8 @@ class ScoutService
/** /**
* @param string $url A URL to an external resource * @param string $url A URL to an external resource
* *
* @throws InvalidUriException When the given $url string could not be validated as an actual URL * @throws InvalidUriException When the given $url string could not be validated as an actual URL
* @throws FileUriException If the given $url string points to a file * @throws NonCrawlableUriException If the given $url string points to a file
*/ */
public function get(string $url): ScoutResult public function get(string $url): ScoutResult
{ {
@@ -54,9 +55,12 @@ class ScoutService
} }
$uri = $this->embed->getCrawler()->createUri($url); $uri = $this->embed->getCrawler()->createUri($url);
$response = $this->httpClient->sendRequest(
new \GuzzleHttp\Psr7\Request('HEAD', $url)
);
if (\pathinfo($uri->getPath(), \PATHINFO_EXTENSION)) { if (!$this->isCrawlable($response)) {
throw new FileUriException($uri); throw new NonCrawlableUriException($uri);
} }
$info = $this->embed->get($url); $info = $this->embed->get($url);
@@ -82,4 +86,20 @@ class ScoutService
return $result; return $result;
} }
private function isCrawlable(ResponseInterface $response): bool
{
$contentType = $response->getHeaderLine('Content-Type');
if ($contentType === '') {
return true;
}
$contentType = \strtolower(\trim(\explode(';', $contentType)[0]));
return \in_array($contentType, [
'text/html',
'application/xhtml+xml',
], true);
}
} }
+4 -10
View File
@@ -20,20 +20,14 @@ class CategoryStateProvider implements ProviderInterface
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{ {
$idOrSlug = $uriVariables['idOrSlug']; $queryBuilder = $this->categoryRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
$queryBuilder = $this->categoryRepository->createQueryBuilder('c');
$queryBuilder->where(\is_numeric($idOrSlug) ? 'c.id = :value' : 'c.slug = :value');
$queryBuilder->setParameter('value', $idOrSlug);
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context)); $query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
$category = $query->getOneOrNullResult();
$project = $query->getOneOrNullResult(); if ($category === null) {
if ($project === null) {
return null; return null;
} }
return $this->autoMapper->map($project, CategoryApiResource::class); return $this->autoMapper->map($category, CategoryApiResource::class);
} }
} }
+16 -19
View File
@@ -10,8 +10,8 @@ use App\ApiResource\Project\ProjectApiResource;
use App\Dto\ProjectCreationDto; use App\Dto\ProjectCreationDto;
use App\Dto\ProjectUpdationDto; use App\Dto\ProjectUpdationDto;
use App\Entity\Project\Project; use App\Entity\Project\Project;
use App\Entity\Project\ProjectCalendar;
use App\Mapping\AutoMapper; use App\Mapping\AutoMapper;
use App\Service\Project\CalendarService;
use App\Service\Project\ProjectService; use App\Service\Project\ProjectService;
use App\State\EntityStateProcessor; use App\State\EntityStateProcessor;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
@@ -24,6 +24,7 @@ class ProjectStateProcessor implements ProcessorInterface
private AutoMapper $autoMapper, private AutoMapper $autoMapper,
private Security $security, private Security $security,
private ProjectService $projectService, private ProjectService $projectService,
private CalendarService $calendarService,
) {} ) {}
/** /**
@@ -33,13 +34,11 @@ class ProjectStateProcessor implements ProcessorInterface
*/ */
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{ {
if ($data instanceof ProjectCreationDto) { $project = match ($data::class) {
$project = $this->getProjectFromCreation($data); ProjectCreationDto::class => $this->getProjectFromCreation($data),
} elseif ($data instanceof ProjectUpdationDto) { ProjectUpdationDto::class => $this->getProjectFromUpdate($data, $context),
$project = $this->getProjectFromUpdate($data, $context); default => throw new \InvalidArgumentException('Unsupported input for Project resource'),
} else { };
throw new \InvalidArgumentException('Unsupported input for Project resource');
}
$project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context); $project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context);
@@ -60,7 +59,6 @@ class ProjectStateProcessor implements ProcessorInterface
$project = $this->autoMapper->map($data, Project::class); $project = $this->autoMapper->map($data, Project::class);
$owner = $this->security->getUser(); $owner = $this->security->getUser();
if (!$owner) { if (!$owner) {
throw new AuthenticationException(); throw new AuthenticationException();
} }
@@ -68,28 +66,25 @@ class ProjectStateProcessor implements ProcessorInterface
$project->setOwner($owner); $project->setOwner($owner);
if (!isset($data->calendar->release)) { if (!isset($data->calendar->release)) {
$data->calendar->release = new \DateTimeImmutable('+28 days'); $project->setCalendar($this->calendarService->makeCalendar(
$project->getDeadline(),
new \DateTimeImmutable('+28 days')
));
} }
$calendar = new ProjectCalendar();
$calendar->release = $data->calendar->release;
$project->setCalendar($calendar);
return $project; return $project;
} }
private function getProjectFromUpdate(ProjectUpdationDto $data, array $context): Project private function getProjectFromUpdate(ProjectUpdationDto $data, array $context): Project
{ {
/** @var Project */
$project = $this->autoMapper->map($context['previous_data'], Project::class);
$actor = $this->security->getUser(); $actor = $this->security->getUser();
if (!$actor) { if (!$actor) {
throw new AuthenticationException(); throw new AuthenticationException();
} }
/** @var Project */
$project = $this->autoMapper->map($context['previous_data'], Project::class);
if (isset($data->status) && $data->status !== $project->getStatus()) { if (isset($data->status) && $data->status !== $project->getStatus()) {
if (!$this->projectService->canTransition($actor, $project, $data->status)) { if (!$this->projectService->canTransition($actor, $project, $data->status)) {
throw new AccessDeniedException(\sprintf( throw new AccessDeniedException(\sprintf(
@@ -98,6 +93,8 @@ class ProjectStateProcessor implements ProcessorInterface
$data->status->value $data->status->value
)); ));
} }
$project = $this->projectService->transition($project, $data->status);
} }
return $this->autoMapper->map($data, $project); return $this->autoMapper->map($data, $project);
+1 -7
View File
@@ -20,14 +20,8 @@ class ProjectStateProvider implements ProviderInterface
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{ {
$idOrSlug = $uriVariables['idOrSlug']; $queryBuilder = $this->projectRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
$queryBuilder = $this->projectRepository->createQueryBuilder('p');
$queryBuilder->where(\is_numeric($idOrSlug) ? 'p.id = :value' : 'p.slug = :value');
$queryBuilder->setParameter('value', $idOrSlug);
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context)); $query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
$project = $query->getOneOrNullResult(); $project = $query->getOneOrNullResult();
if ($project === null) { if ($project === null) {
+42 -19
View File
@@ -7,44 +7,36 @@ use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\Project\RewardClaimApiResource; use App\ApiResource\Project\RewardClaimApiResource;
use App\Dto\RewardClaimCreationDto; use App\Dto\RewardClaimCreationDto;
use App\Dto\RewardClaimUpdationDto;
use App\Entity\Project\RewardClaim; use App\Entity\Project\RewardClaim;
use App\Mapping\AutoMapper; use App\Mapping\AutoMapper;
use App\Repository\Project\RewardClaimRepository;
use App\Security\Voter\AccountingVoter;
use App\State\EntityStateProcessor; use App\State\EntityStateProcessor;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AuthenticationException;
class RewardClaimStateProcessor implements ProcessorInterface class RewardClaimStateProcessor implements ProcessorInterface
{ {
public function __construct( public function __construct(
private EntityStateProcessor $entityStateProcessor,
private AutoMapper $autoMapper, private AutoMapper $autoMapper,
private RewardClaimRepository $rewardClaimRepository,
private Security $security, private Security $security,
private EntityStateProcessor $entityStateProcessor,
) {} ) {}
/** /**
* @param RewardClaimApiResource|RewardClaimCreationDto $data * @param RewardClaimCreationDto|RewardClaimUpdationDto $data
* *
* @return RewardClaimApiResource|null * @return RewardClaimApiResource|null
*/ */
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{ {
/** @var RewardClaim */ $claim = match ($data::class) {
$claim = $this->autoMapper->map($data, RewardClaim::class); RewardClaimUpdationDto::class => $this->getClaimFromUpdate($context),
RewardClaimCreationDto::class => $this->getClaimFromCreate($data),
if ($data instanceof RewardClaimCreationDto) { };
$owner = $this->security->getUser();
if (!$owner) {
throw new AuthenticationException();
}
$claim->setOwner($owner);
$reward = $claim->getReward();
$reward->addClaim($claim);
$claim->setReward($reward);
}
$claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context); $claim = $this->entityStateProcessor->process($claim, $operation, $uriVariables, $context);
@@ -52,6 +44,37 @@ class RewardClaimStateProcessor implements ProcessorInterface
return; return;
} }
if ($claim === null) {
return null;
}
return $this->autoMapper->map($claim, $data); return $this->autoMapper->map($claim, $data);
} }
private function getClaimFromUpdate(array $context): RewardClaim
{
$claim = $this->rewardClaimRepository->find($context['previous_data']->id);
if (!$claim) {
throw new NotFoundHttpException();
}
return $claim;
}
private function getClaimFromCreate(RewardClaimCreationDto $data): RewardClaim
{
/** @var RewardClaim */
$claim = $this->autoMapper->map($data, RewardClaim::class);
$origin = $claim->getCharge()->getCheckout()->getOrigin();
if (!$this->security->isGranted(AccountingVoter::EDIT, $origin)) {
throw new AuthenticationException();
}
$owner = $origin->getUser();
$claim->setOwner($owner);
return $claim;
}
} }
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\ThemeApiResource;
use App\Doctrine\LocalizedExtensionTrait;
use App\Mapping\AutoMapper;
use App\Repository\ThemeRepository;
class ThemeStateProvider implements ProviderInterface
{
use LocalizedExtensionTrait;
public function __construct(
private ThemeRepository $themeRepository,
private AutoMapper $autoMapper,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$queryBuilder = $this->themeRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
$theme = $query->getOneOrNullResult();
if ($theme === null) {
return null;
}
return $this->autoMapper->map($theme, ThemeApiResource::class);
}
}
+23 -9
View File
@@ -2,44 +2,58 @@
namespace App\State\User; namespace App\State\User;
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Metadata as API; use ApiPlatform\Metadata as API;
use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\User\UserApiResource;
use App\Dto\User\UserUpdationDto;
use App\Entity\User\Organization; use App\Entity\User\Organization;
use App\Entity\User\User; use App\Entity\User\User;
use App\Entity\User\UserType; use App\Entity\User\UserType;
use App\Mapping\AutoMapper; use App\Mapping\AutoMapper;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use App\Repository\User\UserRepository;
use App\State\EntityStateProcessor;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserStateProcessor implements ProcessorInterface class UserStateProcessor implements ProcessorInterface
{ {
public function __construct( public function __construct(
private AutoMapper $autoMapper, private AutoMapper $autoMapper,
#[Autowire(service: PersistProcessor::class)] private UserRepository $userRepository,
private ProcessorInterface $persistProcessor, private UserPasswordHasherInterface $userPasswordHasher,
private EntityStateProcessor $entityProcessor,
) {} ) {}
/** /**
* @param UserApiResource $data * @param UserUpdationDto $data
* *
* @return UserApiResource * @return UserUpdationDto
*/ */
public function process(mixed $data, API\Operation $operation, array $uriVariables = [], array $context = []) public function process(mixed $data, API\Operation $operation, array $uriVariables = [], array $context = [])
{ {
$user = $this->userRepository->find($context['previous_data']->id);
if (!$user) {
throw new NotFoundHttpException();
}
/** @var User */ /** @var User */
$user = $this->autoMapper->map($data, User::class); $user = $this->autoMapper->map($data, $user);
if (isset($data->password)) {
$user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password));
}
if ($user->isType(UserType::Organization) && $user->getOrganization() === null) { if ($user->isType(UserType::Organization) && $user->getOrganization() === null) {
$user->setOrganization(Organization::for($user)); $user->setOrganization(Organization::for($user));
} }
$user = $this->persistProcessor->process($user, $operation, $uriVariables, $context); $user = $this->entityProcessor->process($user, $operation, $uriVariables, $context);
if ($operation instanceof DeleteOperationInterface) { if ($operation instanceof DeleteOperationInterface) {
return; return;
} }
return $this->autoMapper->map($user, $data); return $this->autoMapper->map($user, UserApiResource::class);
} }
} }
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class NotExisting extends Constraint
{
public string $message = 'The {{ property }} \'{{ value }}\' is already in use for {{ entity }} records.';
public function __construct(
/**
* FQCN of the entity to check uniqueness for.
*/
public string $entity,
/**
* Name of the property in the `entity` for which to check uniqueness.
*/
public string $property,
?array $groups = null,
mixed $payload = null,
) {
parent::__construct([], $groups, $payload);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Validator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
final class NotExistingValidator extends ConstraintValidator
{
public function __construct(
private EntityManagerInterface $entityManager,
) {}
/**
* @param NotExisting $constraint
*/
public function validate(mixed $value, Constraint $constraint): void
{
$entityClass = $constraint->entity;
$repository = $this->entityManager->getRepository($entityClass);
if (
$value === null
|| $value === ''
|| $repository->findOneBy([$constraint->property => $value]) === null
) {
return;
}
$this->context->buildViolation($constraint->message)
->setParameter('{{ property }}', $constraint->property)
->setParameter('{{ value }}', $value)
->setParameter('{{ entity }}', $this->getClassShortName($entityClass))
->addViolation()
;
}
private function getClassShortName(string $class): string
{
$reflect = new \ReflectionClass($class);
return $reflect->getShortName();
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class UserOwned extends Constraint
{
public string $message = 'Tried to reference "{{ value }}" while not authenticated as the very User.';
public function __construct(
public string $mode = 'strict',
?array $groups = null,
mixed $payload = null,
) {
parent::__construct([], $groups, $payload);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Validator;
use ApiPlatform\Metadata\IriConverterInterface;
use App\ApiResource\User\UserApiResource;
use App\Mapping\AutoMapper;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
final class UserOwnedValidator extends ConstraintValidator
{
public function __construct(
private Security $security,
private AutoMapper $mapper,
private IriConverterInterface $iriConverter,
) {}
/**
* @param UserOwned $constraint
*/
public function validate(mixed $value, Constraint $constraint): void
{
if ($value === null || $value === '') {
return;
}
$value = $this->iriConverter->getIriFromResource($value);
$user = $this->security->getUser();
if ($value && $user) {
/** @var UserApiResource */
$resource = $this->mapper->map($user, UserApiResource::class);
$resourceIri = $this->iriConverter->getIriFromResource($resource);
if ($value === $resourceIri) {
return;
}
}
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation()
;
}
}
+1 -1
View File
@@ -47,7 +47,7 @@
<li> <li>
<div class="bg-white border border-grey rounded-2xl p-4 shadow-sm"> <div class="bg-white border border-grey rounded-2xl p-4 shadow-sm">
<a href="{{ path('oauth_providers_start', { provider: provider }) }}"> <a href="{{ path('oauth_providers_start', { provider: provider }) }}">
<img class="max-h-12 h-full" src="/oauth/providers/{{ provider }}.png" alt={{ provider }}> <img class="max-h-12 h-full" src="/oauth2/providers/{{ provider }}.png" alt={{ provider }}>
</a> </a>
</div> </div>
</li> </li>
+10 -1
View File
@@ -8,6 +8,7 @@ use App\Entity\Project\ProjectDeadline;
use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectStatus;
use App\Entity\User\User; use App\Entity\User\User;
use App\Factory\Project\ProjectFactory; use App\Factory\Project\ProjectFactory;
use App\Service\Project\ProjectService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Test\ResetDatabase;
@@ -18,12 +19,14 @@ class ProjectCalendarTest extends ApiTestCase
use Factories; use Factories;
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
private ProjectService $projectService;
private User $owner; private User $owner;
public function setUp(): void public function setUp(): void
{ {
self::bootKernel(); self::bootKernel();
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class); $this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
$this->projectService = static::getContainer()->get(ProjectService::class);
$this->owner = $this->createTestUser(); $this->owner = $this->createTestUser();
$this->entityManager->persist($this->owner); $this->entityManager->persist($this->owner);
@@ -44,6 +47,7 @@ class ProjectCalendarTest extends ApiTestCase
{ {
return ProjectFactory::createOne([ return ProjectFactory::createOne([
'owner' => $this->owner, 'owner' => $this->owner,
'status' => ProjectStatus::ToCampaign,
'deadline' => $deadline, 'deadline' => $deadline,
])->_real(); ])->_real();
} }
@@ -56,7 +60,9 @@ class ProjectCalendarTest extends ApiTestCase
$this->entityManager->persist($project); $this->entityManager->persist($project);
$this->entityManager->flush(); $this->entityManager->flush();
$project->setStatus(ProjectStatus::InCampaign); $project = $this->projectService->transition($project, ProjectStatus::InCampaign);
$this->entityManager->persist($project);
$this->entityManager->flush(); $this->entityManager->flush();
return $project; return $project;
@@ -65,8 +71,11 @@ class ProjectCalendarTest extends ApiTestCase
public function testReleaseDateUpdatesOnCampaignStart(): void public function testReleaseDateUpdatesOnCampaignStart(): void
{ {
$project = $this->createProjectAndSetToInCampaign(); $project = $this->createProjectAndSetToInCampaign();
$today = new \DateTime();
$today->setTime(0, 0, 0, 0);
$this->assertNotNull($project->getCalendar()->release); $this->assertNotNull($project->getCalendar()->release);
$this->assertGreaterThan($today, $project->getCalendar()->release);
} }
public function testMinimumDeadlineIsSetCorrectly(): void public function testMinimumDeadlineIsSetCorrectly(): void
@@ -0,0 +1,81 @@
<?php
namespace App\Tests\Entity;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Project\Project;
use App\Entity\Project\ProjectDeadline;
use App\Entity\Project\ProjectStatus;
use App\Entity\Project\Review;
use App\Entity\Project\ReviewType;
use App\Entity\User\User;
use App\Factory\Project\ProjectFactory;
use App\Service\Project\ProjectService;
use Doctrine\ORM\EntityManagerInterface;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class ProjectCampaignReviewTest extends ApiTestCase
{
use ResetDatabase;
use Factories;
private EntityManagerInterface $entityManager;
private ProjectService $projectService;
private User $owner;
public function setUp(): void
{
self::bootKernel();
$this->entityManager = static::getContainer()->get(EntityManagerInterface::class);
$this->projectService = static::getContainer()->get(ProjectService::class);
$this->owner = $this->createTestUser();
$this->entityManager->persist($this->owner);
$this->entityManager->flush();
}
private function createTestUser(string $handle = 'test_user', string $email = 'testuser@example.com'): User
{
$user = new User();
$user->setHandle($handle);
$user->setEmail($email);
$user->setPassword('projectapitestpassword');
return $user;
}
private function createTestProject(ProjectDeadline $deadline = ProjectDeadline::Minimum): Project
{
return ProjectFactory::createOne([
'owner' => $this->owner,
'deadline' => $deadline,
'status' => ProjectStatus::InDraft,
])->_real();
}
private function createProjectAndSetToInCampaignReview(): Project
{
$project = $this->createTestProject();
$this->entityManager->persist($project);
$this->entityManager->flush();
$project = $this->projectService->transition($project, ProjectStatus::ToCampaignReview);
$this->entityManager->persist($project);
$this->entityManager->flush();
return $project;
}
public function testCreatesReviewOnStatusChange(): void
{
$project = $this->createProjectAndSetToInCampaignReview();
$reviews = $this->entityManager->getRepository(Review::class)->findBy(['project' => $project->getId()]);
$this->assertNotEmpty($reviews);
$this->assertEquals(1, \count($reviews));
$this->assertEquals(ReviewType::Campaign->value, $reviews[0]->getType()->value);
}
}
+6 -6
View File
@@ -2,8 +2,8 @@
namespace App\Tests\Service\Scout; namespace App\Tests\Service\Scout;
use App\Service\Scout\FileUriException;
use App\Service\Scout\InvalidUriException; use App\Service\Scout\InvalidUriException;
use App\Service\Scout\NonCrawlableUriException;
use App\Service\Scout\ScoutService; use App\Service\Scout\ScoutService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -38,17 +38,17 @@ class ScoutServiceTest extends KernelTestCase
} }
/** /**
* @dataProvider providePathsToBinaryFiles * @dataProvider provideNonCrawlablePaths
*/ */
public function testThrowsExceptionOnPathsToBinaryFiles(string $url) public function testThrowsExceptionOnNonCrawlablePaths(string $url)
{ {
$this->expectException(FileUriException::class); $this->expectException(NonCrawlableUriException::class);
$this->expectExceptionMessageMatches(\sprintf('/\(%s\)$/', \preg_quote($url, '/'))); $this->expectExceptionMessageMatches(\sprintf('/%s/', \preg_quote($url, '/')));
$this->scout->get($url); $this->scout->get($url);
} }
public function providePathsToBinaryFiles(): array public function provideNonCrawlablePaths(): array
{ {
return [ return [
['https://sialmaraz.es/wp-content/uploads/2025/01/MANIFESTACION-ALMARAZ-18_1.mp4'], ['https://sialmaraz.es/wp-content/uploads/2025/01/MANIFESTACION-ALMARAZ-18_1.mp4'],