diff --git a/README.md b/README.md index 8dde2299..a13a1c24 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $ bin/console doctrine:schema:create And finally create your first user: ```bash -$ bin/console kimai:create-user username admin@example.com password en ROLE_SUPER_ADMIN +$ bin/console kimai:create-user username password admin@example.com ROLE_SUPER_ADMIN ``` For available roles, please refer to [the user documentation](var/docs/users.md). diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php index f4ecefd7..3ced2d99 100644 --- a/src/Command/CreateUserCommand.php +++ b/src/Command/CreateUserCommand.php @@ -70,9 +70,8 @@ class CreateUserCommand extends Command ->setDescription('Create a new user') ->setHelp('This command allows you to create a new user.') ->addArgument('username', InputArgument::REQUIRED, 'New username (must be unique)') - ->addArgument('email', InputArgument::REQUIRED, 'Users email address (must be unique)') ->addArgument('password', InputArgument::REQUIRED, 'Users password') - ->addArgument('language', InputArgument::OPTIONAL, 'Users language', User::DEFAULT_LANGUAGE) + ->addArgument('email', InputArgument::REQUIRED, 'Users email address (must be unique)') ->addArgument('role', InputArgument::OPTIONAL, 'Users role (comma separated list)', User::DEFAULT_ROLE) ; } @@ -87,17 +86,14 @@ class CreateUserCommand extends Command $username = $input->getArgument('username'); $email = $input->getArgument('email'); $password = $input->getArgument('password'); - $language = $input->getArgument('language'); $role = $input->getArgument('role'); - $language = $language ?: User::DEFAULT_LANGUAGE; $role = $role ?: User::DEFAULT_ROLE; $user = new User(); $user->setUsername($username) ->setPlainPassword($password) ->setEmail($email) - ->setLanguage($language) ->setRoles(explode(',', $role)) ; diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index c91e2165..fe0cca3c 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -14,7 +14,9 @@ namespace App\Controller; use App\Entity\User; use App\Form\UserEditType; use App\Form\UserPasswordType; +use App\Form\UserPreferencesForm; use App\Form\UserRolesType; +use App\Voter\UserVoter; use Symfony\Component\Form\Form; use App\Entity\Timesheet; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; @@ -40,7 +42,7 @@ class ProfileController extends AbstractController */ public function indexAction(User $profile) { - return $this->getProfileView($profile); + return $this->getProfileView($profile, 'charts'); } /** @@ -50,10 +52,10 @@ class ProfileController extends AbstractController */ public function editAction(User $profile, Request $request) { - $editForm = $this->createEditForm($profile); - $editForm->handleRequest($request); + $form = $this->createEditForm($profile); + $form->handleRequest($request); - if ($editForm->isSubmitted() && $editForm->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($profile); $entityManager->flush(); @@ -63,7 +65,7 @@ class ProfileController extends AbstractController return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]); } - return $this->getProfileView($profile, $editForm, null, null, 'profile'); + return $this->getProfileView($profile, 'settings', $form); } /** @@ -73,10 +75,10 @@ class ProfileController extends AbstractController */ public function passwordAction(User $profile, Request $request) { - $pwdForm = $this->createPasswordForm($profile); - $pwdForm->handleRequest($request); + $form = $this->createPasswordForm($profile); + $form->handleRequest($request); - if ($pwdForm->isSubmitted() && $pwdForm->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { $password = $this->get('security.password_encoder') ->encodePassword($profile, $profile->getPlainPassword()); $profile->setPassword($password); @@ -90,7 +92,7 @@ class ProfileController extends AbstractController return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]); } - return $this->getProfileView($profile, null, $pwdForm, null, 'password'); + return $this->getProfileView($profile, 'password', null, $form); } /** @@ -100,10 +102,10 @@ class ProfileController extends AbstractController */ public function rolesAction(User $profile, Request $request) { - $rolesForm = $this->createRolesForm($profile); - $rolesForm->handleRequest($request); + $form = $this->createRolesForm($profile); + $form->handleRequest($request); - if ($rolesForm->isSubmitted() && $rolesForm->isValid()) { + if ($form->isSubmitted() && $form->isValid()) { $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($profile); $entityManager->flush(); @@ -113,24 +115,73 @@ class ProfileController extends AbstractController return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]); } - return $this->getProfileView($profile, null, null, $rolesForm, 'roles'); + return $this->getProfileView($profile, 'roles', null, null, $form); + } + + /** + * @Route("/{username}/prefs", name="user_profile_preferences") + * @Method({"GET", "POST"}) + * @Security("is_granted('preferences', profile)") + */ + public function savePreferencesAction(User $profile, Request $request) + { + $original = []; + foreach ($profile->getPreferences() as $preference) { + $original[$preference->getName()] = $preference; + } + + $form = $this->createPreferencesForm($profile); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager = $this->getDoctrine()->getManager(); + $preferences = $profile->getPreferences(); + + // do not allow to add unknown preferences + foreach ($preferences as $preference) { + if (!isset($original[$preference->getName()])) { + $preferences->removeElement($preference); + } + } + + // but allow to delete already saved settings + foreach ($original as $name => $preference) { + if (false === $profile->getPreferences()->contains($preference)) { + $entityManager->remove($preference); + } + } + + foreach ($preferences as $preference) { + $preference->setUser($profile); + $entityManager->persist($preference); + $entityManager->flush(); + } + + $this->flashSuccess('action.updated_successfully'); + + return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]); + } + + return $this->getProfileView($profile, 'preferences', null, null, null, $form); } /** * @param User $user + * @param string $tab * @param Form|null $editForm * @param Form|null $pwdForm * @param Form|null $rolesForm - * @param string $tab + * @param Form|null $prefsForm * @return \Symfony\Component\HttpFoundation\Response * @throws \Doctrine\ORM\NonUniqueResultException */ protected function getProfileView( User $user, + string $tab, Form $editForm = null, Form $pwdForm = null, Form $rolesForm = null, - $tab = 'charts' + Form $prefsForm = null ) { /* @var $timesheetRepo TimesheetRepository */ $timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class); @@ -142,27 +193,45 @@ class ProfileController extends AbstractController 'user' => $user, 'stats' => $userStats, 'years' => $monthlyStats, - 'form' => null, - 'form_password' => null, - 'form_roles' => null, + 'forms' => [] ]; - if ($this->isGranted('edit', $user)) { + if ($this->isGranted(UserVoter::EDIT, $user)) { $editForm = $editForm ?: $this->createEditForm($user); - $viewVars['form'] = $editForm->createView(); + $viewVars['forms']['settings'] = $editForm->createView(); } - if ($this->isGranted('password', $user)) { + if ($this->isGranted(UserVoter::PASSWORD, $user)) { $pwdForm = $pwdForm ?: $this->createPasswordForm($user); - $viewVars['form_password'] = $pwdForm->createView(); + $viewVars['forms']['password'] = $pwdForm->createView(); } - if ($this->isGranted('roles', $user)) { + if ($this->isGranted(UserVoter::ROLES, $user)) { $rolesForm = $rolesForm ?: $this->createRolesForm($user); - $viewVars['form_roles'] = $rolesForm->createView(); + $viewVars['forms']['roles'] = $rolesForm->createView(); + } + if ($this->isGranted(UserVoter::PREFERENCES, $user)) { + $prefsForm = $prefsForm ?: $this->createPreferencesForm($user); + $viewVars['forms']['preferences'] = $prefsForm->createView(); } return $this->render('user/profile.html.twig', $viewVars); } + /** + * @param User $user + * @return \Symfony\Component\Form\FormInterface + */ + private function createPreferencesForm(User $user) + { + return $this->createForm( + UserPreferencesForm::class, + $user, + [ + 'action' => $this->generateUrl('user_profile_preferences', ['username' => $user->getUsername()]), + 'method' => 'POST' + ] + ); + } + /** * @param User $user * @return \Symfony\Component\Form\FormInterface diff --git a/src/Controller/SidebarController.php b/src/Controller/SidebarController.php index e608e76d..17775a71 100644 --- a/src/Controller/SidebarController.php +++ b/src/Controller/SidebarController.php @@ -11,6 +11,7 @@ namespace App\Controller; +use App\Form\UserPreferencesForm; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; /** @@ -22,13 +23,32 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; */ class SidebarController extends AbstractController { + /** + * @return \Symfony\Component\HttpFoundation\Response + */ public function homeAction() { return $this->render('sidebar/home.html.twig', []); } + /** + * @return \Symfony\Component\HttpFoundation\Response + */ public function settingsAction() { return $this->render('sidebar/settings.html.twig', []); + + /* + $user = $this->getUser(); + + $form = $this->createForm(UserPreferencesForm::class, $user, [ + 'action' => $this->generateUrl('user_profile_preferences', ['username' => $user->getUsername()]), + 'method' => 'POST', + ]); + + return $this->render('sidebar/settings.html.twig', [ + 'form' => $form->createView(), + ]); + */ } } diff --git a/src/Doctrine/TablePrefixSubscriber.php b/src/Doctrine/TablePrefixSubscriber.php index ff3461dd..f0b1cedb 100644 --- a/src/Doctrine/TablePrefixSubscriber.php +++ b/src/Doctrine/TablePrefixSubscriber.php @@ -12,13 +12,14 @@ namespace App\Doctrine; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\Common\EventSubscriber; /** * Adds a prefix to every doctrine entity AKA database table * * @author Kevin Papst */ -class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber +class TablePrefixSubscriber implements EventSubscriber { protected $prefix = ''; diff --git a/src/Entity/User.php b/src/Entity/User.php index cd8f9c87..0a6e67e5 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Validator\Constraints as KimaiAssert; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -10,7 +12,7 @@ use Symfony\Component\Validator\Constraints as Assert; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** - * User + * Application main User entity. * * @ORM\Entity(repositoryClass="App\Repository\UserRepository") * @ORM\Table( @@ -29,7 +31,6 @@ class User implements UserInterface, AdvancedUserInterface { const DEFAULT_ROLE = 'ROLE_USER'; - const DEFAULT_LANGUAGE = 'de'; /** * @var int @@ -81,14 +82,6 @@ class User implements UserInterface, AdvancedUserInterface */ private $alias; - /** - * @var string - * - * @ORM\Column(name="language", type="string", length=5, nullable=true) - * @Assert\Language() - */ - private $language = self::DEFAULT_LANGUAGE; - /** * @var boolean * @@ -126,12 +119,20 @@ class User implements UserInterface, AdvancedUserInterface */ private $roles = []; + /** + * @var UserPreference[]|Collection + * + * @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user") + */ + private $preferences; + /** * User constructor. */ public function __construct() { $this->registeredAt = new \DateTime(); + $this->preferences = new ArrayCollection(); } /** @@ -151,10 +152,10 @@ class User implements UserInterface, AdvancedUserInterface } /** - * @param $registeredAt + * @param \DateTime $registeredAt * @return $this */ - public function setRegisteredAt($registeredAt) + public function setRegisteredAt(\DateTime $registeredAt) { $this->registeredAt = $registeredAt; @@ -199,15 +200,6 @@ class User implements UserInterface, AdvancedUserInterface return $this->active; } - /** - * @deprecated - * @return boolean - */ - public function getActive() - { - return $this->isActive(); - } - /** * @param string $password * @return $this @@ -324,24 +316,6 @@ class User implements UserInterface, AdvancedUserInterface return $this; } - /** - * @return string - */ - public function getLanguage() - { - return $this->language; - } - - /** - * @param string $language - * @return $this - */ - public function setLanguage($language) - { - $this->language = $language; - return $this; - } - /** * Returns the roles or permissions granted to the user for security. * @@ -369,6 +343,37 @@ class User implements UserInterface, AdvancedUserInterface return $this; } + /** + * @return UserPreference[]|Collection + */ + public function getPreferences(): Collection + { + return $this->preferences; + } + + /** + * @param UserPreference[]|Collection $preferences + * @return User + */ + public function setPreferences(array $preferences) + { + if (!($preferences instanceof Collection) && is_array($preferences)) { + $preferences = new ArrayCollection($preferences); + } + $this->preferences = $preferences; + return $this; + } + + /** + * @param UserPreference $preference + * @return User + */ + public function addPreference(UserPreference $preference) + { + $this->preferences->add($preference); + return $this; + } + /** * @inheritdoc */ diff --git a/src/Entity/UserPreference.php b/src/Entity/UserPreference.php index 734c04c2..fefb125c 100644 --- a/src/Entity/UserPreference.php +++ b/src/Entity/UserPreference.php @@ -3,56 +3,124 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints as Assert; /** * UserPreference * - * @ORM\Table(name="preferences",indexes={@ORM\Index(columns={"userid", "option"})})) - * @ORM\Entity + * @ORM\Entity() + * @ORM\Table( + * name="user_preferences", + * uniqueConstraints={ + * @ORM\UniqueConstraint(columns={"user_id", "name"}) + * } + * ) */ class UserPreference { + const HOURLY_RATE = 'hourly_rate'; + const SKIN = 'skin'; /** - * @var integer + * @var int * * @ORM\Id * @ORM\GeneratedValue - * @ORM\Column(name="userID", type="integer") + * @ORM\Column(name="id", type="integer") */ - private $userid; + private $id; + + /** + * @var User + * + * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="preferences") + * @ORM\JoinColumn(onDelete="CASCADE") + * @Assert\NotNull() + */ + private $user; /** * @var string * - * @ORM\Column(name="option", type="string", length=255) + * @ORM\Column(name="name", type="string", length=50, nullable=false) + * @Assert\Length(min=2, max=50) */ - private $option; + private $name; /** * @var string * - * @ORM\Column(name="value", type="string", length=255, nullable=false) + * @ORM\Column(name="value", type="string", length=255, nullable=true) */ private $value; /** - * Set value - * - * @param string $value - * + * @var string + */ + protected $type = TextType::class; + + /** + * @var Constraint[] + */ + protected $constraints = []; + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id * @return UserPreference */ - public function setValue($value) + public function setId(int $id): UserPreference { - $this->value = $value; - + $this->id = $id; + return $this; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * @param User $user + * @return UserPreference + */ + public function setUser(User $user): UserPreference + { + $this->user = $user; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * @return UserPreference + */ + public function setName(string $name): UserPreference + { + $this->name = $name; return $this; } /** - * Get value - * * @return string */ public function getValue() @@ -61,50 +129,64 @@ class UserPreference } /** - * Set option - * - * @param string $option - * + * @param string $value * @return UserPreference */ - public function setOption($option) + public function setValue(string $value): UserPreference { - $this->option = $option; - + $this->value = $value; return $this; } /** - * Get option + * Sets the form type to edit that setting. * + * @param string $type + * @return UserPreference + */ + public function setType(string $type) + { + $this->type = $type; + return $this; + } + + /** * @return string */ - public function getOption() + public function getType() { - return $this->option; + return $this->type; } /** - * Set userid + * Set the constraints which are used for validation of the value. * - * @param integer $userid - * - * @return UserPreference + * @param Constraint[] $constraints + * @return $this */ - public function setUserid($userid) + public function setConstraints(array $constraints) { - $this->userid = $userid; - + $this->constraints = $constraints; return $this; } /** - * Get userid + * Adds a constraint which is used for validation of the value. * - * @return integer + * @param Constraint $constraint + * @return $this */ - public function getUserid() + public function addConstraint(Constraint $constraint) { - return $this->userid; + $this->constraints[] = $constraint; + return $this; + } + + /** + * @return Constraint[] + */ + public function getConstraints() + { + return $this->constraints; } } diff --git a/src/Event/UserPreferenceEvent.php b/src/Event/UserPreferenceEvent.php new file mode 100644 index 00000000..5a9b1708 --- /dev/null +++ b/src/Event/UserPreferenceEvent.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Event; + +use App\Entity\User; +use App\Entity\UserPreference; +use Symfony\Component\EventDispatcher\Event; + +/** + * @author Kevin Papst + */ +class UserPreferenceEvent extends Event +{ + + const CONFIGURE = 'app.user_preferences'; + + /** + * @var User + */ + protected $user; + /** + * @var UserPreference[] + */ + protected $preferences; + + /** + * UserPreferenceEvent constructor. + * @param User $user + * @param UserPreference[] $preferences + */ + public function __construct(User $user, array $preferences) + { + $this->user = $user; + $this->preferences = $preferences; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @return UserPreference[] + */ + public function getPreferences() + { + return $this->preferences; + } + + /** + * @param UserPreference $preference + */ + public function addUserPreference(UserPreference $preference) + { + foreach ($this->preferences as $pref) { + if ($pref->getName() == $preference->getName()) { + throw new \InvalidArgumentException( + 'Cannot add preference, a preference with the name "'.$preference->getName().'" is already existing' + ); + } + } + $this->preferences[] = $preference; + } +} diff --git a/src/EventListener/TimesheetListener.php b/src/EventListener/TimesheetListener.php index d15e4462..43144c25 100644 --- a/src/EventListener/TimesheetListener.php +++ b/src/EventListener/TimesheetListener.php @@ -11,6 +11,7 @@ namespace App\EventListener; +use App\Entity\UserPreference; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Event\LifecycleEventArgs; @@ -59,11 +60,23 @@ class TimesheetListener implements EventSubscriber $entity = $args->getObject(); if ($entity instanceof Timesheet) { + $duration = 0; if ($entity->getEnd() !== null) { - $entity->setDuration($entity->getEnd()->getTimestamp() - $entity->getBegin()->getTimestamp()); - } + $duration = $entity->getEnd()->getTimestamp() - $entity->getBegin()->getTimestamp(); + $entity->setDuration($duration); - // TODO calculate hourly rate + // TODO allow to set hourly rate on activity, project and customer and prefer these + + $hourlyRate = 0; + foreach ($entity->getUser()->getPreferences() as $preference) { + if ($preference->getName() == UserPreference::HOURLY_RATE) { + $hourlyRate = (int) $preference->getValue(); + } + } + + $rate = $hourlyRate * ($duration / 3600); + $entity->setRate($rate); + } } } } diff --git a/src/EventSubscriber/UserPreferenceSubscriber.php b/src/EventSubscriber/UserPreferenceSubscriber.php new file mode 100644 index 00000000..2460574d --- /dev/null +++ b/src/EventSubscriber/UserPreferenceSubscriber.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\EventSubscriber; + +use App\Entity\User; +use App\Entity\UserPreference; +use App\Event\UserPreferenceEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\HttpKernel\Event\KernelEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Validator\Constraints\Range; + +/** + * @author Kevin Papst + */ +class UserPreferenceSubscriber implements EventSubscriberInterface +{ + + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * @var TokenStorageInterface + */ + protected $storage; + + /** + * PreferenceService constructor. + * @param EventDispatcherInterface $dispatcher + * @param TokenStorageInterface $storage + */ + public function __construct(EventDispatcherInterface $dispatcher, TokenStorageInterface $storage) + { + $this->eventDispatcher = $dispatcher; + $this->storage = $storage; + } + + /** + * @return array + */ + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::CONTROLLER => ['onKernelEvent', -1], + ]; + } + + /** + * @return UserPreference[] + */ + public function getDefaultPreferences() + { + return [ + (new UserPreference()) + ->setName(UserPreference::HOURLY_RATE) + ->setValue(0) + ->setType(IntegerType::class) + ->addConstraint(new Range(['min' => 0])), + /* + (new UserPreference()) + ->setName(UserPreference::SKIN) + ->setValue('blue') + ->setType(SkinType::class), + (new UserPreference()) + ->setName('language') + ->setValue('de') + ->setType(LanguageType::class), + */ + ]; + } + + /** + * @param User $user + * @return User + */ + public function setupUserPreferences(User $user) + { + $prefs = []; + foreach ($user->getPreferences() as $preference) { + $prefs[$preference->getName()] = $preference; + } + + $event = new UserPreferenceEvent($user, $this->getDefaultPreferences()); + $this->eventDispatcher->dispatch(UserPreferenceEvent::CONFIGURE, $event); + + foreach ($event->getPreferences() as $preference) { + if (isset($prefs[$preference->getName()])) { + /** @var UserPreference $pref */ + $prefs[$preference->getName()] + ->setType($preference->getType()) + ->setConstraints($preference->getConstraints()) + ; + } else { + $prefs[$preference->getName()] = $preference; + } + } + + $user->setPreferences(array_values($prefs)); + + return $user; + } + + /** + * @param KernelEvent $event + */ + public function onKernelEvent(KernelEvent $event): void + { + // Ignore sub-requests + if (!$event->isMasterRequest()) { + return; + } + + // ignore events like the toolbar where we do not have a token + if ($this->storage->getToken() === null) { + return; + } + + /** @var User $user */ + $user = $this->storage->getToken()->getUser(); + + if (!($user instanceof User)) { + return; + } + + $this->setupUserPreferences($user); + } +} diff --git a/src/Form/Type/LanguageType.php b/src/Form/Type/LanguageType.php index c8ea3d6e..d9e149c6 100644 --- a/src/Form/Type/LanguageType.php +++ b/src/Form/Type/LanguageType.php @@ -1,9 +1,9 @@ + * (c) Kevin Papst * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -31,8 +31,8 @@ class LanguageType extends AbstractType { $resolver->setDefaults([ 'choices' => array( - Intl::getLocaleBundle()->getLocaleName('de', \Locale::getDefault()) => 'de', - Intl::getLocaleBundle()->getLocaleName('en', \Locale::getDefault()) => 'en', + Intl::getLocaleBundle()->getLocaleName('de', 'de') => 'de', + Intl::getLocaleBundle()->getLocaleName('en', 'en') => 'en', ) ]); } diff --git a/src/Form/Type/SkinType.php b/src/Form/Type/SkinType.php new file mode 100644 index 00000000..339d8c32 --- /dev/null +++ b/src/Form/Type/SkinType.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Custom form field type to select the themes skin. + * + * @author Kevin Papst + */ +class SkinType extends AbstractType +{ + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'required' => true, + 'choices' => array( + 'blue' => 'blue', + 'black' => 'black', + 'green' => 'green', + 'purple' => 'purple', + 'red' => 'red', + 'yellow' => 'yellow', + 'blue-light' => 'blue-light', + 'black-light' => 'black-light', + 'green-light' => 'green-light', + 'purple-light' => 'purple-light', + 'red-light' => 'red-light', + 'yellow-light' => 'yellow-light', + ) + ]); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return ChoiceType::class; + } +} diff --git a/src/Form/Type/UserPreferenceType.php b/src/Form/Type/UserPreferenceType.php new file mode 100644 index 00000000..6c97d8e4 --- /dev/null +++ b/src/Form/Type/UserPreferenceType.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Form\Type; + +use App\Entity\UserPreference; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Custom form field type to edit a user preference. + * + * @author Kevin Papst + */ +class UserPreferenceType extends AbstractType +{ + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventListener( + FormEvents::PRE_SET_DATA, + function (FormEvent $event) { + /** @var UserPreference $preference */ + $preference = $event->getData(); + + if ($preference instanceof UserPreference) { + // prevents unconfigured values from showing up in the form + if ($preference->getType() === null) { + return; + } + + $event->getForm()->add('value', $preference->getType(), [ + 'label' => 'label.' . $preference->getName(), + 'constraints' => $preference->getConstraints() + ]); + } + } + ); + $builder->add('name', HiddenType::class); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => UserPreference::class, + )); + } +} diff --git a/src/Form/UserCreateType.php b/src/Form/UserCreateType.php index 3faf4d13..73eb990e 100644 --- a/src/Form/UserCreateType.php +++ b/src/Form/UserCreateType.php @@ -37,8 +37,8 @@ class UserCreateType extends UserEditType ->add('plainPassword', RepeatedType::class, [ 'required' => true, 'type' => PasswordType::class, - 'first_options' => array('label' => 'security.label.password'), - 'second_options' => array('label' => 'security.label.password_repeat'), + 'first_options' => array('label' => 'label.password'), + 'second_options' => array('label' => 'label.password_repeat'), ]); parent::buildForm($builder, $options); diff --git a/src/Form/UserEditType.php b/src/Form/UserEditType.php index 34d25775..cbd6efe4 100644 --- a/src/Form/UserEditType.php +++ b/src/Form/UserEditType.php @@ -52,10 +52,6 @@ class UserEditType extends AbstractType ->add('email', TextType::class, [ 'label' => 'label.email', ]) - // string - length 5 - ->add('language', LanguageType::class, [ - 'label' => 'label.language', - ]) // boolean ->add('active', YesNoType::class, [ 'label' => 'label.active', diff --git a/src/Form/UserPasswordType.php b/src/Form/UserPasswordType.php index 657f72a4..2a8aeb97 100644 --- a/src/Form/UserPasswordType.php +++ b/src/Form/UserPasswordType.php @@ -34,8 +34,8 @@ class UserPasswordType extends AbstractType $builder ->add('plainPassword', RepeatedType::class, [ 'type' => PasswordType::class, - 'first_options' => array('label' => 'security.label.password'), - 'second_options' => array('label' => 'security.label.password_repeat'), + 'first_options' => array('label' => 'label.password'), + 'second_options' => array('label' => 'label.password_repeat'), ]) ; } diff --git a/src/Form/UserPreferencesForm.php b/src/Form/UserPreferencesForm.php new file mode 100644 index 00000000..bbdd1a94 --- /dev/null +++ b/src/Form/UserPreferencesForm.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Form; + +use App\Entity\User; +use App\Form\Type\UserPreferenceType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Defines the form used to edit the user preferences. + * + * @author Kevin Papst + */ +class UserPreferencesForm extends AbstractType +{ + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('preferences', CollectionType::class, [ + 'entry_type' => UserPreferenceType::class, + 'entry_options' => array('label' => false), + 'allow_add' => false, + 'allow_delete' => false, + 'label' => false, + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_token_id' => 'edit_user_preferences', + + ]); + } +} diff --git a/src/Voter/UserVoter.php b/src/Voter/UserVoter.php index 972d435e..cc1a21af 100644 --- a/src/Voter/UserVoter.php +++ b/src/Voter/UserVoter.php @@ -27,8 +27,20 @@ class UserVoter extends AbstractVoter const DELETE = 'delete'; const PASSWORD = 'password'; const ROLES = 'roles'; + const PREFERENCES = 'preferences'; const VIEW_ALL = 'view_all'; + const ALLOWED_ATTRIBUTES = [ + self::VIEW, + self::VIEW_ALL, + self::EDIT, + self::CREATE, + self::ROLES, + self::PASSWORD, + self::DELETE, + self::PREFERENCES + ]; + /** * @param string $attribute * @param mixed $subject @@ -36,10 +48,7 @@ class UserVoter extends AbstractVoter */ protected function supports($attribute, $subject) { - if (!in_array( - $attribute, - [self::VIEW, self::VIEW_ALL, self::EDIT, self::CREATE, self::ROLES, self::PASSWORD, self::DELETE] - )) { + if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) { return false; } @@ -69,6 +78,7 @@ class UserVoter extends AbstractVoter return $this->canView($subject, $user, $token); case self::EDIT: case self::PASSWORD: + case self::PREFERENCES: return $this->canEdit($subject, $user, $token); case self::DELETE: return $this->canDelete($subject, $user, $token); diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 7fdc7b77..a305358e 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -12,11 +12,11 @@ {% block avanzu_login_form %}
- +
- +
@@ -30,7 +30,7 @@ #}
- +
diff --git a/templates/sidebar/settings.html.twig b/templates/sidebar/settings.html.twig index 1ef193c6..eac224bb 100644 --- a/templates/sidebar/settings.html.twig +++ b/templates/sidebar/settings.html.twig @@ -9,3 +9,9 @@ +{# +{{ form_start(form) }} +{{ form_widget(form) }} + +{{ form_end(form) }} +#} \ No newline at end of file diff --git a/templates/user/profile.html.twig b/templates/user/profile.html.twig index 6f477d62..2ab4ab06 100644 --- a/templates/user/profile.html.twig +++ b/templates/user/profile.html.twig @@ -17,15 +17,9 @@ @@ -140,7 +118,6 @@ {# FIXME hourly rate must be dynamic
  • {{ 'label.hourly_rate'|trans }} 70
  • #}
  • {{ 'label.id'|trans }} {{ user.id }}
  • {{ 'label.username'|trans }} {{ user.username }}
  • -
  • {{ 'label.language'|trans }} {{ user.language }}
  • {{ 'profile.first_entry'|trans }} {{ stats.firstEntry|date }}
  • diff --git a/translations/messages.de.xliff b/translations/messages.de.xliff index 13ba9799..25065c8c 100644 --- a/translations/messages.de.xliff +++ b/translations/messages.de.xliff @@ -45,20 +45,16 @@ security.title.login Anmelden - - security.label.username - Benutzername - - - security.label.password + + label.password Passwort - - security.label.password_repeat + + label.password_repeat Passwort wiederholen - - security.action.sign_in + + action.sign_in Los geht's @@ -225,6 +221,10 @@ label.hourly_rate Stundenlohn + + label.skin + Kimai-Theme + label.hours Stunden @@ -363,7 +363,7 @@ profile.settings - Einstellungen + Profil profile.password @@ -373,6 +373,10 @@ profile.roles Rollen + + profile.preferences + Einstellungen +