Merge branch 'feature/blog-admin' into develop
This commit is contained in:
commit
1c545bfb63
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
|||
/.env.*.local
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/src/Command/TestCommand.php
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
|
|
@ -181,7 +181,7 @@ tr.table-primary-light {
|
|||
}
|
||||
|
||||
.body {
|
||||
padding-top: 71px;
|
||||
padding-top: 60px;
|
||||
|
||||
.nav {
|
||||
padding-left: 10px;
|
||||
|
@ -213,12 +213,12 @@ table.table-fixed, .table-fixed > table {
|
|||
height: 500px;
|
||||
}
|
||||
|
||||
thead, tbody, tr, td, th{
|
||||
thead, tbody, tr, td, th {
|
||||
display: block;
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
td, th {
|
||||
float: left;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
|
BIN
assets/img/blank.png
Normal file
BIN
assets/img/blank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 B |
BIN
assets/img/no-image.png
Normal file
BIN
assets/img/no-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -5,7 +5,7 @@ module.exports = function() {
|
|||
|
||||
tinymce.init({
|
||||
selector: '*[data-tinymce]',
|
||||
base_url: '/nm/tinymce/',
|
||||
base_url: '/vendor/tinymce/',
|
||||
cache_suffix: '?v=4.1.6',
|
||||
language: 'fr_FR',
|
||||
plugins: 'print preview importcss searchreplace visualblocks visualchars fullscreen template table charmap hr pagebreak nonbreaking toc insertdatetime advlist lists wordcount textpattern noneditable help charmap quickbars',
|
||||
|
|
|
@ -9,7 +9,7 @@ import '../css/admin.scss';
|
|||
|
||||
require('../../node_modules/bootstrap/dist/js/bootstrap.min.js');
|
||||
// require('./addons/table-selectable.js')();
|
||||
// require('./addons/table-fixed.js')();
|
||||
require('./addons/table-fixed.js')();
|
||||
// require('./addons/document-selector.js')();
|
||||
require('./addons/form-confirm.js')();
|
||||
require('./addons/form.js')();
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"symfony/dotenv": "5.2.*",
|
||||
"symfony/event-dispatcher": "5.2.*",
|
||||
"symfony/expression-language": "5.2.*",
|
||||
"symfony/finder": "5.2.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/form": "5.2.*",
|
||||
"symfony/framework-bundle": "5.2.*",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
form_themes: ['bootstrap_4_layout.html.twig']
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"jquery": "^3.6.0",
|
||||
"popper.js": "^1.16.0",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"tinymce": "^5.2.0",
|
||||
"tinymce": "^5.7.1",
|
||||
"vanillajs-datepicker": "^1.1.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
}
|
||||
|
|
BIN
public/uploads/post/2021/20210317-035213ovh_feu1.jpg
Normal file
BIN
public/uploads/post/2021/20210317-035213ovh_feu1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
public/vendor/tinymce
vendored
Symbolic link
1
public/vendor/tinymce
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../node_modules/tinymce
|
|
@ -68,7 +68,7 @@ class AccountAdminController extends AdminController
|
|||
} else {
|
||||
$this->addFlash('success', 'Double authentification activée.');
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
$entityManager->update($account);
|
||||
|
||||
return $this->redirectToRoute('admin_account');
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ class AccountAdminController extends AdminController
|
|||
if (!$enable && $account->isTotpAuthenticationEnabled()) {
|
||||
$account->setTotpSecret(null);
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
$entityManager->update($account);
|
||||
|
||||
$this->addFlash('success', 'Double authentification désactivée.');
|
||||
|
||||
|
@ -100,7 +100,8 @@ class AccountAdminController extends AdminController
|
|||
Request $request,
|
||||
UserRepository $repository,
|
||||
TokenGeneratorInterface $tokenGenerator,
|
||||
UserPasswordEncoderInterface $encoder
|
||||
UserPasswordEncoderInterface $encoder,
|
||||
EntityManager $entityManager
|
||||
): Response {
|
||||
$account = $this->getUser();
|
||||
$csrfToken = $request->request->get('_csrf_token');
|
||||
|
@ -129,10 +130,7 @@ class AccountAdminController extends AdminController
|
|||
->setConfirmationToken($tokenGenerator->generateToken())
|
||||
;
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->persist($account);
|
||||
$entityManager->flush();
|
||||
$entityManager->clear();
|
||||
$entityManager->update($account);
|
||||
|
||||
$this->addFlash('success', 'Mot de passe modifié !');
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ class AuthController extends AbstractController
|
|||
$account->setConfirmationToken($tokenGenerator->generateToken());
|
||||
$account->setPasswordRequestedAt(new \DateTime('now'));
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
$entityManager->update($account);
|
||||
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
|
||||
|
||||
$emailSent = true;
|
||||
|
@ -135,7 +135,7 @@ class AuthController extends AbstractController
|
|||
->setPasswordRequestedAt(new \DateTime('now'))
|
||||
;
|
||||
|
||||
$entityManager->update($account)->flush()->clear();
|
||||
$entityManager->update($account);
|
||||
|
||||
$passwordUpdated = true;
|
||||
}
|
||||
|
|
123
src/Controller/Blog/CategoryAdminController.php
Normal file
123
src/Controller/Blog/CategoryAdminController.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Blog;
|
||||
|
||||
use App\Controller\Admin\AdminController;
|
||||
use App\Entity\Blog\Category as Entity;
|
||||
use App\Factory\Blog\CategoryFactory as EntityFactory;
|
||||
use App\Form\Blog\CategoryType as EntityType;
|
||||
use App\Manager\EntityManager;
|
||||
use App\Repository\Blog\CategoryRepositoryQuery as RepositoryQuery;
|
||||
use App\Repository\Blog\PostRepositoryQuery;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/admin/blog/category")
|
||||
*/
|
||||
class CategoryAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/{page}", name="admin_blog_category_index", requirements={"page": "\d+"})
|
||||
*/
|
||||
public function index(int $page = 1, RepositoryQuery $query, Request $request): Response
|
||||
{
|
||||
$pager = $query->paginate($page);
|
||||
|
||||
return $this->render('blog/category_admin/index.html.twig', [
|
||||
'pager' => $pager,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="admin_blog_category_new")
|
||||
*/
|
||||
public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
$entity = $factory->create();
|
||||
$form = $this->createForm(EntityType::class, $entity);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager->create($entity);
|
||||
$this->addFlash('success', 'Donnée enregistrée.');
|
||||
|
||||
return $this->redirectToRoute('admin_blog_category_edit', [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'Le formulaire est invalide.');
|
||||
}
|
||||
|
||||
return $this->render('blog/category_admin/new.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/edit/{entity}", name="admin_blog_category_edit")
|
||||
*/
|
||||
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(EntityType::class, $entity);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager->update($entity);
|
||||
$this->addFlash('success', 'Donnée enregistrée.');
|
||||
|
||||
return $this->redirectToRoute('admin_blog_category_edit', [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'Le formulaire est invalide.');
|
||||
}
|
||||
|
||||
return $this->render('blog/category_admin/edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/show/{entity}", name="admin_blog_category_show")
|
||||
*/
|
||||
public function show(Entity $entity, PostRepositoryQuery $postQuery): Response
|
||||
{
|
||||
$posts = $postQuery->create()
|
||||
->orderBy('.publishedAt', 'DESC')
|
||||
->inCategory($entity)
|
||||
->paginate(1, 10)
|
||||
;
|
||||
|
||||
return $this->render('blog/category_admin/show.html.twig', [
|
||||
'entity' => $entity,
|
||||
'posts' => $posts,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/delete/{entity}", name="admin_blog_category_delete", methods={"DELETE"})
|
||||
*/
|
||||
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||
$entityManager->delete($entity);
|
||||
|
||||
$this->addFlash('success', 'Données supprimées.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_blog_category_index');
|
||||
}
|
||||
|
||||
public function getSection(): string
|
||||
{
|
||||
return 'blog_category';
|
||||
}
|
||||
}
|
125
src/Controller/Blog/PostAdminController.php
Normal file
125
src/Controller/Blog/PostAdminController.php
Normal file
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Blog;
|
||||
|
||||
use App\Controller\Admin\AdminController;
|
||||
use App\Entity\Blog\Post as Entity;
|
||||
use App\Factory\Blog\PostFactory as EntityFactory;
|
||||
use App\Form\Blog\PostType as EntityType;
|
||||
use App\Form\FileUploadHandler;
|
||||
use App\Manager\EntityManager;
|
||||
use App\Repository\Blog\PostRepositoryQuery;
|
||||
use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/admin/blog/post")
|
||||
*/
|
||||
class PostAdminController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @Route("/{page}", name="admin_blog_post_index", requirements={"page": "\d+"})
|
||||
*/
|
||||
public function index(int $page = 1, RepositoryQuery $query, Request $request): Response
|
||||
{
|
||||
$pager = $query->paginate($page);
|
||||
|
||||
return $this->render('blog/post_admin/index.html.twig', [
|
||||
'pager' => $pager,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/new", name="admin_blog_post_new")
|
||||
*/
|
||||
public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
$entity = $factory->create($this->getUser());
|
||||
$form = $this->createForm(EntityType::class, $entity);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$entityManager->create($entity);
|
||||
$this->addFlash('success', 'Donnée enregistrée.');
|
||||
|
||||
return $this->redirectToRoute('admin_blog_post_edit', [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'Le formulaire est invalide.');
|
||||
}
|
||||
|
||||
return $this->render('blog/post_admin/new.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/edit/{entity}", name="admin_blog_post_edit")
|
||||
*/
|
||||
public function edit(Entity $entity, EntityManager $entityManager, FileUploadHandler $fileUpload, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(EntityType::class, $entity);
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isValid()) {
|
||||
$fileUpload->handleForm(
|
||||
$form->get('image')->getData(),
|
||||
'uploads/post/'.date('Y'),
|
||||
function ($filename) use ($entity) {
|
||||
$entity->setImage('post/'.date('Y').'/'.$filename);
|
||||
}
|
||||
);
|
||||
|
||||
$entityManager->update($entity);
|
||||
$this->addFlash('success', 'Donnée enregistrée.');
|
||||
|
||||
return $this->redirectToRoute('admin_blog_post_edit', [
|
||||
'entity' => $entity->getId(),
|
||||
]);
|
||||
}
|
||||
$this->addFlash('warning', 'Le formulaire est invalide.');
|
||||
}
|
||||
|
||||
return $this->render('blog/post_admin/edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/show/{entity}", name="admin_blog_post_show")
|
||||
*/
|
||||
public function show(Entity $entity, PostRepositoryQuery $postQuery): Response
|
||||
{
|
||||
return $this->render('blog/post_admin/show.html.twig', [
|
||||
'entity' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/delete/{entity}", name="admin_blog_post_delete", methods={"DELETE"})
|
||||
*/
|
||||
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
|
||||
$entityManager->delete($entity);
|
||||
|
||||
$this->addFlash('success', 'Données supprimées.');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('admin_blog_post_index');
|
||||
}
|
||||
|
||||
public function getSection(): string
|
||||
{
|
||||
return 'blog_post';
|
||||
}
|
||||
}
|
57
src/Doctrine/Timestampable.php
Normal file
57
src/Doctrine/Timestampable.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
trait Timestampable
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(name="created_at", type="datetime")
|
||||
*/
|
||||
protected $createdAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="updated_at", type="datetime")
|
||||
*/
|
||||
protected $updatedAt;
|
||||
|
||||
/**
|
||||
* @ORM\PrePersist
|
||||
*/
|
||||
public function onPrePersist(): void
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
$this->updatedAt = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\PreUpdate
|
||||
*/
|
||||
public function onPreUpdate(): void
|
||||
{
|
||||
$this->updatedAt = new \DateTime();
|
||||
}
|
||||
|
||||
public function setCreatedAt(?\DateTime $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(?\DateTime $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?\DateTime
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
}
|
133
src/Entity/Blog/Category.php
Normal file
133
src/Entity/Blog/Category.php
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity\Blog;
|
||||
|
||||
use App\Entity\EntityInterface;
|
||||
use App\Repository\Blog\CategoryRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=CategoryRepository::class)
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class Category implements EntityInterface
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $subTitle;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*/
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=Post::class, mappedBy="categories")
|
||||
*/
|
||||
private $posts;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->posts = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubTitle(): ?string
|
||||
{
|
||||
return $this->subTitle;
|
||||
}
|
||||
|
||||
public function setSubTitle(?string $subTitle): self
|
||||
{
|
||||
$this->subTitle = $subTitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(?string $slug): self
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Post[]
|
||||
*/
|
||||
public function getPosts(): Collection
|
||||
{
|
||||
return $this->posts;
|
||||
}
|
||||
|
||||
public function addPost(Post $post): self
|
||||
{
|
||||
if (!$this->posts->contains($post)) {
|
||||
$this->posts[] = $post;
|
||||
$post->addCategory($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePost(Post $post): self
|
||||
{
|
||||
if ($this->posts->removeElement($post)) {
|
||||
$post->removeCategory($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
239
src/Entity/Blog/Post.php
Normal file
239
src/Entity/Blog/Post.php
Normal file
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity\Blog;
|
||||
|
||||
use App\Doctrine\Timestampable;
|
||||
use App\Entity\EntityInterface;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Blog\PostRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=PostRepository::class)
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class Post implements EntityInterface
|
||||
{
|
||||
use Timestampable;
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*/
|
||||
private $subTitle;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $metaDescription;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $status;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $image;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime", nullable=true)
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $imageCaption;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="posts")
|
||||
*/
|
||||
private $author;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=Category::class, inversedBy="posts")
|
||||
*/
|
||||
private $categories;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->categories = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubTitle(): ?string
|
||||
{
|
||||
return $this->subTitle;
|
||||
}
|
||||
|
||||
public function setSubTitle(?string $subTitle): self
|
||||
{
|
||||
$this->subTitle = $subTitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(?string $content): self
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMetaDescription(): ?string
|
||||
{
|
||||
return $this->metaDescription;
|
||||
}
|
||||
|
||||
public function setMetaDescription(?string $metaDescription): self
|
||||
{
|
||||
$this->metaDescription = $metaDescription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(int $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImage(): ?string
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
public function setImage(?string $image, bool $force = false): self
|
||||
{
|
||||
if (false === $force && null === $image) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->image = $image;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(?string $slug): self
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPublishedAt(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(?\DateTimeInterface $publishedAt): self
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImageCaption(): ?string
|
||||
{
|
||||
return $this->imageCaption;
|
||||
}
|
||||
|
||||
public function setImageCaption(?string $imageCaption): self
|
||||
{
|
||||
$this->imageCaption = $imageCaption;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthor(): ?User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(?User $author): self
|
||||
{
|
||||
$this->author = $author;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Category[]
|
||||
*/
|
||||
public function getCategories(): Collection
|
||||
{
|
||||
return $this->categories;
|
||||
}
|
||||
|
||||
public function addCategory(Category $category): self
|
||||
{
|
||||
if (!$this->categories->contains($category)) {
|
||||
$this->categories[] = $category;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeCategory(Category $category): self
|
||||
{
|
||||
$this->categories->removeElement($category);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
interface Entity
|
||||
interface EntityInterface
|
||||
{
|
||||
}
|
|
@ -2,18 +2,23 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Doctrine\Timestampable;
|
||||
use App\Entity\Blog\Post;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=UserRepository::class)
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
* @ORM\Table(name="`user`")
|
||||
*/
|
||||
class User implements UserInterface, TwoFactorInterface, Entity
|
||||
class User implements UserInterface, TwoFactorInterface, EntityInterface
|
||||
{
|
||||
use Timestampable;
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
|
@ -62,6 +67,16 @@ class User implements UserInterface, TwoFactorInterface, Entity
|
|||
*/
|
||||
private $isAdmin;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=Post::class, mappedBy="author")
|
||||
*/
|
||||
private $posts;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->posts = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
@ -176,16 +191,6 @@ class User implements UserInterface, TwoFactorInterface, Entity
|
|||
return null !== $this->getTotpSecret();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationUsername(): string
|
||||
{
|
||||
return $this->getEmail();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface
|
||||
{
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
public function isGoogleAuthenticatorEnabled(): bool
|
||||
{
|
||||
return $this->isTotpAuthenticationEnabled();
|
||||
|
@ -241,4 +246,34 @@ class User implements UserInterface, TwoFactorInterface, Entity
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Post[]
|
||||
*/
|
||||
public function getPosts(): Collection
|
||||
{
|
||||
return $this->posts;
|
||||
}
|
||||
|
||||
public function addPost(Post $post): self
|
||||
{
|
||||
if (!$this->posts->contains($post)) {
|
||||
$this->posts[] = $post;
|
||||
$post->setAuthor($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePost(Post $post): self
|
||||
{
|
||||
if ($this->posts->removeElement($post)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($post->getAuthor() === $this) {
|
||||
$post->setAuthor(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Event\EntityManager;
|
||||
|
||||
use App\Entity\Entity;
|
||||
use App\Entity\EntityInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
|
@ -16,14 +16,14 @@ class EntityManagerEvent extends Event
|
|||
const UPDATE_EVENT = 'entity_manager_event.update';
|
||||
const DELETE_EVENT = 'entity_manager_event.delete';
|
||||
|
||||
protected Entity $entity;
|
||||
protected EntityInterface $entity;
|
||||
|
||||
public function __construct(Entity $entity)
|
||||
public function __construct(EntityInterface $entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEntity(): Entity
|
||||
public function getEntity(): EntityInterface
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
|
79
src/EventSuscriber/BlogPostEventSubscriber.php
Normal file
79
src/EventSuscriber/BlogPostEventSubscriber.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\EventSuscriber;
|
||||
|
||||
use App\Entity\Blog\Post;
|
||||
use App\Entity\EntityInterface;
|
||||
use App\Event\EntityManager\EntityManagerEvent;
|
||||
use App\Repository\Blog\PostRepositoryQuery;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* class BlogPostEventSubscriber.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class BlogPostEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
protected Filesystem $filesystem;
|
||||
protected PostRepositoryQuery $query;
|
||||
|
||||
public function __construct(Filesystem $filesystem, PostRepositoryQuery $query)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
EntityManagerEvent::UPDATE_EVENT => 'onUpdate',
|
||||
EntityManagerEvent::DELETE_EVENT => 'onDelete',
|
||||
];
|
||||
}
|
||||
|
||||
public function support(EntityInterface $entity)
|
||||
{
|
||||
return $entity instanceof Post;
|
||||
}
|
||||
|
||||
public function onUpdate(EntityManagerEvent $event)
|
||||
{
|
||||
if (!$this->support($event->getEntity())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeOrphanUploads();
|
||||
}
|
||||
|
||||
public function onDelete(EntityManagerEvent $event)
|
||||
{
|
||||
if (!$this->support($event->getEntity())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeOrphanUploads();
|
||||
}
|
||||
|
||||
protected function removeOrphanUploads()
|
||||
{
|
||||
$finder = new Finder();
|
||||
$finder->files()->in('uploads/post');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$image = str_replace('uploads/', '', $file->getPathname());
|
||||
|
||||
$post = $this->query->create()
|
||||
->where('.image = :image')
|
||||
->setParameter(':image', $image)
|
||||
->findOne()
|
||||
;
|
||||
|
||||
if (null === $post) {
|
||||
$this->filesystem->remove($file->getRealPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use App\Event\EntityManager\EntityManagerEvent;
|
|||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* class EventListener.
|
||||
* class EntityManagerEventSubscriber.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
|
|
18
src/Factory/Blog/CategoryFactory.php
Normal file
18
src/Factory/Blog/CategoryFactory.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Factory\Blog;
|
||||
|
||||
use App\Entity\Blog\Category;
|
||||
|
||||
/**
|
||||
* class CategoryFactory.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class CategoryFactory
|
||||
{
|
||||
public function create(): Category
|
||||
{
|
||||
return new Category();
|
||||
}
|
||||
}
|
26
src/Factory/Blog/PostFactory.php
Normal file
26
src/Factory/Blog/PostFactory.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Factory\Blog;
|
||||
|
||||
use App\Entity\Blog\Post;
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* class PostFactory.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class PostFactory
|
||||
{
|
||||
public function create(?User $author = null): Post
|
||||
{
|
||||
$entity = new Post();
|
||||
|
||||
$entity
|
||||
->setAuthor($author)
|
||||
->setStatus(0)
|
||||
;
|
||||
|
||||
return $entity;
|
||||
}
|
||||
}
|
79
src/Form/Blog/CategoryType.php
Normal file
79
src/Form/Blog/CategoryType.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Blog;
|
||||
|
||||
use App\Entity\Blog\Category;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class CategoryType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add(
|
||||
'title',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Titre',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'subTitle',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'Sous-titre',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'Description',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'data-tinymce' => '',
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'slug',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Slug',
|
||||
'required' => false,
|
||||
'help' => 'Laisser vide pour une génération automatique',
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Category::class,
|
||||
]);
|
||||
}
|
||||
}
|
208
src/Form/Blog/PostType.php
Normal file
208
src/Form/Blog/PostType.php
Normal file
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Blog;
|
||||
|
||||
use App\Entity\Blog\Category;
|
||||
use App\Entity\Blog\Post;
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Date;
|
||||
use Symfony\Component\Validator\Constraints\Image;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class PostType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add(
|
||||
'title',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Titre',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'subTitle',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'Sous-titre',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'metaDescription',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Meta description',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'content',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'Contenu',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'data-tinymce' => '',
|
||||
'rows' => 20,
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'slug',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Slug',
|
||||
'required' => false,
|
||||
'help' => 'Laisser vide pour une génération automatique',
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'categories',
|
||||
EntityType::class,
|
||||
[
|
||||
'label' => 'Catégories',
|
||||
'class' => Category::class,
|
||||
'choice_label' => 'title',
|
||||
'required' => false,
|
||||
'multiple' => true,
|
||||
'attr' => [
|
||||
'data-jschoice' => '',
|
||||
],
|
||||
'query_builder' => function (EntityRepository $repo) {
|
||||
return $repo->createQueryBuilder('a')
|
||||
->orderBy('a.title', 'ASC')
|
||||
;
|
||||
},
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'status',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'label' => 'Statut',
|
||||
'required' => true,
|
||||
'choices' => [
|
||||
'Brouillon' => 0,
|
||||
'Publié' => 1,
|
||||
],
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'imageCaption',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'Titre de l\'image',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'publishedAt',
|
||||
DateType::class,
|
||||
[
|
||||
'label' => 'Date de publication',
|
||||
'required' => false,
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
new Date(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'author',
|
||||
EntityType::class,
|
||||
[
|
||||
'label' => 'Auteur',
|
||||
'class' => User::class,
|
||||
'choice_label' => 'displayName',
|
||||
'required' => true,
|
||||
'attr' => [
|
||||
],
|
||||
'query_builder' => function (EntityRepository $repo) {
|
||||
return $repo->createQueryBuilder('u')
|
||||
->orderBy('u.displayName', 'ASC')
|
||||
;
|
||||
},
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'image',
|
||||
FileType::class,
|
||||
[
|
||||
'label' => 'Image',
|
||||
'required' => false,
|
||||
'data_class' => null,
|
||||
'attr' => [
|
||||
],
|
||||
'constraints' => [
|
||||
new Image(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Post::class,
|
||||
]);
|
||||
}
|
||||
}
|
28
src/Form/FileUploadHandler.php
Normal file
28
src/Form/FileUploadHandler.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
/**
|
||||
* class FileUploadHandler.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class FileUploadHandler
|
||||
{
|
||||
public function handleForm(?UploadedFile $uploadedFile, string $path, callable $afterUploadCallback): void
|
||||
{
|
||||
if (null === $uploadedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
|
||||
$filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension();
|
||||
|
||||
$uploadedFile->move($path, $filename);
|
||||
|
||||
$afterUploadCallback($filename);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Manager;
|
||||
|
||||
use App\Entity\Entity;
|
||||
use App\Entity\EntityInterface;
|
||||
use App\Event\EntityManager\EntityManagerEvent;
|
||||
use Doctrine\ORM\EntityManager as DoctrineEntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
@ -25,7 +25,7 @@ class EntityManager
|
|||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function create(Entity $entity): self
|
||||
public function create(EntityInterface $entity): self
|
||||
{
|
||||
$this->persist($entity);
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT);
|
||||
|
@ -33,7 +33,7 @@ class EntityManager
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function update(Entity $entity): self
|
||||
public function update(EntityInterface $entity): self
|
||||
{
|
||||
$this->persist($entity);
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT);
|
||||
|
@ -41,9 +41,11 @@ class EntityManager
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function delete(Entity $entity): self
|
||||
public function delete(EntityInterface $entity): self
|
||||
{
|
||||
$this->remove($entity);
|
||||
$this->entityManager->remove($entity);
|
||||
$this->flush();
|
||||
|
||||
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
|
||||
|
||||
return $this;
|
||||
|
@ -63,8 +65,9 @@ class EntityManager
|
|||
return $this;
|
||||
}
|
||||
|
||||
protected function persist(Entity $entity)
|
||||
protected function persist(EntityInterface $entity)
|
||||
{
|
||||
$this->entityManager->persist($entity);
|
||||
$this->flush();
|
||||
}
|
||||
}
|
||||
|
|
21
src/Repository/Blog/CategoryRepository.php
Normal file
21
src/Repository/Blog/CategoryRepository.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository\Blog;
|
||||
|
||||
use App\Entity\Blog\Category;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method Category|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Category|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Category[] findAll()
|
||||
* @method Category[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class CategoryRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Category::class);
|
||||
}
|
||||
}
|
19
src/Repository/Blog/CategoryRepositoryQuery.php
Normal file
19
src/Repository/Blog/CategoryRepositoryQuery.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository\Blog;
|
||||
|
||||
use App\Repository\RepositoryQuery;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
|
||||
/**
|
||||
* class CategoryRepositoryQuery.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class CategoryRepositoryQuery extends RepositoryQuery
|
||||
{
|
||||
public function __construct(CategoryRepository $repository, PaginatorInterface $paginator)
|
||||
{
|
||||
parent::__construct($repository, 'c', $paginator);
|
||||
}
|
||||
}
|
21
src/Repository/Blog/PostRepository.php
Normal file
21
src/Repository/Blog/PostRepository.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository\Blog;
|
||||
|
||||
use App\Entity\Blog\Post;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Post|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Post[] findAll()
|
||||
* @method Post[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class PostRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Post::class);
|
||||
}
|
||||
}
|
33
src/Repository/Blog/PostRepositoryQuery.php
Normal file
33
src/Repository/Blog/PostRepositoryQuery.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository\Blog;
|
||||
|
||||
use App\Entity\Blog\Category;
|
||||
use App\Repository\RepositoryQuery;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
|
||||
/**
|
||||
* class PostRepositoryQuery.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class PostRepositoryQuery extends RepositoryQuery
|
||||
{
|
||||
public function __construct(PostRepository $repository, PaginatorInterface $paginator)
|
||||
{
|
||||
parent::__construct($repository, 'p', $paginator);
|
||||
}
|
||||
|
||||
public function inCategory(Category $category)
|
||||
{
|
||||
$c = 'c'.mt_rand();
|
||||
|
||||
$this
|
||||
->innerJoin('p.categories', $c)
|
||||
->andWhere($c.'.id = :category')
|
||||
->setParameter(':category', $category->getId())
|
||||
;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
88
src/Repository/RepositoryQuery.php
Normal file
88
src/Repository/RepositoryQuery.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
|
||||
/**
|
||||
* class RepositoryQuery.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
abstract class RepositoryQuery
|
||||
{
|
||||
protected ServiceEntityRepository $repository;
|
||||
protected QueryBuilder $query;
|
||||
protected PaginatorInterface $paginator;
|
||||
protected string $id;
|
||||
|
||||
public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->query = $repository->createQueryBuilder($id);
|
||||
$this->paginator = $paginator;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function __call(string $name, $params): self
|
||||
{
|
||||
$fn = function (&$data) {
|
||||
if (is_string($data)) {
|
||||
$words = explode(' ', $data);
|
||||
|
||||
foreach ($words as $k => $v) {
|
||||
if (isset($v[0]) && '.' === $v[0]) {
|
||||
$words[$k] = $this->id.$v;
|
||||
}
|
||||
}
|
||||
|
||||
$data = implode(' ', $words);
|
||||
} elseif (is_array($data)) {
|
||||
foreach ($data as $k => $v) {
|
||||
$fn($data[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
};
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$fn($params[$key]);
|
||||
}
|
||||
|
||||
call_user_func_array([$this->query, $name], $params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$class = get_called_class();
|
||||
|
||||
return new $class($this->repository, $this->paginator);
|
||||
}
|
||||
|
||||
public function call(callable $fn): self
|
||||
{
|
||||
$fn($this->query, $this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function findOne()
|
||||
{
|
||||
return $this->query->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function find()
|
||||
{
|
||||
return $this->query->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function paginate(int $page = 1, int $limit = 20)
|
||||
{
|
||||
return $this->paginator->paginate($this->query->getQuery(), $page, $limit);
|
||||
}
|
||||
}
|
|
@ -35,33 +35,4 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
|
|||
$this->_em->persist($user);
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return User[] Returns an array of User objects
|
||||
// */
|
||||
/*
|
||||
public function findByExampleField($value)
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->andWhere('u.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->orderBy('u.id', 'ASC')
|
||||
->setMaxResults(10)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public function findOneBySomeField($value): ?User
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->andWhere('u.exampleField = :val')
|
||||
->setParameter('val', $value)
|
||||
->getQuery()
|
||||
->getOneOrNullResult()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
18
src/Repository/UserRepositoryQuery.php
Normal file
18
src/Repository/UserRepositoryQuery.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
|
||||
/**
|
||||
* class UserRepositoryQuery.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
class UserRepositoryQuery extends RepositoryQuery
|
||||
{
|
||||
public function __construct(UserRepository $repository, PaginatorInterface $paginator)
|
||||
{
|
||||
parent::__construct($repository, 'u', $paginator);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
<body>
|
||||
{{ include('admin/module/flashes.html.twig') }}
|
||||
|
||||
<nav class="navbar fixed-top navbar-expand-md navbar-light bg-white border-bottom">
|
||||
<nav class="navbar fixed-top navbar-expand-md navbar-light bg-light border-bottom">
|
||||
<a class="navbar-brand" href="{{ path('admin_dashboard_index') }}">
|
||||
<img id="logo" src="{{ asset('build/images/logo.png') }}" alt="Tinternet & cie" title="Tinternet & cie">
|
||||
|
||||
|
@ -64,7 +64,7 @@
|
|||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_blog_post_index') }}">
|
||||
<span class="fa fa-pen"></span>
|
||||
|
||||
<span class="nav-item-label">Articles</span>
|
||||
|
@ -72,7 +72,7 @@
|
|||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ macros.active_class('blog_post', section) }}" href="{{ path('admin_dashboard_index') }}">
|
||||
<a class="nav-link {{ macros.active_class('blog_category', section) }}" href="{{ path('admin_blog_category_index') }}">
|
||||
<span class="fa fa-puzzle-piece"></span>
|
||||
|
||||
<span class="nav-item-label">Catégories</span>
|
||||
|
@ -104,7 +104,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
|
||||
<script src="{{ asset('vendor/tinymce/tinymce.min.js') }}"></script>
|
||||
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
{% endblock %}
|
||||
|
|
17
templates/blog/category_admin/_form.html.twig
Normal file
17
templates/blog/category_admin/_form.html.twig
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="row">
|
||||
<div class="col-4 p-3">
|
||||
<div class="row">
|
||||
{% for item in ['title', 'subTitle', 'slug'] %}
|
||||
<div class="col-12">
|
||||
{{ form_row(form[item]) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 p-3">
|
||||
{% for item in ['description'] %}
|
||||
{{ form_row(form[item]) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
57
templates/blog/category_admin/edit.html.twig
Normal file
57
templates/blog/category_admin/edit.html.twig
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">{{ entity.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_category_index') }}" class="btn btn-light">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
Retour à la liste
|
||||
</a>
|
||||
<a href="{{ path('admin_blog_category_show', {entity: entity.id}) }}" class="btn btn-secondary">
|
||||
<span class="fa fa-eye pr-1"></span>
|
||||
Voir
|
||||
</a>
|
||||
|
||||
<button type="submit" form="form-main" class="btn btn-primary">
|
||||
<span class="fa fa-save pr-1"></span>
|
||||
Enregistrer
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-white dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="font-weight-bold">
|
||||
⋅⋅⋅
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<button type="submit" form="form-delete" class="dropdown-item">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
{{ include('blog/category_admin/_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ path('admin_blog_category_delete', {entity: entity.id}) }}" id="form-delete" data-form-confirm>
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
|
||||
</form>
|
||||
{% endblock %}
|
77
templates/blog/category_admin/index.html.twig
Normal file
77
templates/blog/category_admin/index.html.twig
Normal file
|
@ -0,0 +1,77 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">Catégories</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_category_new') }}" class="btn btn-primary">
|
||||
<span class="fa fa-plus pr-1"></span>
|
||||
Nouveau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ knp_pagination_render(pager) }}
|
||||
</div>
|
||||
|
||||
<table class="table" data-table-fixed>
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="col-6">Titre</th>
|
||||
<th class="col-4">Articles</th>
|
||||
<th class="col-2 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in pager %}
|
||||
{% set edit = path('admin_blog_category_edit', {entity: item.id}) %}
|
||||
{% set show = path('admin_blog_category_show', {entity: item.id}) %}
|
||||
|
||||
<tr data-dblclick="{{ edit }}">
|
||||
<td class="col-6">
|
||||
<a href="{{ show }}" class="font-weight-bold text-body d-block">
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="col-4">
|
||||
<a href="" class="btn btn-sm btn-light">
|
||||
{% set postsCount = item.posts|length %}
|
||||
|
||||
<span class="font-weight-bold">{{ postsCount }}</span> {{ postsCount < 2 ? 'article' : 'articles' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="col-2 text-right">
|
||||
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
|
||||
<form method="post" action="{{ path('admin_blog_category_delete', {entity: item.id}) }}" id="form-delete-{{ item.id }}" data-form-confirm>
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ item.id) }}">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="col-12 text-center p-4 text-black-50">
|
||||
<div class="display-1">
|
||||
<span class="fa fa-search"></span>
|
||||
</div>
|
||||
<div class="display-5 mt-3">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
39
templates/blog/category_admin/new.html.twig
Normal file
39
templates/blog/category_admin/new.html.twig
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">Nouvelle catégorie</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_category_index') }}" class="btn btn-light">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
|
||||
Retour à la liste
|
||||
</a>
|
||||
|
||||
<button type="submit" form="form-main" class="btn btn-primary">
|
||||
<span class="fa fa-save pr-1"></span>
|
||||
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
{{ include('blog/category_admin/_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
</form>
|
||||
{% endblock %}
|
85
templates/blog/category_admin/show.html.twig
Normal file
85
templates/blog/category_admin/show.html.twig
Normal file
|
@ -0,0 +1,85 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">{{ entity.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_category_index') }}" class="btn btn-secondary">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
|
||||
Retour à la liste
|
||||
</a>
|
||||
<a href="{{ path('admin_blog_category_edit', {entity: entity.id}) }}" class="btn btn-primary">
|
||||
<span class="fa fa-edit pr-1"></span>
|
||||
|
||||
Éditer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4 p-3">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Titre</span>
|
||||
|
||||
{{ entity.title }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Sous-titre</span>
|
||||
|
||||
{{ entity.subTitle }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">URL</span>
|
||||
|
||||
{{ absolute_url('/' ~ entity.slug) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-8 p-3">
|
||||
<div class="font-weight-bold pb-2">Description</div>
|
||||
|
||||
{{ entity.description|raw|nl2br }}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<table class="table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Derniers articles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in posts %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ path('admin_blog_post_show', {entity: item.id}) }}" class="text-body">
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="text-center p-4 text-black-50">
|
||||
<div class="display-1">
|
||||
<span class="fa fa-search"></span>
|
||||
</div>
|
||||
<div class="display-5 mt-3">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
36
templates/blog/post_admin/_form.html.twig
Normal file
36
templates/blog/post_admin/_form.html.twig
Normal file
|
@ -0,0 +1,36 @@
|
|||
<div class="row">
|
||||
<div class="col-3 p-3">
|
||||
<div class="row">
|
||||
{% for item in ['title', 'subTitle', 'categories', 'metaDescription', 'slug'] %}
|
||||
<div class="col-12">
|
||||
{{ form_row(form[item]) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7 p-3">
|
||||
<div class="row">
|
||||
{% for item in ['content'] %}
|
||||
<div class="col-12">
|
||||
{{ form_row(form[item]) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 p-3">
|
||||
<div class="row">
|
||||
{% for item in ['image', 'imageCaption', 'status', 'publishedAt', 'author'] %}
|
||||
<div class="col-12">
|
||||
{{ form_row(form[item]) }}
|
||||
|
||||
{% if item == 'image' %}
|
||||
{% if entity.image %}
|
||||
<img src="{{ asset('uploads/' ~ entity.image) }}" class="img-fluid">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
57
templates/blog/post_admin/edit.html.twig
Normal file
57
templates/blog/post_admin/edit.html.twig
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">{{ entity.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-light">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
Retour à la liste
|
||||
</a>
|
||||
<a href="{{ path('admin_blog_post_show', {entity: entity.id}) }}" class="btn btn-secondary">
|
||||
<span class="fa fa-eye pr-1"></span>
|
||||
Voir
|
||||
</a>
|
||||
|
||||
<button type="submit" form="form-main" class="btn btn-primary">
|
||||
<span class="fa fa-save pr-1"></span>
|
||||
Enregistrer
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-white dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="font-weight-bold">
|
||||
⋅⋅⋅
|
||||
</span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<button type="submit" form="form-delete" class="dropdown-item">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
{{ include('blog/post_admin/_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ path('admin_blog_post_delete', {entity: entity.id}) }}" id="form-delete" data-form-confirm>
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
|
||||
</form>
|
||||
{% endblock %}
|
103
templates/blog/post_admin/index.html.twig
Normal file
103
templates/blog/post_admin/index.html.twig
Normal file
|
@ -0,0 +1,103 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">Articles</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_post_new') }}" class="btn btn-primary">
|
||||
<span class="fa fa-plus pr-1"></span>
|
||||
Nouveau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ knp_pagination_render(pager) }}
|
||||
</div>
|
||||
|
||||
<table class="table" data-table-fixed>
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="col-6">Titre</th>
|
||||
<th class="col-2">Mise à jour</th>
|
||||
<th class="col-1">Statut</th>
|
||||
<th class="col-3 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in pager %}
|
||||
{% set edit = path('admin_blog_post_edit', {entity: item.id}) %}
|
||||
{% set show = path('admin_blog_post_show', {entity: item.id}) %}
|
||||
|
||||
<tr>
|
||||
<td class="col-6">
|
||||
{% if item.image %}
|
||||
{% set image = asset('uploads/' ~ item.image) %}
|
||||
{% else %}
|
||||
{% set image = asset('build/images/no-image.png') %}
|
||||
{% endif %}
|
||||
|
||||
<img src="{{ asset('build/images/blank.png') }}" style="background: url({{ image }}); background-size: cover" class="rounded float-left mr-2">
|
||||
|
||||
<a href="{{ show }}" class="font-weight-bold text-body d-block">
|
||||
{{ item.title }}
|
||||
</a>
|
||||
|
||||
{% set categories = [] %}
|
||||
|
||||
{% for category in item.categories %}
|
||||
{% set url = path('admin_blog_category_show', {entity: category.id}) %}
|
||||
{% set categories = categories|merge(['<a href="' ~ url ~ '">' ~ category.title ~ '</a>']) %}
|
||||
{% endfor %}
|
||||
|
||||
Dans {{ categories|join(', ')|raw }} par <a href="">{{ item.author.displayName }}</a>
|
||||
</td>
|
||||
<td class="col-2">
|
||||
<span class="btn btn-sm btn-light">
|
||||
<span class="fa fa-calendar-alt text-black-50 mr-1"></span>
|
||||
{{ item.updatedAt|date('d/m/Y H:i') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="col-1">
|
||||
{% set map = {
|
||||
0: ['warning', 'Brouillon'],
|
||||
1: ['success', 'Publié'],
|
||||
} %}
|
||||
<button class="btn btn-sm btn-{{ map[item.status].0 }}">
|
||||
{{ map[item.status].1 }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="col-3 text-right">
|
||||
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
|
||||
<form method="post" action="{{ path('admin_blog_post_delete', {entity: item.id}) }}" id="form-delete-{{ item.id }}" data-form-confirm>
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ item.id) }}">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="col-12 text-center p-4 text-black-50">
|
||||
<div class="display-1">
|
||||
<span class="fa fa-search"></span>
|
||||
</div>
|
||||
<div class="display-5 mt-3">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
39
templates/blog/post_admin/new.html.twig
Normal file
39
templates/blog/post_admin/new.html.twig
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">Nouvel article</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-light">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
|
||||
Retour à la liste
|
||||
</a>
|
||||
|
||||
<button type="submit" form="form-main" class="btn btn-primary">
|
||||
<span class="fa fa-save pr-1"></span>
|
||||
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active">
|
||||
<div class="tab-form">
|
||||
{{ include('blog/post_admin/_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_rest(form) }}
|
||||
</form>
|
||||
{% endblock %}
|
106
templates/blog/post_admin/show.html.twig
Normal file
106
templates/blog/post_admin/show.html.twig
Normal file
|
@ -0,0 +1,106 @@
|
|||
{% extends 'admin/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
|
||||
<div class="d-flex">
|
||||
<div class="mr-auto w-50">
|
||||
<h1 class="display-5">{{ entity.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-secondary">
|
||||
<span class="fa fa-list pr-1"></span>
|
||||
|
||||
Retour à la liste
|
||||
</a>
|
||||
<a href="{{ path('admin_blog_post_edit', {entity: entity.id}) }}" class="btn btn-primary">
|
||||
<span class="fa fa-edit pr-1"></span>
|
||||
|
||||
Éditer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3 p-3">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Titre</span>
|
||||
|
||||
{{ entity.title }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Sous-titre</span>
|
||||
|
||||
{{ entity.subTitle }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Catégories</span>
|
||||
|
||||
{% for category in entity.categories %}
|
||||
<a class="d-block" href="{{ path('admin_blog_post_show', {entity: category.id}) }}">{{ category.title }}</a>
|
||||
{% endfor %}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">URL</span>
|
||||
|
||||
{{ absolute_url('/' ~ entity.slug) }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Meta description</span>
|
||||
|
||||
{{ entity.metaDescription }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6 p-3">
|
||||
<div class="font-weight-bold pb-2">Contenu</div>
|
||||
|
||||
{{ entity.content|raw|nl2br }}
|
||||
</div>
|
||||
<div class="col-3 p-3">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-3 d-block">Image</span>
|
||||
|
||||
{% if entity.image %}
|
||||
<figure>
|
||||
<img src="{{ asset('uploads/' ~ entity.image) }}" alt="{{ entity.imageCaption }}" title="{{ entity.imageCaption }}" class="img-fluid">
|
||||
|
||||
<figcaption>
|
||||
{{ entity.imageCaption }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Statut</span>
|
||||
|
||||
{% if entity.status == 0 %}
|
||||
Brouillon
|
||||
{% else %}
|
||||
Publié
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Date de publication</span>
|
||||
|
||||
{{ entity.publishedAt ? entity.publishedAt|date('d/m/Y H:i') : '-' }}
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="font-weight-bold pb-2 d-block">Auteur</span>
|
||||
|
||||
<a href="">
|
||||
{{ entity.author.displayName }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -5771,10 +5771,10 @@ timsort@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||
|
||||
tinymce@^5.2.0:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.7.0.tgz#bc565877e4041db83848a330dfa7916993110cba"
|
||||
integrity sha512-WikgMpJbqYPaucV3lfstCj+Y4NquZlK61gyuJ2eqDUBlSU+4fFh6rpwnelVTxuvtEyJsAVOl8HZmbzBfDJiLsQ==
|
||||
tinymce@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.7.1.tgz#658a6fb4c7d53a8496cc00f8da33f4b8290da06d"
|
||||
integrity sha512-1gY8RClc734srSlkYwY0MQzmkS1j73PuPC+nYtNtrrQVPY9VNcZ4bOiRwzTbdjPPD8GOtv6BAk8Ww/H2RiqKpA==
|
||||
|
||||
tmp@^0.2.1:
|
||||
version "0.2.1"
|
||||
|
|
Loading…
Reference in a new issue