Added delete user feature #225 (#249)

This commit is contained in:
Kevin Papst
2018-07-31 13:16:40 +02:00
committed by GitHub
parent 0996eca3c5
commit a8a8424ced
29 changed files with 879 additions and 49 deletions

View File

@@ -122,7 +122,7 @@ class ActivityController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
return $this->redirectToRoute('admin_activity');
}
return $this->render(
@@ -160,7 +160,7 @@ class ActivityController extends AbstractController
$editForm->get('create_more')->setData(true);
$activity = $newActivity;
} else {
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
return $this->redirectToRoute('admin_activity');
}
}

View File

@@ -101,7 +101,7 @@ class CustomerController extends AbstractController
$this->flashSuccess('action.updated_successfully');
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
return $this->redirectToRoute('admin_customer');
}
return $this->render('admin/customer_edit.html.twig', [
@@ -139,7 +139,7 @@ class CustomerController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
return $this->redirectToRoute('admin_customer');
}
return $this->render('admin/customer_delete.html.twig', [

View File

@@ -116,7 +116,7 @@ class ProjectController extends AbstractController
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
return $this->redirectToRoute('admin_project');
}
return $this->render('admin/project_delete.html.twig', [
@@ -151,7 +151,7 @@ class ProjectController extends AbstractController
$editForm->get('create_more')->setData(true);
$project = $newProject;
} else {
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
return $this->redirectToRoute('admin_project');
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Controller\Admin;
use App\Controller\AbstractController;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Form\Toolbar\UserToolbarForm;
use App\Form\UserCreateType;
@@ -19,6 +20,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* Controller used to manage users in the admin part of the site.
@@ -29,6 +31,27 @@ use Symfony\Component\HttpFoundation\Request;
*/
class UserController extends AbstractController
{
/**
* @var UserPasswordEncoderInterface
*/
protected $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
*/
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
/**
* @return \App\Repository\UserRepository
*/
protected function getRepository()
{
return $this->getDoctrine()->getRepository(User::class);
}
/**
* @Route("/", defaults={"page": 1}, name="admin_user")
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_user_paginated")
@@ -47,7 +70,7 @@ class UserController extends AbstractController
}
/* @var $entries Pagerfanta */
$entries = $this->getDoctrine()->getRepository(User::class)->findByQuery($query);
$entries = $this->getRepository()->findByQuery($query);
return $this->render('admin/user.html.twig', [
'entries' => $entries,
@@ -69,8 +92,7 @@ class UserController extends AbstractController
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($user, $user->getPlainPassword());
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
@@ -99,6 +121,49 @@ class UserController extends AbstractController
);
}
/**
* The route to delete an existing user.
*
* @Route("/{id}/delete", name="admin_user_delete")
* @Method({"GET", "POST"})
* @Security("is_granted('delete', userToDelete)")
*
* @param User $userToDelete
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function deleteAction(User $userToDelete, Request $request)
{
$stats = $this->getDoctrine()->getRepository(Timesheet::class)->getUserStatistics($userToDelete);
$deleteForm = $this->createFormBuilder()
->setAction($this->generateUrl('admin_user_delete', ['id' => $userToDelete->getId()]))
->setMethod('POST')
->getForm();
$deleteForm->handleRequest($request);
if (0 == $stats->getRecordsTotal() || ($deleteForm->isSubmitted() && $deleteForm->isValid())) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($userToDelete);
$entityManager->flush();
$this->flashSuccess('action.deleted_successfully');
return $this->redirectToRoute('admin_user');
}
return $this->render(
'admin/user_delete.html.twig',
[
'user' => $userToDelete,
'stats' => $stats,
'form' => $deleteForm->createView(),
]
);
}
/**
* @param UserQuery $query
* @return \Symfony\Component\Form\FormInterface

View File

@@ -22,6 +22,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* User profile controller
@@ -31,6 +32,19 @@ use Symfony\Component\HttpFoundation\Request;
*/
class ProfileController extends AbstractController
{
/**
* @var UserPasswordEncoderInterface
*/
protected $encoder;
/**
* @param UserPasswordEncoderInterface $encoder
*/
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
/**
* @Route("/{username}", name="user_profile")
* @Method("GET")
@@ -75,8 +89,7 @@ class ProfileController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($profile, $profile->getPlainPassword());
$password = $this->encoder->encodePassword($profile, $profile->getPlainPassword());
$profile->setPassword($password);
$entityManager = $this->getDoctrine()->getManager();
@@ -147,11 +160,9 @@ class ProfileController extends AbstractController
}
}
foreach ($preferences as $preference) {
$preference->setUser($profile);
$entityManager->persist($preference);
$entityManager->flush();
}
$profile->setPreferences($preferences);
$entityManager->persist($profile);
$entityManager->flush();
$this->flashSuccess('action.updated_successfully');

View File

@@ -69,6 +69,14 @@ class Activity
*/
private $timesheets;
/**
* @return Timesheet[]
*/
public function getTimesheets(): array
{
return $this->timesheets;
}
/**
* @return Project
*/

View File

@@ -64,7 +64,7 @@ class Timesheet
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(name="user", referencedColumnName="id")
* @ORM\JoinColumn(name="user", referencedColumnName="id", onDelete="CASCADE")
* @Assert\NotNull()
*/
private $user;

View File

@@ -191,12 +191,13 @@ class User extends BaseUser implements UserInterface
* @param UserPreference[]|Collection<UserPreference> $preferences
* @return User
*/
public function setPreferences(array $preferences)
public function setPreferences($preferences)
{
if (!($preferences instanceof Collection) && is_array($preferences)) {
$preferences = new ArrayCollection($preferences);
$this->preferences = new ArrayCollection();
foreach ($preferences as $preference) {
$this->addPreference($preference);
}
$this->preferences = $preferences;
return $this;
}
@@ -238,6 +239,7 @@ class User extends BaseUser implements UserInterface
public function addPreference(UserPreference $preference)
{
$this->preferences->add($preference);
$preference->setUser($this);
return $this;
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
/**
* Add constraints for the "delete user" feature.
*/
final class Version20180730044139 extends AbstractMigration
{
/**
* @var Index[]
*/
protected $indexesOld = [];
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('users');
$activity = $this->getTableName('activities');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activity . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE');
}
}
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\DBALException
* @throws \Doctrine\DBAL\Migrations\AbortMigrationException
*/
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('user');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)');
}
}
}

View File

@@ -36,6 +36,10 @@ class TimesheetStatistic
* @var \DateTime
*/
protected $firstEntry;
/**
* @var int
*/
protected $recordsTotal = 0;
/**
* @return int
@@ -116,4 +120,23 @@ class TimesheetStatistic
{
$this->firstEntry = $firstEntry;
}
/**
* @return int
*/
public function getRecordsTotal(): int
{
return $this->recordsTotal;
}
/**
* @param int $recordsTotal
* @return TimesheetStatistic
*/
public function setRecordsTotal(int $recordsTotal)
{
$this->recordsTotal = $recordsTotal;
return $this;
}
}

View File

@@ -121,6 +121,10 @@ class TimesheetRepository extends AbstractRepository
->createQuery('SELECT SUM(t.duration) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
->setParameter('user', $user)
->getSingleScalarResult();
$recordsTotal = $this->getEntityManager()
->createQuery('SELECT COUNT(t.id) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
->setParameter('user', $user)
->getSingleScalarResult();
$rateTotal = $this->getEntityManager()
->createQuery('SELECT SUM(t.rate) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
->setParameter('user', $user)
@@ -142,6 +146,7 @@ class TimesheetRepository extends AbstractRepository
$stats->setAmountThisMonth($amountMonth);
$stats->setDurationThisMonth($durationMonth);
$stats->setFirstEntry(new DateTime($firstEntry));
$stats->setRecordsTotal($recordsTotal);
return $stats;
}
@@ -203,6 +208,9 @@ class TimesheetRepository extends AbstractRepository
$durationTotal = $this->getEntityManager()
->createQuery('SELECT SUM(t.duration) FROM ' . Timesheet::class . ' t')
->getSingleScalarResult();
$recordsTotal = $this->getEntityManager()
->createQuery('SELECT COUNT(t.id) FROM ' . Timesheet::class . ' t')
->getSingleScalarResult();
$rateTotal = $this->getEntityManager()
->createQuery('SELECT SUM(t.rate) FROM ' . Timesheet::class . ' t')
->getSingleScalarResult();
@@ -228,6 +236,7 @@ class TimesheetRepository extends AbstractRepository
$stats->setActiveThisMonth($activeMonth);
$stats->setAmountThisMonth($amountMonth);
$stats->setDurationThisMonth($durationMonth);
$stats->setRecordsTotal($recordsTotal);
return $stats;
}

View File

@@ -93,7 +93,7 @@ class UserVoter extends AbstractVoter
*/
protected function canEditPreferences(User $profile, User $user, TokenInterface $token)
{
return $profile->getId() == $user->getId();
return $profile->getId() === $user->getId();
}
/**
@@ -107,7 +107,7 @@ class UserVoter extends AbstractVoter
return true;
}
return $profile->getId() == $user->getId();
return $profile->getId() === $user->getId();
}
/**
@@ -121,7 +121,7 @@ class UserVoter extends AbstractVoter
return true;
}
return $profile->getId() == $user->getId();
return $profile->getId() === $user->getId();
}
/**
@@ -131,7 +131,11 @@ class UserVoter extends AbstractVoter
*/
protected function canDelete(User $profile, User $user, TokenInterface $token)
{
return false;
if (!$this->canAdminUsers($token)) {
return false;
}
return $profile->getId() !== $user->getId();
}
/**
@@ -140,6 +144,6 @@ class UserVoter extends AbstractVoter
*/
protected function canAdminUsers(TokenInterface $token)
{
return $this->isFullyAuthenticated($token) && $this->hasRole('ROLE_SUPER_ADMIN', $token);
return $this->isFullyAuthenticated($token) && $this->hasRole(User::ROLE_SUPER_ADMIN, $token);
}
}