Merge branch 'dune-risky'

Conflicts:
	src/Bundle/ChillActivityBundle/config/services.yaml
	src/Bundle/ChillPersonBundle/translations/messages.fr.yml
This commit is contained in:
2021-06-02 16:23:45 +02:00
38 changed files with 1267 additions and 426 deletions

View File

@@ -38,7 +38,7 @@ use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency;
* Class Activity
*
* @package Chill\ActivityBundle\Entity
* @ORM\Entity()
* @ORM\Entity(repositoryClass="Chill\ActivityBundle\Repository\ActivityRepository")
* @ORM\Table(name="activity")
* @ORM\HasLifecycleCallbacks()
* @UserCircleConsistency(

View File

@@ -0,0 +1,169 @@
<?php
/*
* Chill is a software for social workers
*
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS,
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\Person;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Entity\Scope;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query\Expr\Orx;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\EntityManagerInterface;
final class ActivityACLAwareRepository
{
private AuthorizationHelper $authorizationHelper;
private TokenStorageInterface $tokenStorage;
private ActivityRepository $repository;
private EntityManagerInterface $em;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
ActivityRepository $repository,
EntityManagerInterface $em
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->repository = $repository;
$this->em = $em;
}
public function queryTimelineIndexer(string $context, array $args = []): array
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$from = $this->getFromClauseCenter($args);
[$where, $parameters] = $this->getWhereClause($context, $args);
return [
'id' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('id'),
'type' => 'activity',
'date' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('date'),
'FROM' => $from,
'WHERE' => $where,
'parameters' => $parameters
];
}
private function getFromClauseCenter(array $args): string
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
return $metadataActivity->getTableName().' JOIN '
.$metadataPerson->getTableName().' ON '
.$metadataPerson->getTableName().'.'.
$associationMapping['joinColumns'][0]['referencedColumnName']
.' = '
.$associationMapping['joinColumns'][0]['name']
;
}
private function getWhereClause(string $context, array $args): array
{
$where = '';
$parameters = [];
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$activityToPerson = $metadataActivity->getAssociationMapping('person')['joinColumns'][0]['name'];
$activityToScope = $metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name'];
$personToCenter = $metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'];
// acls:
$role = new Role(ActivityVoter::SEE);
$reachableCenters = $this->authorizationHelper->getReachableCenters($this->tokenStorage->getToken()->getUser(),
$role);
if (count($reachableCenters) === 0) {
// insert a dummy condition
return 'FALSE = TRUE';
}
if ($context === 'person') {
// we start with activities having the person_id linked to person
$where .= sprintf('%s = ? AND ', $activityToPerson);
$parameters[] = $person->getId();
}
// we add acl (reachable center and scopes)
$where .= '('; // first loop for the for centers
$centersI = 0; // like centers#i
foreach ($reachableCenters as $center) {
// we pass if not in centers
if (!\in_array($center, $args['centers'])) {
continue;
}
// we get all the reachable scopes for this center
$reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), $role, $center);
// we get the ids for those scopes
$reachablesScopesId = array_map(
function(Scope $scope) { return $scope->getId(); },
$reachableScopes
);
// if not the first center
if ($centersI > 0) {
$where .= ') OR (';
}
// condition for the center
$where .= sprintf(' %s.%s = ? ', $metadataPerson->getTableName(), $personToCenter);
$parameters[] = $center->getId();
// begin loop for scopes
$where .= ' AND (';
$scopesI = 0; //like scope#i
foreach ($reachablesScopesId as $scopeId) {
if ($scopesI > 0) {
$where .= ' OR ';
}
$where .= sprintf(' %s.%s = ? ', $metadataActivity->getTableName(), $activityToScope);
$parameters[] = $scopeId;
$scopesI ++;
}
// close loop for scopes
$where .= ') ';
$centersI++;
}
// close loop for centers
$where .= ')';
return [$where, $parameters];
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* Chill is a software for social workers
*
* Copyright (C) 2021, Champs Libres Cooperative SCRLFS,
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method AccompanyingPeriodParticipation|null find($id, $lockMode = null, $lockVersion = null)
* @method AccompanyingPeriodParticipation|null findOneBy(array $criteria, array $orderBy = null)
* @method AccompanyingPeriodParticipation[] findAll()
* @method AccompanyingPeriodParticipation[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ActivityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Activity::class);
}
}

View File

@@ -1,11 +1,11 @@
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
<div>
<h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span></h3>
<h3>{{ activity.date|format_date('long') }}<span class="activity"> / {{ 'Activity'|trans }}</span>{% if 'person' != context %} / {{ activity.person|chill_entity_render_box({'addLink': true}) }}{% endif %}</h3>
<div class="statement">
<span class="statement">{{ '%user% has done an %activity_type%'|trans(
{
'%user%' : user,
'%user%' : activity.user,
'%activity_type%': activity.type.name|localize_translatable_string,
'%date%' : activity.date|format_date('long') }
) }}</span>
@@ -29,13 +29,13 @@
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activity_show', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-view">
<a href="{{ path('chill_activity_activity_show', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-view">
{{ 'Show the activity'|trans }}
</a>
</li>
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_edit', { 'person_id': person.id, 'id': activity.id} ) }}" class="sc-button bt-edit">
<a href="{{ path('chill_activity_activity_edit', { 'person_id': activity.person.id, 'id': activity.id} ) }}" class="sc-button bt-edit">
{{ 'Edit the activity'|trans }}
</a>
</li>

View File

@@ -21,6 +21,7 @@
namespace Chill\ActivityBundle\Timeline;
use Chill\MainBundle\Timeline\TimelineProviderInterface;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
use Doctrine\ORM\EntityManager;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -28,13 +29,13 @@ use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\Mapping\ClassMetadata;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Scope;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Timeline\TimelineSingleQuery;
/**
* Provide activity for inclusion in timeline
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @author Champs Libres <info@champs-libres.coop>
*/
*/
class TimelineActivityProvider implements TimelineProviderInterface
{
@@ -55,6 +56,10 @@ class TimelineActivityProvider implements TimelineProviderInterface
* @var \Chill\MainBundle\Entity\User
*/
protected $user;
protected ActivityACLAwareRepository $aclAwareRepository;
private const SUPPORTED_CONTEXTS = [ 'center', 'person'];
/**
* TimelineActivityProvider constructor.
@@ -66,11 +71,13 @@ class TimelineActivityProvider implements TimelineProviderInterface
public function __construct(
EntityManager $em,
AuthorizationHelper $helper,
TokenStorageInterface $storage
TokenStorageInterface $storage,
ActivityACLAwareRepository $aclAwareRepository
)
{
$this->em = $em;
$this->helper = $helper;
$this->aclAwareRepository = $aclAwareRepository;
if (!$storage->getToken()->getUser() instanceof \Chill\MainBundle\Entity\User)
{
@@ -86,67 +93,69 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
public function fetchQuery($context, array $args)
{
$this->checkContext($context);
if ('center' === $context) {
return TimelineSingleQuery::fromArray($this->aclAwareRepository
->queryTimelineIndexer($context, $args));
}
$metadataActivity = $this->em->getClassMetadata('ChillActivityBundle:Activity');
$metadataPerson = $this->em->getClassMetadata('ChillPersonBundle:Person');
$metadataActivity = $this->em->getClassMetadata(Activity::class);
[$where, $parameters] = $this->getWhereClauseForPerson($args['person']);
return array(
return TimelineSingleQuery::fromArray([
'id' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('id'),
'type' => 'activity',
'date' => $metadataActivity->getTableName()
.'.'.$metadataActivity->getColumnName('date'),
'FROM' => $this->getFromClause($metadataActivity, $metadataPerson),
'WHERE' => $this->getWhereClause($metadataActivity, $metadataPerson,
$args['person'])
);
'FROM' => $this->getFromClausePerson($args['person']),
'WHERE' => $where,
'parameters' => $parameters
]);
}
private function getWhereClause(ClassMetadata $metadataActivity,
ClassMetadata $metadataPerson, Person $person)
private function getWhereClauseForPerson(Person $person)
{
$role = new Role('CHILL_ACTIVITY_SEE');
$reachableCenters = $this->helper->getReachableCenters($this->user,
$role);
$parameters = [];
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
if (count($reachableCenters) === 0) {
return 'FALSE = TRUE';
$role = new Role('CHILL_ACTIVITY_SEE');
$reachableScopes = $this->helper->getReachableScopes($this->user,
$role, $person->getCenter());
$whereClause = sprintf(' {activity.person_id} = ? AND {activity.scope_id} IN ({scopes_ids}) ');
$scopes_ids = [];
// first parameter: activity.person_id
$parameters[] = $person->getId();
// loop on reachable scopes
foreach ($reachableScopes as $scope) {
if (\in_array($scope->getId(), $scopes_ids)) {
continue;
}
$scopes_ids[] = '?';
$parameters[] = $scope->getId();
}
// we start with activities having the person_id linked to person
// (currently only context "person" is supported)
$whereClause = sprintf('%s = %d',
$associationMapping['joinColumns'][0]['name'],
$person->getId());
// we add acl (reachable center and scopes)
$centerAndScopeLines = array();
foreach ($reachableCenters as $center) {
$reachablesScopesId = array_map(
function(Scope $scope) { return $scope->getId(); },
$this->helper->getReachableScopes($this->user, $role,
$person->getCenter())
);
$centerAndScopeLines[] = sprintf('(%s = %d AND %s IN (%s))',
$metadataPerson->getTableName().'.'.
$metadataPerson->getAssociationMapping('center')['joinColumns'][0]['name'],
$center->getId(),
$metadataActivity->getTableName().'.'.
return [
\strtr(
$whereClause,
[
'{activity.person_id}' => $associationMapping['joinColumns'][0]['name'],
'{activity.scope_id}' => $metadataActivity->getTableName().'.'.
$metadataActivity->getAssociationMapping('scope')['joinColumns'][0]['name'],
implode(',', $reachablesScopesId));
}
$whereClause .= ' AND ('.implode(' OR ', $centerAndScopeLines).')';
return $whereClause;
'{scopes_ids}' => \implode(", ", $scopes_ids)
,
]
),
$parameters
];
}
private function getFromClause(ClassMetadata $metadataActivity,
ClassMetadata $metadataPerson)
private function getFromClausePerson()
{
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
return $metadataActivity->getTableName().' JOIN '
@@ -157,14 +166,14 @@ class TimelineActivityProvider implements TimelineProviderInterface
.$associationMapping['joinColumns'][0]['name']
;
}
/**
*
* {@inheritDoc}
*/
public function getEntities(array $ids)
{
$activities = $this->em->getRepository('ChillActivityBundle:Activity')
$activities = $this->em->getRepository(Activity::class)
->findBy(array('id' => $ids));
$result = array();
@@ -183,14 +192,13 @@ class TimelineActivityProvider implements TimelineProviderInterface
{
$this->checkContext($context);
return array(
return [
'template' => 'ChillActivityBundle:Timeline:activity_person_context.html.twig',
'template_data' => array(
'template_data' => [
'activity' => $entity,
'person' => $args['person'],
'user' => $entity->getUser()
)
);
'context' => $context
]
];
}
/**
@@ -210,7 +218,7 @@ class TimelineActivityProvider implements TimelineProviderInterface
*/
private function checkContext($context)
{
if ($context !== 'person') {
if (FALSE === \in_array($context, self::SUPPORTED_CONTEXTS)) {
throw new \LogicException("The context '$context' is not "
. "supported. Currently only 'person' is supported");
}

View File

@@ -22,9 +22,11 @@ services:
- '@doctrine.orm.entity_manager'
- '@chill.main.security.authorization.helper'
- '@security.token_storage'
- '@Chill\ActivityBundle\Repository\ActivityACLAwareRepository'
public: true
tags:
- { name: chill.timeline, context: 'person' }
- { name: chill.timeline, context: 'center' }
Chill\ActivityBundle\Menu\:
autowire: true

View File

@@ -1,18 +1,32 @@
---
services:
chill_activity.repository.activity_type:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments:
- 'Chill\ActivityBundle\Entity\ActivityType'
chill_activity.repository.reason:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments:
- 'Chill\ActivityBundle\Entity\ActivityReason'
chill_activity.repository.reason_category:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments:
- 'Chill\ActivityBundle\Entity\ActivityReasonCategory'
Chill\ActivityBundle\Repository\ActivityRepository:
tags: [doctrine.repository_service]
arguments:
- '@Doctrine\Persistence\ManagerRegistry'
Chill\ActivityBundle\Repository\ActivityACLAwareRepository:
arguments:
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$repository: '@Chill\ActivityBundle\Repository\ActivityRepository'
$em: '@Doctrine\ORM\EntityManagerInterface'