first draft
This commit is contained in:
7
src/.htaccess
Normal file
7
src/.htaccess
Normal file
@@ -0,0 +1,7 @@
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
24
src/AppBundle/AppBundle.php
Normal file
24
src/AppBundle/AppBundle.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
/**
|
||||
* Kimai main application bundle.
|
||||
*
|
||||
* @see http://symfony.com/doc/current/cookbook/bundles/best_practices.html
|
||||
* @see http://symfony.com/doc/current/best_practices/business-logic.html
|
||||
*/
|
||||
class AppBundle extends Bundle
|
||||
{
|
||||
}
|
||||
222
src/AppBundle/Controller/Admin/BlogController.php
Normal file
222
src/AppBundle/Controller/Admin/BlogController.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Controller\Admin;
|
||||
|
||||
use AppBundle\Entity\Post;
|
||||
use AppBundle\Form\PostType;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Controller used to manage blog contents in the backend.
|
||||
*
|
||||
* Please note that the application backend is developed manually for learning
|
||||
* purposes. However, in your real Symfony application you should use any of the
|
||||
* existing bundles that let you generate ready-to-use backends without effort.
|
||||
* See http://knpbundles.com/keyword/admin
|
||||
*
|
||||
* @Route("/admin/post")
|
||||
* @Security("has_role('ROLE_ADMIN')")
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class BlogController extends Controller
|
||||
{
|
||||
/**
|
||||
* Lists all Post entities.
|
||||
*
|
||||
* This controller responds to two different routes with the same URL:
|
||||
* * 'admin_post_index' is the route with a name that follows the same
|
||||
* structure as the rest of the controllers of this class.
|
||||
* * 'admin_index' is a nice shortcut to the backend homepage. This allows
|
||||
* to create simpler links in the templates. Moreover, in the future we
|
||||
* could move this annotation to any other controller while maintaining
|
||||
* the route name and therefore, without breaking any existing link.
|
||||
*
|
||||
* @Route("/", name="admin_index")
|
||||
* @Route("/", name="admin_post_index")
|
||||
* @Method("GET")
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$posts = $entityManager->getRepository(Post::class)->findAll();
|
||||
|
||||
return $this->render('admin/blog/index.html.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Post entity.
|
||||
*
|
||||
* @Route("/new", name="admin_post_new")
|
||||
* @Method({"GET", "POST"})
|
||||
*
|
||||
* NOTE: the Method annotation is optional, but it's a recommended practice
|
||||
* to constraint the HTTP methods each controller responds to (by default
|
||||
* it responds to all methods).
|
||||
*/
|
||||
public function newAction(Request $request)
|
||||
{
|
||||
$post = new Post();
|
||||
$post->setAuthorEmail($this->getUser()->getEmail());
|
||||
|
||||
// See http://symfony.com/doc/current/book/forms.html#submitting-forms-with-multiple-buttons
|
||||
$form = $this->createForm(PostType::class, $post)
|
||||
->add('saveAndCreateNew', SubmitType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
// the isSubmitted() method is completely optional because the other
|
||||
// isValid() method already checks whether the form is submitted.
|
||||
// However, we explicitly add it to improve code readability.
|
||||
// See http://symfony.com/doc/current/best_practices/forms.html#handling-form-submits
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$post->setSlug($this->get('slugger')->slugify($post->getTitle()));
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($post);
|
||||
$entityManager->flush();
|
||||
|
||||
// Flash messages are used to notify the user about the result of the
|
||||
// actions. They are deleted automatically from the session as soon
|
||||
// as they are accessed.
|
||||
// See http://symfony.com/doc/current/book/controller.html#flash-messages
|
||||
$this->addFlash('success', 'post.created_successfully');
|
||||
|
||||
if ($form->get('saveAndCreateNew')->isClicked()) {
|
||||
return $this->redirectToRoute('admin_post_new');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_post_index');
|
||||
}
|
||||
|
||||
return $this->render('admin/blog/new.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and displays a Post entity.
|
||||
*
|
||||
* @Route("/{id}", requirements={"id": "\d+"}, name="admin_post_show")
|
||||
* @Method("GET")
|
||||
*/
|
||||
public function showAction(Post $post)
|
||||
{
|
||||
// This security check can also be performed:
|
||||
// 1. Using an annotation: @Security("post.isAuthor(user)")
|
||||
// 2. Using a "voter" (see http://symfony.com/doc/current/cookbook/security/voters_data_permission.html)
|
||||
if (null === $this->getUser() || !$post->isAuthor($this->getUser())) {
|
||||
throw $this->createAccessDeniedException('Posts can only be shown to their authors.');
|
||||
}
|
||||
|
||||
$deleteForm = $this->createDeleteForm($post);
|
||||
|
||||
return $this->render('admin/blog/show.html.twig', [
|
||||
'post' => $post,
|
||||
'delete_form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a form to edit an existing Post entity.
|
||||
*
|
||||
* @Route("/{id}/edit", requirements={"id": "\d+"}, name="admin_post_edit")
|
||||
* @Method({"GET", "POST"})
|
||||
*/
|
||||
public function editAction(Post $post, Request $request)
|
||||
{
|
||||
if (null === $this->getUser() || !$post->isAuthor($this->getUser())) {
|
||||
throw $this->createAccessDeniedException('Posts can only be edited by their authors.');
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
$editForm = $this->createForm(PostType::class, $post);
|
||||
$deleteForm = $this->createDeleteForm($post);
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$post->setSlug($this->get('slugger')->slugify($post->getTitle()));
|
||||
$entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'post.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute('admin_post_edit', ['id' => $post->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('admin/blog/edit.html.twig', [
|
||||
'post' => $post,
|
||||
'edit_form' => $editForm->createView(),
|
||||
'delete_form' => $deleteForm->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a Post entity.
|
||||
*
|
||||
* @Route("/{id}", name="admin_post_delete")
|
||||
* @Method("DELETE")
|
||||
* @Security("post.isAuthor(user)")
|
||||
*
|
||||
* The Security annotation value is an expression (if it evaluates to false,
|
||||
* the authorization mechanism will prevent the user accessing this resource).
|
||||
* The isAuthor() method is defined in the AppBundle\Entity\Post entity.
|
||||
*/
|
||||
public function deleteAction(Request $request, Post $post)
|
||||
{
|
||||
$form = $this->createDeleteForm($post);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
|
||||
$entityManager->remove($post);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'post.deleted_successfully');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_post_index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form to delete a Post entity by id.
|
||||
*
|
||||
* This is necessary because browsers don't support HTTP methods different
|
||||
* from GET and POST. Since the controller that removes the blog posts expects
|
||||
* a DELETE method, the trick is to create a simple form that *fakes* the
|
||||
* HTTP DELETE method.
|
||||
* See http://symfony.com/doc/current/cookbook/routing/method_parameters.html.
|
||||
*
|
||||
* @param Post $post The post object
|
||||
*
|
||||
* @return \Symfony\Component\Form\Form The form
|
||||
*/
|
||||
private function createDeleteForm(Post $post)
|
||||
{
|
||||
return $this->createFormBuilder()
|
||||
->setAction($this->generateUrl('admin_post_delete', ['id' => $post->getId()]))
|
||||
->setMethod('DELETE')
|
||||
->getForm()
|
||||
;
|
||||
}
|
||||
}
|
||||
132
src/AppBundle/Controller/BlogController.php
Normal file
132
src/AppBundle/Controller/BlogController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Controller;
|
||||
|
||||
use AppBundle\Entity\Comment;
|
||||
use AppBundle\Entity\Post;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use AppBundle\Form\CommentType;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Controller used to manage blog contents in the public part of the site.
|
||||
*
|
||||
* @Route("/blog")
|
||||
* @Security("has_role('ROLE_USER')")
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class BlogController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="blog_index")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="blog_index_paginated")
|
||||
* @Method("GET")
|
||||
* @Cache(smaxage="10")
|
||||
*/
|
||||
public function indexAction($page)
|
||||
{
|
||||
$posts = $this->getDoctrine()->getRepository(Post::class)->findLatest($page);
|
||||
|
||||
return $this->render('blog/index.html.twig', ['posts' => $posts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/posts/{slug}", name="blog_post")
|
||||
* @Method("GET")
|
||||
*
|
||||
* NOTE: The $post controller argument is automatically injected by Symfony
|
||||
* after performing a database query looking for a Post with the 'slug'
|
||||
* value given in the route.
|
||||
* See http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
|
||||
*/
|
||||
public function postShowAction(Post $post)
|
||||
{
|
||||
// Symfony provides a function called 'dump()' which is an improved version
|
||||
// of the 'var_dump()' function. It's useful to quickly debug the contents
|
||||
// of any variable, but it's not available in the 'prod' environment to
|
||||
// prevent any leak of sensitive information.
|
||||
// This function can be used both in PHP files and Twig templates. The only
|
||||
// requirement is to have enabled the DebugBundle.
|
||||
if ('dev' === $this->getParameter('kernel.environment')) {
|
||||
dump($post, $this->get('security.token_storage')->getToken()->getUser(), new \DateTime());
|
||||
}
|
||||
|
||||
return $this->render('blog/post_show.html.twig', ['post' => $post]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/comment/{postSlug}/new", name="comment_new")
|
||||
* @Method("POST")
|
||||
* @Security("is_granted('IS_AUTHENTICATED_FULLY')")
|
||||
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
|
||||
*
|
||||
* NOTE: The ParamConverter mapping is required because the route parameter
|
||||
* (postSlug) doesn't match any of the Doctrine entity properties (slug).
|
||||
* See http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter
|
||||
*/
|
||||
public function commentNewAction(Request $request, Post $post)
|
||||
{
|
||||
$form = $this->createForm(CommentType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var Comment $comment */
|
||||
$comment = $form->getData();
|
||||
$comment->setAuthorEmail($this->getUser()->getEmail());
|
||||
$comment->setPost($post);
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($comment);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
|
||||
}
|
||||
|
||||
return $this->render('blog/comment_form_error.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This controller is called directly via the render() function in the
|
||||
* blog/post_show.html.twig template. That's why it's not needed to define
|
||||
* a route name for it.
|
||||
*
|
||||
* The "id" of the Post is passed in and then turned into a Post object
|
||||
* automatically by the ParamConverter.
|
||||
*
|
||||
* @param Post $post
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function commentFormAction(Post $post)
|
||||
{
|
||||
$form = $this->createForm(CommentType::class);
|
||||
|
||||
return $this->render('blog/_comment_form.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
src/AppBundle/Controller/SecurityController.php
Normal file
54
src/AppBundle/Controller/SecurityController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage the application security.
|
||||
* See http://symfony.com/doc/current/cookbook/security/form_login_setup.html.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class SecurityController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/login", name="security_login")
|
||||
*/
|
||||
public function loginAction()
|
||||
{
|
||||
$helper = $this->get('security.authentication_utils');
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
// last username entered by the user (if any)
|
||||
'last_username' => $helper->getLastUsername(),
|
||||
// last authentication error (if any)
|
||||
'error' => $helper->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the route the user can use to logout.
|
||||
*
|
||||
* But, this will never be executed. Symfony will intercept this first
|
||||
* and handle the logout automatically. See logout in app/config/security.yml
|
||||
*
|
||||
* @Route("/logout", name="security_logout")
|
||||
*/
|
||||
public function logoutAction()
|
||||
{
|
||||
throw new \Exception('This should never be reached!');
|
||||
}
|
||||
}
|
||||
200
src/AppBundle/DataFixtures/ORM/LoadFixtures.php
Normal file
200
src/AppBundle/DataFixtures/ORM/LoadFixtures.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\DataFixtures\ORM;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use AppBundle\Entity\Post;
|
||||
use AppBundle\Entity\Comment;
|
||||
use Doctrine\Common\DataFixtures\FixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests. Execute this command to load the data:
|
||||
*
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* See http://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
{
|
||||
/** @var ContainerInterface */
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
// FIXME CAN BE REMOVED
|
||||
$this->loadUsers($manager);
|
||||
$this->loadPosts($manager);
|
||||
}
|
||||
|
||||
private function loadUsers(ObjectManager $manager)
|
||||
{
|
||||
$passwordEncoder = $this->container->get('security.password_encoder');
|
||||
|
||||
$johnUser = new User();
|
||||
$johnUser->setUsername('john_user');
|
||||
$johnUser->setEmail('john_user@symfony.com');
|
||||
$encodedPassword = $passwordEncoder->encodePassword($johnUser, 'kitten');
|
||||
$johnUser->setPassword($encodedPassword);
|
||||
$manager->persist($johnUser);
|
||||
|
||||
$annaAdmin = new User();
|
||||
$annaAdmin->setUsername('anna_admin');
|
||||
$annaAdmin->setEmail('anna_admin@symfony.com');
|
||||
$annaAdmin->setRoles(['ROLE_ADMIN']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($annaAdmin, 'kitten');
|
||||
$annaAdmin->setPassword($encodedPassword);
|
||||
$manager->persist($annaAdmin);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function loadPosts(ObjectManager $manager)
|
||||
{
|
||||
foreach (range(1, 30) as $i) {
|
||||
$post = new Post();
|
||||
|
||||
$post->setTitle($this->getRandomPostTitle());
|
||||
$post->setSummary($this->getRandomPostSummary());
|
||||
$post->setSlug($this->container->get('slugger')->slugify($post->getTitle()));
|
||||
$post->setContent($this->getPostContent());
|
||||
$post->setAuthorEmail('anna_admin@symfony.com');
|
||||
$post->setPublishedAt(new \DateTime('now - '.$i.'days'));
|
||||
|
||||
foreach (range(1, 5) as $j) {
|
||||
$comment = new Comment();
|
||||
|
||||
$comment->setAuthorEmail('john_user@symfony.com');
|
||||
$comment->setPublishedAt(new \DateTime('now + '.($i + $j).'seconds'));
|
||||
$comment->setContent($this->getRandomCommentContent());
|
||||
$comment->setPost($post);
|
||||
|
||||
$manager->persist($comment);
|
||||
$post->addComment($comment);
|
||||
}
|
||||
|
||||
$manager->persist($post);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setContainer(ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
protected function getPostContent()
|
||||
{
|
||||
return <<<MARKDOWN
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit, sed do eiusmod tempor
|
||||
incididunt ut labore et **dolore magna aliqua**: Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
||||
deserunt mollit anim id est laborum.
|
||||
|
||||
* Ut enim ad minim veniam
|
||||
* Quis nostrud exercitation *ullamco laboris*
|
||||
* Nisi ut aliquip ex ea commodo consequat
|
||||
|
||||
Praesent id fermentum lorem. Ut est lorem, fringilla at accumsan nec, euismod at
|
||||
nunc. Aenean mattis sollicitudin mattis. Nullam pulvinar vestibulum bibendum.
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
|
||||
himenaeos. Fusce nulla purus, gravida ac interdum ut, blandit eget ex. Duis a
|
||||
luctus dolor.
|
||||
|
||||
Integer auctor massa maximus nulla scelerisque accumsan. *Aliquam ac malesuada*
|
||||
ex. Pellentesque tortor magna, vulputate eu vulputate ut, venenatis ac lectus.
|
||||
Praesent ut lacinia sem. Mauris a lectus eget felis mollis feugiat. Quisque
|
||||
efficitur, mi ut semper pulvinar, urna urna blandit massa, eget tincidunt augue
|
||||
nulla vitae est.
|
||||
|
||||
Ut posuere aliquet tincidunt. Aliquam erat volutpat. **Class aptent taciti**
|
||||
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi
|
||||
arcu orci, gravida eget aliquam eu, suscipit et ante. Morbi vulputate metus vel
|
||||
ipsum finibus, ut dapibus massa feugiat. Vestibulum vel lobortis libero. Sed
|
||||
tincidunt tellus et viverra scelerisque. Pellentesque tincidunt cursus felis.
|
||||
Sed in egestas erat.
|
||||
|
||||
Aliquam pulvinar interdum massa, vel ullamcorper ante consectetur eu. Vestibulum
|
||||
lacinia ac enim vel placerat. Integer pulvinar magna nec dui malesuada, nec
|
||||
congue nisl dictum. Donec mollis nisl tortor, at congue erat consequat a. Nam
|
||||
tempus elit porta, blandit elit vel, viverra lorem. Sed sit amet tellus
|
||||
tincidunt, faucibus nisl in, aliquet libero.
|
||||
MARKDOWN;
|
||||
}
|
||||
|
||||
protected function getPhrases()
|
||||
{
|
||||
return [
|
||||
'Lorem ipsum dolor sit amet consectetur adipiscing elit',
|
||||
'Pellentesque vitae velit ex',
|
||||
'Mauris dapibus risus quis suscipit vulputate',
|
||||
'Eros diam egestas libero eu vulputate risus',
|
||||
'In hac habitasse platea dictumst',
|
||||
'Morbi tempus commodo mattis',
|
||||
'Ut suscipit posuere justo at vulputate',
|
||||
'Ut eleifend mauris et risus ultrices egestas',
|
||||
'Aliquam sodales odio id eleifend tristique',
|
||||
'Urna nisl sollicitudin id varius orci quam id turpis',
|
||||
'Nulla porta lobortis ligula vel egestas',
|
||||
'Curabitur aliquam euismod dolor non ornare',
|
||||
'Sed varius a risus eget aliquam',
|
||||
'Nunc viverra elit ac laoreet suscipit',
|
||||
'Pellentesque et sapien pulvinar consectetur',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRandomPhrase()
|
||||
{
|
||||
return $this->getRandomPostTitle();
|
||||
}
|
||||
|
||||
private function getRandomPostTitle()
|
||||
{
|
||||
$titles = $this->getPhrases();
|
||||
|
||||
return $titles[array_rand($titles)];
|
||||
}
|
||||
|
||||
private function getRandomPostSummary($maxLength = 255)
|
||||
{
|
||||
$phrases = $this->getPhrases();
|
||||
|
||||
$numPhrases = mt_rand(6, 12);
|
||||
shuffle($phrases);
|
||||
|
||||
return substr(implode(' ', array_slice($phrases, 0, $numPhrases-1)), 0, $maxLength);
|
||||
}
|
||||
|
||||
private function getRandomCommentContent()
|
||||
{
|
||||
$phrases = $this->getPhrases();
|
||||
|
||||
$numPhrases = mt_rand(2, 15);
|
||||
shuffle($phrases);
|
||||
|
||||
return implode(' ', array_slice($phrases, 0, $numPhrases-1));
|
||||
}
|
||||
}
|
||||
54
src/AppBundle/Doctrine/TablePrefixSubscriber.php
Normal file
54
src/AppBundle/Doctrine/TablePrefixSubscriber.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Doctrine;
|
||||
|
||||
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
||||
|
||||
/**
|
||||
* Adds a prefix to every doctrine entity AKA database table
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
|
||||
{
|
||||
protected $prefix = '';
|
||||
|
||||
public function __construct($prefix)
|
||||
{
|
||||
$this->prefix = (string) $prefix;
|
||||
}
|
||||
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return array('loadClassMetadata');
|
||||
}
|
||||
|
||||
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
|
||||
{
|
||||
$classMetadata = $args->getClassMetadata();
|
||||
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
|
||||
// if we are in an inheritance hierarchy, only apply this once
|
||||
return;
|
||||
}
|
||||
|
||||
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
|
||||
// Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
|
||||
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) ) {
|
||||
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
|
||||
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/AppBundle/Entity/Comment.php
Normal file
126
src/AppBundle/Entity/Comment.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="demo_comment")
|
||||
*
|
||||
* Defines the properties of the Comment entity to represent the blog comments.
|
||||
* See http://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See http://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Comment
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="comment.blank")
|
||||
* @Assert\Length(
|
||||
* min = "5",
|
||||
* minMessage = "comment.too_short",
|
||||
* max = "10000",
|
||||
* maxMessage = "comment.too_long"
|
||||
* )
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\Email()
|
||||
*/
|
||||
private $authorEmail;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
* @Assert\DateTime()
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Assert\IsTrue(message = "comment.is_spam")
|
||||
*/
|
||||
public function isLegitComment()
|
||||
{
|
||||
$containsInvalidCharacters = false !== strpos($this->content, '@');
|
||||
|
||||
return !$containsInvalidCharacters;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
public function setContent($content)
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getAuthorEmail()
|
||||
{
|
||||
return $this->authorEmail;
|
||||
}
|
||||
public function setAuthorEmail($authorEmail)
|
||||
{
|
||||
$this->authorEmail = $authorEmail;
|
||||
}
|
||||
|
||||
public function getPublishedAt()
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
public function setPublishedAt(\DateTime $publishedAt)
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getPost()
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
public function setPost(Post $post)
|
||||
{
|
||||
$this->post = $post;
|
||||
}
|
||||
}
|
||||
184
src/AppBundle/Entity/Post.php
Normal file
184
src/AppBundle/Entity/Post.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
|
||||
* @ORM\Table(name="demo_post")
|
||||
*
|
||||
* Defines the properties of the Post entity to represent the blog posts.
|
||||
* See http://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See http://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Post
|
||||
{
|
||||
/**
|
||||
* Use constants to define configuration options that rarely change instead
|
||||
* of specifying them in app/config/config.yml.
|
||||
* See http://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options
|
||||
*/
|
||||
const NUM_ITEMS = 10;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank(message="post.blank_summary")
|
||||
*/
|
||||
private $summary;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="post.blank_content")
|
||||
* @Assert\Length(min = "10", minMessage = "post.too_short_content")
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\Email()
|
||||
*/
|
||||
private $authorEmail;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
* @Assert\DateTime()
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="Comment",
|
||||
* mappedBy="post",
|
||||
* orphanRemoval=true
|
||||
* )
|
||||
* @ORM\OrderBy({"publishedAt" = "DESC"})
|
||||
*/
|
||||
private $comments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
$this->comments = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getSlug()
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug($slug)
|
||||
{
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent($content)
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getAuthorEmail()
|
||||
{
|
||||
return $this->authorEmail;
|
||||
}
|
||||
|
||||
public function setAuthorEmail($authorEmail)
|
||||
{
|
||||
$this->authorEmail = $authorEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the given User the author of this Post?
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthor(User $user)
|
||||
{
|
||||
return $user->getEmail() === $this->getAuthorEmail();
|
||||
}
|
||||
|
||||
public function getPublishedAt()
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(\DateTime $publishedAt)
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getComments()
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function addComment(Comment $comment)
|
||||
{
|
||||
$this->comments->add($comment);
|
||||
$comment->setPost($this);
|
||||
}
|
||||
|
||||
public function removeComment(Comment $comment)
|
||||
{
|
||||
$this->comments->removeElement($comment);
|
||||
}
|
||||
|
||||
public function getSummary()
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function setSummary($summary)
|
||||
{
|
||||
$this->summary = $summary;
|
||||
}
|
||||
}
|
||||
130
src/AppBundle/Entity/User.php
Normal file
130
src/AppBundle/Entity/User.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace AppBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
|
||||
* @ORM\Table(name="demo_user")
|
||||
*
|
||||
* Defines the properties of the User entity to represent the application users.
|
||||
* See http://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See http://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class User implements UserInterface
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
*/
|
||||
private $username;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $password;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json_array")
|
||||
*/
|
||||
private $roles = [];
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
public function setEmail($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the roles or permissions granted to the user for security.
|
||||
*/
|
||||
public function getRoles()
|
||||
{
|
||||
$roles = $this->roles;
|
||||
|
||||
// guarantees that a user always has at least one role for security
|
||||
if (empty($roles)) {
|
||||
$roles[] = 'ROLE_USER';
|
||||
}
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function setRoles(array $roles)
|
||||
{
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the salt that was originally used to encode the password.
|
||||
*/
|
||||
public function getSalt()
|
||||
{
|
||||
// See "Do you need to use a Salt?" at http://symfony.com/doc/current/cookbook/security/entity_provider.html
|
||||
// we're using bcrypt in security.yml to encode the password, so
|
||||
// the salt value is built-in and you don't have to generate one
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes sensitive data from the user.
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
{
|
||||
// if you had a plainPassword property, you'd nullify it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
}
|
||||
55
src/AppBundle/Event/ConfigureAdminMenuEvent.php
Normal file
55
src/AppBundle/Event/ConfigureAdminMenuEvent.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Event;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* The ConfigureAdminMenuEvent is used for populating the administration navigation.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ConfigureAdminMenuEvent extends Event
|
||||
{
|
||||
const CONFIGURE = 'app.admin_menu_configure';
|
||||
|
||||
private $factory;
|
||||
private $menu;
|
||||
|
||||
/**
|
||||
* @param \Knp\Menu\FactoryInterface $factory
|
||||
* @param \Knp\Menu\ItemInterface $menu
|
||||
*/
|
||||
public function __construct(FactoryInterface $factory, ItemInterface $menu)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
$this->menu = $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Knp\Menu\FactoryInterface
|
||||
*/
|
||||
public function getFactory()
|
||||
{
|
||||
return $this->factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Knp\Menu\ItemInterface
|
||||
*/
|
||||
public function getMenu()
|
||||
{
|
||||
return $this->menu;
|
||||
}
|
||||
}
|
||||
55
src/AppBundle/Event/ConfigureMainMenuEvent.php
Normal file
55
src/AppBundle/Event/ConfigureMainMenuEvent.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Event;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class ConfigureMainMenuEvent extends Event
|
||||
{
|
||||
const CONFIGURE = 'app.main_menu_configure';
|
||||
|
||||
private $factory;
|
||||
private $menu;
|
||||
|
||||
/**
|
||||
* @param \Knp\Menu\FactoryInterface $factory
|
||||
* @param \Knp\Menu\ItemInterface $menu
|
||||
*/
|
||||
public function __construct(FactoryInterface $factory, ItemInterface $menu)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
$this->menu = $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Knp\Menu\FactoryInterface
|
||||
*/
|
||||
public function getFactory()
|
||||
{
|
||||
return $this->factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Knp\Menu\ItemInterface
|
||||
*/
|
||||
public function getMenu()
|
||||
{
|
||||
return $this->menu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\EventListener;
|
||||
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* When visiting the homepage, this listener redirects the user to the most
|
||||
* appropriate localized version according to the browser settings.
|
||||
*
|
||||
* See http://symfony.com/doc/current/components/http_kernel/introduction.html#the-kernel-request-event
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class RedirectToPreferredLocaleListener
|
||||
{
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $urlGenerator;
|
||||
|
||||
/**
|
||||
* List of supported locales.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $locales = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $defaultLocale = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param UrlGeneratorInterface $urlGenerator
|
||||
* @param string $locales Supported locales separated by '|'
|
||||
* @param string|null $defaultLocale
|
||||
*/
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator, $locales, $defaultLocale = null)
|
||||
{
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
|
||||
$this->locales = explode('|', trim($locales));
|
||||
if (empty($this->locales)) {
|
||||
throw new \UnexpectedValueException('The list of supported locales must not be empty.');
|
||||
}
|
||||
|
||||
$this->defaultLocale = $defaultLocale ?: $this->locales[0];
|
||||
|
||||
if (!in_array($this->defaultLocale, $this->locales)) {
|
||||
throw new \UnexpectedValueException(sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales));
|
||||
}
|
||||
|
||||
// Add the default locale at the first position of the array,
|
||||
// because Symfony\HttpFoundation\Request::getPreferredLanguage
|
||||
// returns the first element when no an appropriate language is found
|
||||
array_unshift($this->locales, $this->defaultLocale);
|
||||
$this->locales = array_unique($this->locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GetResponseEvent $event
|
||||
*/
|
||||
public function onKernelRequest(GetResponseEvent $event)
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
// Ignore sub-requests and all URLs but the homepage
|
||||
if (!$event->isMasterRequest() || '/' !== $request->getPathInfo()) {
|
||||
return;
|
||||
}
|
||||
// Ignore requests from referrers with the same HTTP host in order to prevent
|
||||
// changing language for users who possibly already selected it for this application.
|
||||
if (0 === stripos($request->headers->get('referer'), $request->getSchemeAndHttpHost())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preferredLanguage = $request->getPreferredLanguage($this->locales);
|
||||
|
||||
if ($preferredLanguage !== $this->defaultLocale) {
|
||||
$response = new RedirectResponse($this->urlGenerator->generate('homepage', ['_locale' => $preferredLanguage]));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/AppBundle/Form/CommentType.php
Normal file
58
src/AppBundle/Form/CommentType.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Form;
|
||||
|
||||
use AppBundle\Entity\Comment;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Defines the form used to create and manipulate blog comments. Although in this
|
||||
* case the form is trivial and we could build it inside the controller, a good
|
||||
* practice is to always define your forms as classes.
|
||||
* See http://symfony.com/doc/current/book/forms.html#creating-form-classes
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class CommentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
//
|
||||
// $builder->add('content', null, ['required' => false]);
|
||||
|
||||
$builder
|
||||
->add('content')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Comment::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
76
src/AppBundle/Form/PostType.php
Normal file
76
src/AppBundle/Form/PostType.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Form;
|
||||
|
||||
use AppBundle\Entity\Post;
|
||||
use AppBundle\Form\Type\DateTimePickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Defines the form used to create and manipulate blog posts.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class PostType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
// For the full reference of options defined by each form field type
|
||||
// see http://symfony.com/doc/current/reference/forms/types.html
|
||||
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
//
|
||||
// $builder->add('title', null, ['required' => false, ...]);
|
||||
|
||||
$builder
|
||||
->add('title', null, [
|
||||
'attr' => ['autofocus' => true],
|
||||
'label' => 'label.title',
|
||||
])
|
||||
->add('summary', TextareaType::class, [
|
||||
'label' => 'label.summary',
|
||||
])
|
||||
->add('content', null, [
|
||||
'attr' => ['rows' => 20],
|
||||
'label' => 'label.content',
|
||||
])
|
||||
->add('authorEmail', null, [
|
||||
'label' => 'label.author_email',
|
||||
])
|
||||
->add('publishedAt', DateTimePickerType::class, [
|
||||
'label' => 'label.published_at',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Post::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
src/AppBundle/Form/Type/DateTimePickerType.php
Normal file
66
src/AppBundle/Form/Type/DateTimePickerType.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Form\Type;
|
||||
|
||||
use AppBundle\Utils\MomentFormatConverter;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to manipulate datetime values across
|
||||
* Bootstrap Date\Time Picker javascript plugin.
|
||||
* See http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class DateTimePickerType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var MomentFormatConverter
|
||||
*/
|
||||
private $formatConverter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->formatConverter = new MomentFormatConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['attr']['data-date-format'] = $this->formatConverter->convert($options['format']);
|
||||
$view->vars['attr']['data-date-locale'] = \Locale::getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateTimeType::class;
|
||||
}
|
||||
}
|
||||
129
src/AppBundle/Menu/MenuBuilder.php
Normal file
129
src/AppBundle/Menu/MenuBuilder.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Menu;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use AppBundle\Event\ConfigureMainMenuEvent;
|
||||
use AppBundle\Event\ConfigureAdminMenuEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
|
||||
|
||||
/**
|
||||
* Class MenuBuilder
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class MenuBuilder
|
||||
{
|
||||
/**
|
||||
* @var FactoryInterface
|
||||
*/
|
||||
private $factory;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
/**
|
||||
* @var AuthorizationChecker
|
||||
*/
|
||||
private $security;
|
||||
|
||||
/**
|
||||
* MenuBuilder constructor.
|
||||
* @param FactoryInterface $factory
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param AuthorizationChecker $security
|
||||
*/
|
||||
public function __construct(FactoryInterface $factory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
AuthorizationChecker $security)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->security = $security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the main menu.
|
||||
*
|
||||
* @param array $options
|
||||
* @return \Knp\Menu\ItemInterface
|
||||
*/
|
||||
public function createMainMenu(array $options)
|
||||
{
|
||||
$menu = $this->factory->createItem('main');
|
||||
$menu->setChildrenAttribute('class', 'nav navbar-nav navbar-right');
|
||||
|
||||
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_FULLY');
|
||||
$isAdmin = $isLoggedIn && $this->security->isGranted('ROLE_ADMIN');
|
||||
|
||||
if ($isLoggedIn) {
|
||||
$item = $menu->addChild('Homepage', array('route' => 'blog_index'));
|
||||
$item->setLabel('menu.homepage');
|
||||
$item->setChildrenAttribute('icon', 'home');
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureMainMenuEvent::CONFIGURE,
|
||||
new ConfigureMainMenuEvent($this->factory, $menu)
|
||||
);
|
||||
|
||||
if ($isAdmin) {
|
||||
$item = $menu->addChild('Administration', array('route' => 'admin_post_index'));
|
||||
$item->setLabel('menu.admin');
|
||||
$item->setChildrenAttribute('icon', 'lock');
|
||||
}
|
||||
|
||||
if ($isLoggedIn) {
|
||||
$item = $menu->addChild('Logout', array('route' => 'security_logout'));
|
||||
$item->setLabel('menu.logout');
|
||||
$item->setChildrenAttribute('icon', 'sign-out');
|
||||
}
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the admin menu.
|
||||
*
|
||||
* @param array $options
|
||||
* @return \Knp\Menu\ItemInterface
|
||||
*/
|
||||
public function createAdminMenu(array $options)
|
||||
{
|
||||
$menu = $this->factory->createItem('admin');
|
||||
$menu->setChildrenAttribute('class', 'nav navbar-nav navbar-right');
|
||||
|
||||
$isLoggedIn = $this->security->isGranted('IS_AUTHENTICATED_FULLY');
|
||||
$isAdmin = $isLoggedIn && $this->security->isGranted('ROLE_ADMIN');
|
||||
|
||||
// FIXME CAN BE REMOVED
|
||||
if ($isAdmin) {
|
||||
$item = $menu->addChild('Post list', array('route' => 'admin_post_index'));
|
||||
$item->setLabel('menu.post_list');
|
||||
$item->setChildrenAttribute('icon', 'list-alt');
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent($this->factory, $menu)
|
||||
);
|
||||
|
||||
if ($isLoggedIn) {
|
||||
$item = $menu->addChild('Logout', array('route' => 'security_logout'));
|
||||
$item->setLabel('menu.logout');
|
||||
$item->setChildrenAttribute('icon', 'sign-out');
|
||||
}
|
||||
|
||||
return $menu;
|
||||
}
|
||||
}
|
||||
61
src/AppBundle/Repository/PostRepository.php
Normal file
61
src/AppBundle/Repository/PostRepository.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Repository;
|
||||
|
||||
use AppBundle\Entity\Post;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* This custom Doctrine repository contains some methods which are useful when
|
||||
* querying for blog post information.
|
||||
* See http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class PostRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @return Query
|
||||
*/
|
||||
public function queryLatest()
|
||||
{
|
||||
return $this->getEntityManager()
|
||||
->createQuery('
|
||||
SELECT p
|
||||
FROM AppBundle:Post p
|
||||
WHERE p.publishedAt <= :now
|
||||
ORDER BY p.publishedAt DESC
|
||||
')
|
||||
->setParameter('now', new \DateTime())
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
public function findLatest($page = 1)
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($this->queryLatest(), false));
|
||||
$paginator->setMaxPerPage(Post::NUM_ITEMS);
|
||||
$paginator->setCurrentPage($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
29
src/AppBundle/Repository/UserRepository.php
Normal file
29
src/AppBundle/Repository/UserRepository.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* This custom Doctrine repository is empty because so far we don't need any custom
|
||||
* method to query for application user information. But it's always a good practice
|
||||
* to define a custom repository that will be used when the application grows.
|
||||
* See http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class UserRepository extends EntityRepository
|
||||
{
|
||||
}
|
||||
75
src/AppBundle/Tests/Controller/Admin/BlogControllerTest.php
Normal file
75
src/AppBundle/Tests/Controller/Admin/BlogControllerTest.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Tests\Controller\Admin;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Functional test for the controllers defined inside the BlogController used
|
||||
* for managing the blog in the backend.
|
||||
* See http://symfony.com/doc/current/book/testing.html#functional-tests
|
||||
*
|
||||
* Whenever you test resources protected by a firewall, consider using the
|
||||
* technique explained in:
|
||||
* http://symfony.com/doc/current/cookbook/testing/http_authentication.html
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
*/
|
||||
class BlogControllerTest extends WebTestCase
|
||||
{
|
||||
public function testRegularUsersCannotAccessToTheBackend()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'john_user',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
|
||||
$client->request('GET', '/en/admin/post/');
|
||||
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAdministratorUsersCanAccessToTheBackend()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'anna_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
|
||||
$client->request('GET', '/en/admin/post/');
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testIndex()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'anna_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
|
||||
$crawler = $client->request('GET', '/en/admin/post/');
|
||||
|
||||
$this->assertCount(
|
||||
30,
|
||||
$crawler->filter('body#admin_post_index #main tbody tr'),
|
||||
'The backend homepage displays all the available posts.'
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/AppBundle/Tests/Controller/BlogControllerTest.php
Normal file
42
src/AppBundle/Tests/Controller/BlogControllerTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use AppBundle\Entity\Post;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Functional test for the controllers defined inside BlogController.
|
||||
* See http://symfony.com/doc/current/book/testing.html#functional-tests
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
*/
|
||||
class BlogControllerTest extends WebTestCase
|
||||
{
|
||||
public function testIndex()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$crawler = $client->request('GET', '/en/blog/');
|
||||
|
||||
$this->assertCount(
|
||||
Post::NUM_ITEMS,
|
||||
$crawler->filter('article.post'),
|
||||
'The homepage displays the right number of posts.'
|
||||
);
|
||||
}
|
||||
}
|
||||
85
src/AppBundle/Tests/Controller/DefaultControllerTest.php
Normal file
85
src/AppBundle/Tests/Controller/DefaultControllerTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Functional test that implements a "smoke test" of all the public and secure
|
||||
* URLs of the application.
|
||||
* See http://symfony.com/doc/current/best_practices/tests.html#functional-tests.
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
*/
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* PHPUnit's data providers allow to execute the same tests repeated times
|
||||
* using a different set of data each time.
|
||||
* See http://symfony.com/doc/current/cookbook/form/unit_testing.html#testing-against-different-sets-of-data.
|
||||
*
|
||||
* @dataProvider getPublicUrls
|
||||
*/
|
||||
public function testPublicUrls($url)
|
||||
{
|
||||
$client = self::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$this->assertTrue(
|
||||
$client->getResponse()->isSuccessful(),
|
||||
sprintf('The %s public URL loads correctly.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The application contains a lot of secure URLs which shouldn't be
|
||||
* publicly accessible. This tests ensures that whenever a user tries to
|
||||
* access one of those pages, a redirection to the login form is performed.
|
||||
*
|
||||
* @dataProvider getSecureUrls
|
||||
*/
|
||||
public function testSecureUrls($url)
|
||||
{
|
||||
$client = self::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$this->assertTrue($client->getResponse()->isRedirect());
|
||||
|
||||
$this->assertEquals(
|
||||
'http://localhost/en/login',
|
||||
$client->getResponse()->getTargetUrl(),
|
||||
sprintf('The %s secure URL redirects to the login form.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
public function getPublicUrls()
|
||||
{
|
||||
yield ['/'];
|
||||
yield ['/en/blog/'];
|
||||
yield ['/en/blog/posts/morbi-tempus-commodo-mattis'];
|
||||
yield ['/en/login'];
|
||||
}
|
||||
|
||||
public function getSecureUrls()
|
||||
{
|
||||
yield ['/en/admin/post/'];
|
||||
yield ['/en/admin/post/new'];
|
||||
yield ['/en/admin/post/1'];
|
||||
yield ['/en/admin/post/1/edit'];
|
||||
}
|
||||
}
|
||||
49
src/AppBundle/Tests/Utils/SluggerTest.php
Normal file
49
src/AppBundle/Tests/Utils/SluggerTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tests\Utils;
|
||||
|
||||
use AppBundle\Utils\Slugger;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* Unit test for the application utils.
|
||||
* See http://symfony.com/doc/current/book/testing.html#unit-tests
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
*/
|
||||
class SluggerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getSlugs
|
||||
*/
|
||||
public function testSlugify($string, $slug)
|
||||
{
|
||||
$slugger = new Slugger();
|
||||
$result = $slugger->slugify($string);
|
||||
|
||||
$this->assertEquals($slug, $result);
|
||||
}
|
||||
|
||||
public function getSlugs()
|
||||
{
|
||||
yield ['Lorem Ipsum' , 'lorem-ipsum'];
|
||||
yield [' Lorem Ipsum ' , 'lorem-ipsum'];
|
||||
yield [' lOrEm iPsUm ' , 'lorem-ipsum'];
|
||||
yield ['!Lorem Ipsum!' , 'lorem-ipsum'];
|
||||
yield ['lorem-ipsum' , 'lorem-ipsum'];
|
||||
}
|
||||
}
|
||||
118
src/AppBundle/Twig/Extensions.php
Normal file
118
src/AppBundle/Twig/Extensions.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Twig;
|
||||
|
||||
use AppBundle\Utils\Markdown;
|
||||
use Symfony\Component\Intl\Intl;
|
||||
|
||||
/**
|
||||
* This Twig extension adds new filters:
|
||||
* - 'md2html' to transform Markdown contents into HTML contents
|
||||
* - 'gmdate' to transform timestamps into a gmdate() formatted string
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class Extensions extends \Twig_Extension
|
||||
{
|
||||
/**
|
||||
* @var Markdown
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $locales;
|
||||
|
||||
/**
|
||||
* Extensions constructor.
|
||||
* @param Markdown $parser
|
||||
* @param $locales
|
||||
*/
|
||||
public function __construct(Markdown $parser, $locales)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->locales = $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
|
||||
new \Twig_SimpleFilter('gmdate', array($this, 'gmdate')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new \Twig_SimpleFunction('locales', [$this, 'getLocales']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a timestamp into a gmdate() formatted string.
|
||||
*
|
||||
* @param $seconds
|
||||
* @param string $format
|
||||
* @return false|string
|
||||
*/
|
||||
public function gmdate($seconds, $format = 'H:i')
|
||||
{
|
||||
return gmdate($format, $seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the given Markdown content into HTML content.
|
||||
*
|
||||
* @param string $content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function markdownToHtml($content)
|
||||
{
|
||||
return $this->parser->toHtml($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the list of codes of the locales (languages) enabled in the
|
||||
* application and returns an array with the name of each locale written
|
||||
* in its own language (e.g. English, Français, Español, etc.)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLocales()
|
||||
{
|
||||
$localeCodes = explode('|', $this->locales);
|
||||
|
||||
$locales = [];
|
||||
foreach ($localeCodes as $localeCode) {
|
||||
$locales[] = ['code' => $localeCode, 'name' => Intl::getLocaleBundle()->getLocaleName($localeCode, $localeCode)];
|
||||
}
|
||||
|
||||
return $locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'kimai.extension';
|
||||
}
|
||||
}
|
||||
56
src/AppBundle/Utils/Markdown.php
Normal file
56
src/AppBundle/Utils/Markdown.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Utils;
|
||||
|
||||
/**
|
||||
* This class is a light interface between an external Markdown parser library
|
||||
* and the application. It's generally recommended to create these light interfaces
|
||||
* to decouple your application from the implementation details of the third-party library.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Markdown
|
||||
{
|
||||
/**
|
||||
* @var \Parsedown
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* @var \HTMLPurifier
|
||||
*/
|
||||
private $purifier;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->parser = new \Parsedown();
|
||||
|
||||
$purifierConfig = \HTMLPurifier_Config::create([
|
||||
'Cache.DefinitionImpl' => null, // Disable caching
|
||||
]);
|
||||
$this->purifier = new \HTMLPurifier($purifierConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHtml($text)
|
||||
{
|
||||
$html = $this->parser->text($text);
|
||||
$safeHtml = $this->purifier->purify($html);
|
||||
|
||||
return $safeHtml;
|
||||
}
|
||||
}
|
||||
52
src/AppBundle/Utils/MomentFormatConverter.php
Normal file
52
src/AppBundle/Utils/MomentFormatConverter.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Utils;
|
||||
|
||||
/**
|
||||
* This class is used to convert PHP date format to moment.js format
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class MomentFormatConverter
|
||||
{
|
||||
/**
|
||||
* This defines the mapping between PHP ICU date format (key) and moment.js date format (value)
|
||||
* For ICU formats see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
|
||||
* For Moment formats see http://momentjs.com/docs/#/displaying/format/
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $formatConvertRules = [
|
||||
// year
|
||||
'yyyy' => 'YYYY', 'yy' => 'YY', 'y' => 'YYYY',
|
||||
// day
|
||||
'dd' => 'DD', 'd' => 'D',
|
||||
// day of week
|
||||
'EE' => 'ddd', 'EEEEEE' => 'dd',
|
||||
// timezone
|
||||
'ZZZZZ' => 'Z', 'ZZZ' => 'ZZ',
|
||||
// letter 'T'
|
||||
'\'T\'' => 'T',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns associated moment.js format.
|
||||
*
|
||||
* @param string $format PHP Date format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function convert($format)
|
||||
{
|
||||
return strtr($format, self::$formatConvertRules);
|
||||
}
|
||||
}
|
||||
34
src/AppBundle/Utils/Slugger.php
Normal file
34
src/AppBundle/Utils/Slugger.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Utils;
|
||||
|
||||
/**
|
||||
* FIXME CAN BE REMOVED
|
||||
*
|
||||
* This class is used to provide an example of integrating simple classes as
|
||||
* services into a Symfony application.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Slugger
|
||||
{
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function slugify($string)
|
||||
{
|
||||
return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower(strip_tags($string))), '-');
|
||||
}
|
||||
}
|
||||
43
src/TimesheetBundle/Controller/TimesheetController.php
Normal file
43
src/TimesheetBundle/Controller/TimesheetController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\Controller;
|
||||
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
|
||||
/**
|
||||
* Controller used to manage timesheet contents in the public part of the site.
|
||||
*
|
||||
* @Route("/timesheet")
|
||||
* @Security("has_role('ROLE_CUSTOMER')")
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetController extends Controller
|
||||
{
|
||||
/**
|
||||
* @Route("/", defaults={"page": 1}, name="timesheet")
|
||||
* @Route("/timesheet/{page}", requirements={"page": "[1-9]\d*"}, name="timesheet_paginated")
|
||||
* @Method("GET")
|
||||
* @Cache(smaxage="10")
|
||||
*/
|
||||
public function indexAction($page)
|
||||
{
|
||||
$entries = $this->getDoctrine()->getRepository(Timesheet::class)->findLatest($page);
|
||||
|
||||
return $this->render('TimesheetBundle:timesheet:index.html.twig', ['entries' => $entries]);
|
||||
}
|
||||
}
|
||||
105
src/TimesheetBundle/DataFixtures/ORM/LoadFixtures.php
Normal file
105
src/TimesheetBundle/DataFixtures/ORM/LoadFixtures.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\DataFixtures\ORM;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use Doctrine\Common\DataFixtures\FixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use AppBundle\DataFixtures\ORM\LoadFixtures as AppBundleLoadFixtures;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests. Execute this command to load the data:
|
||||
*
|
||||
* $ php bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class LoadFixtures extends AppBundleLoadFixtures
|
||||
{
|
||||
const AMOUNT_ACTIVITIES = 20;
|
||||
const AMOUNT_TIMESHEET = 1000;
|
||||
const AMOUNT_PROJECTS = 10;
|
||||
const AMOUNT_CUSTOMER = 10;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$this->loadTimesheet($manager);
|
||||
}
|
||||
|
||||
private function loadTimesheet(ObjectManager $manager)
|
||||
{
|
||||
$amountUsers = count($manager->getRepository(User::class)->findAll());
|
||||
|
||||
for ($i = 0; $i <= self::AMOUNT_TIMESHEET; $i++) {
|
||||
|
||||
$start = new \DateTime();
|
||||
$start = $start->modify('- ' . (rand(1, 400)) . ' days');
|
||||
$start = $start->modify('- ' . (rand(1, 86400)) . ' seconds');
|
||||
$end = clone $start;
|
||||
$end = $end->modify('+ '.(rand(1, 43200)).' seconds');
|
||||
|
||||
$entry = new Timesheet();
|
||||
$entry->setProjectid(rand(1, self::AMOUNT_PROJECTS));
|
||||
$entry->setActivityid(rand(1, self::AMOUNT_ACTIVITIES));
|
||||
$entry->setStatusid(1); // TODO
|
||||
$entry->setBillable(true);
|
||||
$entry->setBudget(0);
|
||||
$entry->setCleared(false);
|
||||
$entry->setComment($this->getRandomPhrase());
|
||||
$entry->setDescription($this->getRandomPhrase());
|
||||
$entry->setLocation($this->getRandomLocation());
|
||||
$entry->setStart($start->getTimestamp());
|
||||
$entry->setEnd($end->getTimestamp());
|
||||
$entry->setDuration($end->modify('- ' . $start->getTimestamp() . ' seconds')->getTimestamp());
|
||||
$entry->setUserid(rand(1, $amountUsers));
|
||||
//$entry->setApproved(false); // TODO
|
||||
//$entry->setFixedrate(); // TODO
|
||||
//$entry->setRate(); // TODO
|
||||
//$entry->setTrackingnumber(); // TODO
|
||||
|
||||
$manager->persist($entry);
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function getLocations()
|
||||
{
|
||||
return [
|
||||
'Köln',
|
||||
'München',
|
||||
'New York',
|
||||
'Buenos Aires',
|
||||
'Hawai',
|
||||
'Amsterdam',
|
||||
'London',
|
||||
'San Francisco',
|
||||
'Tokio',
|
||||
'Berlin',
|
||||
'Sao Paulo',
|
||||
'Mexico City',
|
||||
];
|
||||
}
|
||||
|
||||
private function getRandomLocation()
|
||||
{
|
||||
$titles = $this->getLocations();
|
||||
|
||||
return $titles[array_rand($titles)];
|
||||
}
|
||||
}
|
||||
604
src/TimesheetBundle/Entity/Timesheet.php
Normal file
604
src/TimesheetBundle/Entity/Timesheet.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Timesheet entity.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="TimesheetBundle\Repository\TimesheetRepository")
|
||||
* @ORM\Table(name="timeSheet", indexes={@ORM\Index(name="userID", columns={"userID"}), @ORM\Index(name="projectID", columns={"projectID"}), @ORM\Index(name="activityID", columns={"activityID"})})
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class Timesheet
|
||||
{
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="start", type="integer", nullable=false)
|
||||
*/
|
||||
private $start = '0';
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="end", type="integer", nullable=false)
|
||||
*/
|
||||
private $end = '0';
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="duration", type="integer", nullable=false)
|
||||
*/
|
||||
private $duration = '0';
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="userID", type="integer", nullable=false)
|
||||
*/
|
||||
private $userid;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="projectID", type="integer", nullable=false)
|
||||
*/
|
||||
private $projectid;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="activityID", type="integer", nullable=false)
|
||||
*/
|
||||
private $activityid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="description", type="text", length=65535, nullable=true)
|
||||
*/
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", length=65535, nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*
|
||||
* @ORM\Column(name="commentType", type="boolean", nullable=false)
|
||||
*/
|
||||
private $commenttype = '0';
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*
|
||||
* @ORM\Column(name="cleared", type="boolean", nullable=false)
|
||||
*/
|
||||
private $cleared = '0';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="location", type="string", length=50, nullable=true)
|
||||
*/
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="trackingNumber", type="string", length=30, nullable=true)
|
||||
*/
|
||||
private $trackingnumber;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="rate", type="decimal", precision=10, scale=2, nullable=false)
|
||||
*/
|
||||
private $rate = '0.00';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="fixedRate", type="decimal", precision=10, scale=2, nullable=false)
|
||||
*/
|
||||
private $fixedrate = '0.00';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="budget", type="decimal", precision=10, scale=2, nullable=true)
|
||||
*/
|
||||
private $budget;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="approved", type="decimal", precision=10, scale=2, nullable=true)
|
||||
*/
|
||||
private $approved;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="statusID", type="smallint", nullable=false)
|
||||
*/
|
||||
private $statusid;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=true)
|
||||
*/
|
||||
private $billable;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Column(name="timeEntryID", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $timeentryid;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set start
|
||||
*
|
||||
* @param integer $start
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setStart($start)
|
||||
{
|
||||
$this->start = $start;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getStart()
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set end
|
||||
*
|
||||
* @param integer $end
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setEnd($end)
|
||||
{
|
||||
$this->end = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getEnd()
|
||||
{
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set duration
|
||||
*
|
||||
* @param integer $duration
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setDuration($duration)
|
||||
{
|
||||
$this->duration = $duration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get duration
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getDuration()
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userid
|
||||
*
|
||||
* @param integer $userid
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setUserid($userid)
|
||||
{
|
||||
$this->userid = $userid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userid
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getUserid()
|
||||
{
|
||||
return $this->userid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set projectid
|
||||
*
|
||||
* @param integer $projectid
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setProjectid($projectid)
|
||||
{
|
||||
$this->projectid = $projectid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projectid
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getProjectid()
|
||||
{
|
||||
return $this->projectid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set activityid
|
||||
*
|
||||
* @param integer $activityid
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setActivityid($activityid)
|
||||
{
|
||||
$this->activityid = $activityid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activityid
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getActivityid()
|
||||
{
|
||||
return $this->activityid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set comment
|
||||
*
|
||||
* @param string $comment
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setComment($comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getComment()
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set commenttype
|
||||
*
|
||||
* @param boolean $commenttype
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setCommenttype($commenttype)
|
||||
{
|
||||
$this->commenttype = $commenttype;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commenttype
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getCommenttype()
|
||||
{
|
||||
return $this->commenttype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cleared
|
||||
*
|
||||
* @param boolean $cleared
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setCleared($cleared)
|
||||
{
|
||||
$this->cleared = $cleared;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cleared
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getCleared()
|
||||
{
|
||||
return $this->cleared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set location
|
||||
*
|
||||
* @param string $location
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setLocation($location)
|
||||
{
|
||||
$this->location = $location;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get location
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocation()
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set trackingnumber
|
||||
*
|
||||
* @param string $trackingnumber
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setTrackingnumber($trackingnumber)
|
||||
{
|
||||
$this->trackingnumber = $trackingnumber;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trackingnumber
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTrackingnumber()
|
||||
{
|
||||
return $this->trackingnumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set rate
|
||||
*
|
||||
* @param string $rate
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setRate($rate)
|
||||
{
|
||||
$this->rate = $rate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRate()
|
||||
{
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fixedrate
|
||||
*
|
||||
* @param string $fixedrate
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setFixedrate($fixedrate)
|
||||
{
|
||||
$this->fixedrate = $fixedrate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fixedrate
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFixedrate()
|
||||
{
|
||||
return $this->fixedrate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set budget
|
||||
*
|
||||
* @param string $budget
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setBudget($budget)
|
||||
{
|
||||
$this->budget = $budget;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get budget
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBudget()
|
||||
{
|
||||
return $this->budget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set approved
|
||||
*
|
||||
* @param string $approved
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setApproved($approved)
|
||||
{
|
||||
$this->approved = $approved;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approved
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getApproved()
|
||||
{
|
||||
return $this->approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set statusid
|
||||
*
|
||||
* @param integer $statusid
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setStatusid($statusid)
|
||||
{
|
||||
$this->statusid = $statusid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statusid
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getStatusid()
|
||||
{
|
||||
return $this->statusid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set billable
|
||||
*
|
||||
* @param boolean $billable
|
||||
*
|
||||
* @return KimaiTimesheet
|
||||
*/
|
||||
public function setBillable($billable)
|
||||
{
|
||||
$this->billable = $billable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get billable
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getBillable()
|
||||
{
|
||||
return $this->billable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeentryid
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getTimeentryid()
|
||||
{
|
||||
return $this->timeentryid;
|
||||
}
|
||||
}
|
||||
47
src/TimesheetBundle/EventListener/Menu.php
Normal file
47
src/TimesheetBundle/EventListener/Menu.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\EventListener;
|
||||
|
||||
use AppBundle\Event\ConfigureMainMenuEvent;
|
||||
use AppBundle\Event\ConfigureAdminMenuEvent;
|
||||
|
||||
/**
|
||||
* Class Menu
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class Menu
|
||||
{
|
||||
/**
|
||||
* @param \AppBundle\Event\ConfigureMainMenuEvent $event
|
||||
*/
|
||||
public function onMainMenuConfigure(ConfigureMainMenuEvent $event)
|
||||
{
|
||||
$menu = $event->getMenu();
|
||||
|
||||
$item = $menu->addChild('Timesheet', array('route' => 'timesheet'));
|
||||
$item->setLabel('menu.timesheet');
|
||||
$item->setChildrenAttribute('icon', 'clock-o');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \AppBundle\Event\ConfigureAdminMenuEvent $event
|
||||
*/
|
||||
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
|
||||
{
|
||||
$menu = $event->getMenu();
|
||||
|
||||
$item = $menu->addChild('TimeAdmin', array('route' => 'timesheet'));
|
||||
//$item->setLabel('menu.admin_timesheet');
|
||||
$item->setChildrenAttribute('icon', 'clock-o');
|
||||
}
|
||||
}
|
||||
54
src/TimesheetBundle/Repository/TimesheetRepository.php
Normal file
54
src/TimesheetBundle/Repository/TimesheetRepository.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle\Repository;
|
||||
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* Class TimesheetRepository
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetRepository extends EntityRepository
|
||||
{
|
||||
|
||||
/**
|
||||
* @return Query
|
||||
*/
|
||||
public function queryLatest()
|
||||
{
|
||||
return $this->getEntityManager()
|
||||
->createQuery('
|
||||
SELECT p
|
||||
FROM TimesheetBundle:Timesheet p
|
||||
ORDER BY p.start DESC
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
public function findLatest($page = 1)
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($this->queryLatest(), false));
|
||||
$paginator->setMaxPerPage(25);
|
||||
$paginator->setCurrentPage($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body_id 'timesheet_index' %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if entries %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><i class="fa fa-calendar"></i> {{ 'label.date'|trans }}</th>
|
||||
<th><i class="fa fa-circle-o"></i> {{ 'label.start'|trans }}</th>
|
||||
<th><i class="fa fa-circle"></i> {{ 'label.end'|trans }}</th>
|
||||
<th><i class="fa fa-clock-o"></i> {{ 'label.duration'|trans }}</th>
|
||||
<th><i class="fa fa-user"></i> {{ 'label.user'|trans }}</th>
|
||||
<th>{{ 'label.description'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td>{{ entry.start|date("m/d/Y") }}</td>
|
||||
<td>{{ entry.start|date("H:i") }}</td>
|
||||
<td>{{ entry.end|date("H:i") }}</td>
|
||||
<td>{{ entry.duration|gmdate }}</td>
|
||||
<td>{{ entry.userid }}</td>
|
||||
<td>{{ entry.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="well">{{ 'post.no_posts_found'|trans }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="navigation text-center">
|
||||
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { routeName: 'timesheet_paginated' }) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
23
src/TimesheetBundle/TimesheetBundle.php
Normal file
23
src/TimesheetBundle/TimesheetBundle.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai package.
|
||||
*
|
||||
* (c) Kevin Papst <kevin@kevinpapst.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace TimesheetBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
/**
|
||||
* This class defines the Bundle for all Timesheet related topics
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetBundle extends Bundle
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user