Merge branch 'master' into onTheFly

This commit is contained in:
2021-09-30 12:08:24 +02:00
33 changed files with 915 additions and 510 deletions

View File

@@ -0,0 +1,38 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class AddressApiController extends ApiController
{
/**
* Duplicate an existing address
*
* @Route("/api/1.0/main/address/{id}/duplicate.json", name="chill_api_main_address_duplicate",
* methods={"POST"})
*
* @param Address $address
*/
public function duplicate(Address $address): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_USER');
$new = Address::createFromAddress($address);
$em = $this->getDoctrine()->getManager();
$em->persist($new);
$em->flush();
return $this->json($new, Response::HTTP_OK, [], [
AbstractNormalizer::GROUPS => ['read']
]);
}
}

View File

@@ -19,6 +19,7 @@
namespace Chill\MainBundle\DependencyInjection;
use Chill\MainBundle\Controller\AddressApiController;
use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
@@ -319,6 +320,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'apis' => [
[
'class' => \Chill\MainBundle\Entity\Address::class,
'controller' => AddressApiController::class,
'name' => 'address',
'base_path' => '/api/1.0/main/address',
'base_role' => 'ROLE_USER',

View File

@@ -376,10 +376,22 @@ class Address
public static function createFromAddress(Address $original) : Address
{
return (new Address())
->setBuildingName($original->getBuildingName())
->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms())
->setDistribution($original->getDistribution())
->setExtra($original->getExtra())
->setFlat($original->getFlat())
->setFloor($original->getFloor())
->setIsNoAddress($original->getIsNoAddress())
->setLinkedToThirdParty($original->getLinkedToThirdParty())
->setPoint($original->getPoint())
->setPostcode($original->getPostcode())
->setStreetAddress1($original->getStreetAddress1())
->setStreetAddress2($original->getStreetAddress2())
->setSteps($original->getSteps())
->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber())
->setValidFrom($original->getValidFrom())
->setValidTo($original->getValidTo())
;
}
@@ -506,7 +518,7 @@ class Address
return $this->validTo;
}
public function setValidTo(\DateTimeInterface $validTo): self
public function setValidTo(?\DateTimeInterface $validTo = null): self
{
$this->validTo = $validTo;

View File

@@ -7,6 +7,7 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
final class AddressRepository implements ObjectRepository
@@ -47,4 +48,9 @@ final class AddressRepository implements ObjectRepository
public function getClassName() {
return Address::class;
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
}

View File

@@ -1,4 +1,4 @@
/*
/**
* Endpoint chill_api_single_country__index
* method GET, get Country Object
* @returns {Promise} a promise containing all Country object
@@ -14,7 +14,7 @@ const fetchCountries = () => {
});
};
/*
/**
* Endpoint chill_api_single_postal_code__index
* method GET, get Country Object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
@@ -29,7 +29,7 @@ const fetchCities = (country) => {
});
};
/*
/**
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code
@@ -44,7 +44,7 @@ const fetchReferenceAddresses = (postalCode) => {
});
};
/*
/**
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code
@@ -60,7 +60,7 @@ const fetchAddresses = () => {
});
};
/*
/**
* Endpoint chill_api_single_address__entity__create
* method POST, post Address Object
* @returns {Promise}
@@ -81,8 +81,28 @@ const postAddress = (address) => {
});
};
/**
*
* @param address
* @returns {Promise<Response>}
*/
const duplicateAddress = (address) => {
const url = `/api/1.0/main/address/${address.address_id}/duplicate.json`;
return fetch(url, {
'method': 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) {
return response.json();
}
throw Error('Error with request resource response');
});
};
/*
/**
* Endpoint chill_api_single_address__entity__create
* method PATCH, patch Address Instance
*
@@ -142,6 +162,7 @@ const getAddress = (id) => {
};
export {
duplicateAddress,
fetchCountries,
fetchCities,
fetchReferenceAddresses,

View File

@@ -34,6 +34,7 @@
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
@pick-address="this.pickAddress"
ref="suggestAddress">
</suggest-pane>
</template>
@@ -55,6 +56,7 @@
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:insideModal="false"
@pick-address="this.pickAddress"
ref="suggestAddress">
<template v-slot:before v-if="!bypassFirstStep">
@@ -217,7 +219,16 @@
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import { getAddress, fetchCountries, fetchCities, fetchReferenceAddresses, patchAddress, postAddress, postPostalCode } from '../api';
import {
duplicateAddress,
fetchCountries,
fetchCities,
fetchReferenceAddresses,
getAddress,
patchAddress,
postAddress,
postPostalCode,
} from '../api';
import { postAddressToPerson, postAddressToHousehold } from "ChillPersonAssets/vuejs/_api/AddAddress.js";
import ShowPane from './ShowPane.vue';
import SuggestPane from './SuggestPane.vue';
@@ -234,6 +245,9 @@ export default {
EditPane,
DatePane
},
emits: {
pickAddress: null
},
data() {
return {
flag: {
@@ -311,8 +325,10 @@ export default {
return (this.validFrom || this.validTo) ? true : false;
},
hasSuggestions() {
// TODO
//return addressSuggestions.length > 0
console.log(this.context.suggestions);
if (typeof(this.context.suggestions) !== 'undefined') {
return this.context.suggestions.length > 0;
}
return false;
},
displaySuggestions() {
@@ -647,9 +663,12 @@ export default {
this.flag.loading = false;
this.flag.success = true;
resolve({
address,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
addressId: this.entity.address.address_id,
}
);
}))
@@ -695,6 +714,9 @@ export default {
this.flag.loading = false;
this.flag.success = true;
return resolve({
address,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
@@ -705,6 +727,29 @@ export default {
this.errorMsg.push(error);
this.flag.loading = false;
});
},
/**
*
* @param address the address selected
*/
pickAddress(address) {
console.log('pickAddress', address);
duplicateAddress(address).then(newAddress => {
this.entity.address = newAddress;
this.flag.loading = false;
this.flag.success = true;
let payload = {
address: newAddress,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
};
this.addressChangedCallback(payload);
this.closeSuggestPane();
});
}
}
}

View File

@@ -10,13 +10,14 @@
<h4 class="h3">{{ $t('address_suggestions') }}</h4>
<div class="flex-table AddressSuggestionList">
<div class="item-bloc">
<div v-for="a in context.suggestions" class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">
<!-- QUESTION normal que ça vienne avant l'adresse ? pourquoi pas après avoir affiché le address-render-box ? -->
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose">
<button class="btn btn-sm btn-choose" @click="this.pickAddress(a)">
{{ $t('use_this_address') }}
</button>
</li>
@@ -25,9 +26,7 @@
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<!--
<address-render-box></address-render-box>
-->
<address-render-box :address="a"></address-render-box>
</li>
</ul>
</div>
@@ -68,9 +67,14 @@ export default {
'flag',
'entity',
'errorMsg',
'insideModal'
'insideModal',
],
computed: {},
methods: {}
methods: {
pickAddress(address) {
console.log('pickAddress in suggest pane', address);
this.$emit('pickAddress', address);
},
}
}
</script>

View File

@@ -10,7 +10,7 @@
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<tr data-username="{{ entity.username|e('html_attr') }}">
<td>
{% if entity.isEnabled %}
<i class="fa fa-check chill-green"></i>

View File

@@ -17,32 +17,31 @@
*/
namespace Chill\MainBundle\Test;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Prepare a client authenticated with a user
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* Prepare a client authenticated with a user
*/
trait PrepareClientTrait
{
/**
* Create a new client with authentication information.
*
*
* @param string $username the username (default 'center a_social')
* @param string $password the password (default 'password')
* @return \Symfony\Component\BrowserKit\Client
* @throws \LogicException
*/
public function getClientAuthenticated(
$username = 'center a_social',
$username = 'center a_social',
$password = 'password'
) {
): KernelBrowser {
if (!$this instanceof WebTestCase) {
throw new \LogicException(sprintf("The current class does not "
. "implements %s", WebTestCase::class));
}
return static::createClient(array(), array(
'PHP_AUTH_USER' => $username,
'PHP_AUTH_PW' => $password,

View File

@@ -0,0 +1,49 @@
<?php
namespace Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Repository\AddressRepository;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
private KernelBrowser $client;
use PrepareClientTrait;
public function setUp()
{
self::bootKernel();
$this->client = $this->getClientAuthenticated();
}
/**
* @dataProvider generateAddressIds
* @param int $addressId
*/
public function testDuplicate(int $addressId)
{
$this->client->request('POST', "/api/1.0/main/address/$addressId/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
public function generateAddressIds()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$qb = $em->createQueryBuilder();
$addresses = $qb->select('a')->from(Address::class, 'a')
->setMaxResults(2)
->getQuery()
->getResult();
foreach ($addresses as $a) {
yield [ $a->getId() ];
}
}
}

View File

@@ -2,12 +2,17 @@
namespace Chill\MainBundle\Tests\Controller;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserControllerTest extends WebTestCase
{
private $client;
private array $toDelete = [];
public function setUp()
{
self::bootKernel();
@@ -22,18 +27,14 @@ class UserControllerTest extends WebTestCase
public function testList()
{
// get the list
$crawler = $this->client->request('GET', '/fr/admin/user/');
$crawler = $this->client->request('GET', '/fr/admin/main/user');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(),
"Unexpected HTTP status code for GET /admin/user/");
$link = $crawler->selectLink('Ajouter un nouvel utilisateur')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $link);
$this->assertRegExp('|/fr/admin/user/new$|', $link->getUri());
"Unexpected HTTP status code for GET /admin/main/user");
}
public function testNew()
{
$crawler = $this->client->request('GET', '/fr/admin/user/new');
$crawler = $this->client->request('GET', '/fr/admin/main/user/new');
$username = 'Test_user'. uniqid();
$password = 'Password1234!';
@@ -54,22 +55,15 @@ class UserControllerTest extends WebTestCase
$this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(),
'Missing element td:contains("Test user")');
$update = $crawler->selectLink('Modifier')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $update);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit$|', $update->getUri());
//test the auth of the new client
$this->isPasswordValid($username, $password);
return $update;
}
protected function isPasswordValid($username, $password)
{
/* @var $passwordEncoder \Symfony\Component\Security\Core\Encoder\UserPasswordEncoder */
$passwordEncoder = self::$kernel->getContainer()
->get('security.password_encoder');
$passwordEncoder = self::$container
->get(UserPasswordEncoderInterface::class);
$user = self::$kernel->getContainer()
->get('doctrine.orm.entity_manager')
@@ -81,46 +75,33 @@ class UserControllerTest extends WebTestCase
/**
*
* @param \Symfony\Component\DomCrawler\Link $update
* @depends testNew
* @dataProvider dataGenerateUserId
*/
public function testUpdate(\Symfony\Component\DomCrawler\Link $update)
public function testUpdate(int $userId, string $username)
{
$crawler = $this->client->click($update);
$crawler = $this->client->request('GET', "/fr/admin/main/user/$userId/edit");
$username = 'Foo bar '.uniqid();
$form = $crawler->selectButton('Mettre à jour')->form(array(
$form = $crawler->selectButton('Enregistrer & fermer')->form(array(
'chill_mainbundle_user[username]' => $username,
));
$this->client->submit($form);
$crawler = $this->client->followRedirect();
// Check the element contains an attribute with value equals "Foo"
$this->assertGreaterThan(0, $crawler->filter('[value="'.$username.'"]')->count(),
'Missing element [value="Foo bar"]');
$updatePassword = $crawler->selectLink('Modifier le mot de passe')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $updatePassword);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit_password$|',
$updatePassword->getUri());
return array('link' => $updatePassword, 'username' => $username);
$this->assertGreaterThan(0, $crawler->filter('[data-username="'.$username.'"]')->count(),
'Missing element [data-username="Foo bar"]');
}
/**
*
* @param \Symfony\Component\DomCrawler\Link $updatePassword
* @depends testUpdate
* @dataProvider dataGenerateUserId
*/
public function testUpdatePassword(array $params)
public function testUpdatePassword(int $userId, $username)
{
$link = $params['link'];
$username = $params['username'];
$crawler = $this->client->request('GET', "/fr/admin/user/$userId/edit_password");
$newPassword = '1234Password!';
$crawler = $this->client->click($link);
$form = $crawler->selectButton('Changer le mot de passe')->form(array(
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
'chill_mainbundle_user_password[new_password][second]' => $newPassword,
@@ -130,10 +111,38 @@ class UserControllerTest extends WebTestCase
$this->assertTrue($this->client->getResponse()->isRedirect(),
"the response is a redirection");
$this->client->followRedirect();
$this->isPasswordValid($username, $newPassword);
}
protected function tearDown()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
foreach ($this->toDelete as list($class, $id)) {
$obj = $em->getRepository($class)->find($id);
$em->remove($obj);
}
$em->flush();
}
public function dataGenerateUserId()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword(self::$container->get(UserPasswordEncoderInterface::class)->encodePassword($user,
'password'));
$em->persist($user);
$em->flush();
$this->toDelete[] = [User::class, $user->getId()];
yield [ $user->getId(), $user->getUsername() ];
}
}

View File

@@ -293,6 +293,32 @@ paths:
401:
description: "Unauthorized"
/1.0/main/address/{id}/duplicate.json:
post:
tags:
- address
summary: Duplicate an existing address
parameters:
- name: id
in: path
required: true
description: The address id that will be duplicated
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Address'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/address-reference.json:
get:
tags: