mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
[wip] add actions to crud
This commit is contained in:
parent
d6354da24e
commit
199930d23a
@ -96,3 +96,4 @@ Version 1.5.14
|
||||
Branch CRUD-Init
|
||||
================
|
||||
|
||||
- create an api for rendering entities
|
||||
|
@ -31,6 +31,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Chill\MainBundle\CRUD\Resolver\Resolver;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -161,7 +162,8 @@ class CRUDController extends AbstractController
|
||||
}
|
||||
|
||||
$defaultTemplateParameters = [
|
||||
'entity' => $entity
|
||||
'entity' => $entity,
|
||||
'crud_name' => $this->getCrudName()
|
||||
];
|
||||
|
||||
return $this->render(
|
||||
@ -193,17 +195,15 @@ class CRUDController extends AbstractController
|
||||
$em->flush();
|
||||
$this->onPostFlush($action, $entity, $form, $request);
|
||||
|
||||
$this->addFlash('succes', $this->generateFormSuccessMessage($action, $entity));
|
||||
$this->addFlash('success', $this->generateFormSuccessMessage($action, $entity));
|
||||
|
||||
$result = $this->onBeforeRedirect($action, $entity, $form, $request);
|
||||
$result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request);
|
||||
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index', [
|
||||
'id' => $entity->getId()
|
||||
]);
|
||||
return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index');
|
||||
|
||||
} elseif ($form->isSubmitted()) {
|
||||
$this->addFlash('error', $this->generateFormErrorMessage($action, $form));
|
||||
@ -223,7 +223,11 @@ class CRUDController extends AbstractController
|
||||
|
||||
protected function formCreateAction($action, Request $request, $formClass = null): Response
|
||||
{
|
||||
$entity = $this->createEntity($action, $request);
|
||||
if ($request->query->has('duplicate')) {
|
||||
$entity = $this->duplicateEntity($action, $request);
|
||||
} else {
|
||||
$entity = $this->createEntity($action, $request);
|
||||
}
|
||||
|
||||
$this->checkACL($action, $entity);
|
||||
|
||||
@ -243,9 +247,9 @@ class CRUDController extends AbstractController
|
||||
$em->flush();
|
||||
$this->onPostFlush($action, $entity, $form, $request);
|
||||
$this->getPaginatorFactory();
|
||||
$this->addFlash('succes', $this->generateFormSuccessMessage($action, $entity));
|
||||
$this->addFlash('success', $this->generateFormSuccessMessage($action, $entity));
|
||||
|
||||
$result = $this->onBeforeRedirect($action, $entity, $form, $request);
|
||||
$result = $this->onBeforeRedirectAfterSubmission($action, $entity, $form, $request);
|
||||
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
@ -280,9 +284,17 @@ class CRUDController extends AbstractController
|
||||
return $this->getDoctrine()
|
||||
->getRepository($this->getEntityClass())
|
||||
->find($id);
|
||||
|
||||
}
|
||||
|
||||
protected function duplicateEntity(string $action, Request $request)
|
||||
{
|
||||
$id = $request->query->get('duplicate_id', 0);
|
||||
$originalEntity = $this->getEntity($action, $id, $request);
|
||||
|
||||
$this->getDoctrine()->getManager()
|
||||
->detach($originalEntity);
|
||||
|
||||
return $originalEntity;
|
||||
}
|
||||
|
||||
protected function getEntityClass(): string
|
||||
@ -311,14 +323,7 @@ class CRUDController extends AbstractController
|
||||
|
||||
protected function buildDefaultRole($action)
|
||||
{
|
||||
if (empty($this->crudConfig['base_role'])) {
|
||||
throw new \LogicException(sprintf("the base role is not defined. You must define "
|
||||
. "on or override %s or %s methods", __METHOD__, "getRoleFor"));
|
||||
}
|
||||
|
||||
return \strtoupper(
|
||||
$this->crudConfig['base_role'].
|
||||
'_'.
|
||||
return $this->getCrudResolver()->buildDefaultRole($this->getCrudName(),
|
||||
$action);
|
||||
}
|
||||
|
||||
@ -334,21 +339,19 @@ class CRUDController extends AbstractController
|
||||
|
||||
$form = $this->createForm($formClass, $entity, $formOptions);
|
||||
|
||||
$this->addDefaultButtons($action, $form);
|
||||
$this->customizeForm($action, $form);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function addDefaultButtons($action, FormInterface $form)
|
||||
protected function customizeForm($action, FormInterface $form)
|
||||
{
|
||||
$form->add('submit', SubmitType::class, [
|
||||
'label' => $this->generateLabelForButton($action, 'submit', $form)
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function generateLabelForButton($action, $formName, $form)
|
||||
{
|
||||
return $action;
|
||||
return sprintf("crud.%s.button_action_form", $action);
|
||||
}
|
||||
|
||||
protected function generateFormErrorMessage($action, FormInterface $form): string
|
||||
@ -362,13 +365,13 @@ class CRUDController extends AbstractController
|
||||
{
|
||||
switch ($action) {
|
||||
case 'edit':
|
||||
$msg = "The data have been successfully updated";
|
||||
$msg = "crud.edit.success";
|
||||
break;
|
||||
case 'new':
|
||||
$msg = "The date have been successfully created";
|
||||
$msg = "crud.new.success";
|
||||
break;
|
||||
default:
|
||||
$msg = "Your request has been successfully executed";
|
||||
$msg = "crud.default.success";
|
||||
}
|
||||
|
||||
return $this->getTranslator()->trans($msg);
|
||||
@ -403,6 +406,8 @@ class CRUDController extends AbstractController
|
||||
return '@ChillMain/CRUD/edit.html.twig';
|
||||
case 'index':
|
||||
return '@ChillMain/CRUD/index.html.twig';
|
||||
case 'view':
|
||||
return '@ChillMain/CRUD/view.html.twig';
|
||||
default:
|
||||
throw new \LogicException("the view for action $action is not "
|
||||
. "defined. You should override ".__METHOD__." to add this "
|
||||
@ -445,8 +450,20 @@ class CRUDController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
protected function onBeforeRedirect(string $action, $entity, FormInterface $form, Request $request)
|
||||
protected function onBeforeRedirectAfterSubmission(string $action, $entity, FormInterface $form, Request $request)
|
||||
{
|
||||
$next = $request->request->get("submit", "save-and-close");
|
||||
|
||||
switch ($next) {
|
||||
case "save-and-close":
|
||||
return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index');
|
||||
case "save-and-new":
|
||||
return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_new');
|
||||
default:
|
||||
return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [
|
||||
'id' => $entity->getId()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getActionConfig(string $action)
|
||||
@ -481,6 +498,11 @@ class CRUDController extends AbstractController
|
||||
return $this->get(EventDispatcherInterface::class);
|
||||
}
|
||||
|
||||
protected function getCrudResolver(): Resolver
|
||||
{
|
||||
return $this->get(Resolver::class);
|
||||
}
|
||||
|
||||
public static function getSubscribedServices()
|
||||
{
|
||||
return \array_merge(
|
||||
@ -490,6 +512,7 @@ class CRUDController extends AbstractController
|
||||
'translator' => TranslatorInterface::class,
|
||||
AuthorizationHelper::class => AuthorizationHelper::class,
|
||||
EventDispatcherInterface::class => EventDispatcherInterface::class,
|
||||
Resolver::class => Resolver::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -41,10 +41,35 @@ class Resolver
|
||||
*/
|
||||
protected $propertyAccess;
|
||||
|
||||
function __construct(EntityManagerInterface $em)
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $crudConfig;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const ROLE_VIEW = 'role.view';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const ROLE_EDIT = 'role.edit';
|
||||
|
||||
/**
|
||||
* The key to get the role necessary for the action
|
||||
*/
|
||||
const ROLE = 'role';
|
||||
|
||||
function __construct(EntityManagerInterface $em, array $crudConfig)
|
||||
{
|
||||
$this->em = $em;
|
||||
|
||||
foreach($crudConfig as $conf) {
|
||||
$this->crudConfig[$conf['name']] = $conf;
|
||||
}
|
||||
|
||||
$this->buildPropertyAccess();
|
||||
}
|
||||
|
||||
@ -69,13 +94,37 @@ class Resolver
|
||||
return $this->propertyAccess->getValue($entity, $path);
|
||||
}
|
||||
|
||||
public function getConfigValue($key, $crudName, $action = null)
|
||||
{
|
||||
$config = $this->crudConfig[$crudName];
|
||||
|
||||
switch ($key) {
|
||||
case self::ROLE:
|
||||
dump($config);
|
||||
return $config['actions'][$action]['role'] ?? $this->buildDefaultRole($crudName, $action);
|
||||
}
|
||||
}
|
||||
|
||||
public function buildDefaultRole($crudName, $action)
|
||||
{
|
||||
if (empty($this->crudConfig[$crudName]['base_role'])) {
|
||||
throw new \LogicException(sprintf("the base role is not defined. You must define "
|
||||
. "on or override %s or %s methods", __METHOD__, "getRoleFor"));
|
||||
}
|
||||
|
||||
return \strtoupper(
|
||||
$this->crudConfig[$crudName]['base_role'].
|
||||
'_'.
|
||||
$action);
|
||||
}
|
||||
|
||||
public function getTwigTemplate($entity, $path): string
|
||||
{
|
||||
list($focusEntity, $subPath) = $this->getFocusedEntity($entity, $path);
|
||||
|
||||
$classMetadata = $this->em->getClassMetadata(\get_class($focusEntity));
|
||||
$type = $classMetadata->getTypeOfField($subPath);
|
||||
dump($type);
|
||||
|
||||
switch ($type) {
|
||||
|
||||
default:
|
||||
|
@ -22,6 +22,7 @@ namespace Chill\MainBundle\CRUD\Templating;
|
||||
|
||||
use Chill\MainBundle\CRUD\Resolver\Resolver;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\Environment;
|
||||
|
||||
@ -50,6 +51,14 @@ class TwigCRUDResolver extends AbstractExtension
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunctions()
|
||||
{
|
||||
return [
|
||||
new TwigFunction('chill_crud_config', [$this, 'getConfig'],
|
||||
['is_safe' => 'html'])
|
||||
];
|
||||
}
|
||||
|
||||
public function display(Environment $env, $entity, $path): string
|
||||
{
|
||||
$data = $this->resolver->getData($entity, $path);
|
||||
@ -57,5 +66,10 @@ class TwigCRUDResolver extends AbstractExtension
|
||||
|
||||
return $env->render($template, ['data' => $data, 'entity' => $entity, ]);
|
||||
}
|
||||
|
||||
public function getConfig($configKey, $crudName, $action = null)
|
||||
{
|
||||
return $this->resolver->getConfigValue($configKey, $crudName, $action);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -154,6 +154,10 @@ class Configuration implements ConfigurationInterface
|
||||
->defaultNull()
|
||||
->info('the role that will be required for this action. Override option `base_role`')
|
||||
->end()
|
||||
->scalarNode('template')
|
||||
->defaultNull()
|
||||
->info('the template to render the view')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
|
@ -7,6 +7,7 @@ services:
|
||||
Chill\MainBundle\CRUD\Resolver\Resolver:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
$crudConfig: '%chill_main_crud_route_loader_config%'
|
||||
|
||||
Chill\MainBundle\CRUD\Templating\TwigCRUDResolver:
|
||||
arguments:
|
||||
|
@ -10,6 +10,7 @@ ul.record_actions, ul.record_actions_column {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.5em 0;
|
||||
flex-wrap: wrap-reverse;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
|
@ -1,7 +1,7 @@
|
||||
.sc-button {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&.bt-submit, &.bt-save, &.bt-create, &.bt-new {
|
||||
&.bt-submit, &.bt-save, &.bt-create, &.bt-new, &.bt-duplicate {
|
||||
@include button($green, $white);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
&:not(.change-icon) {
|
||||
|
||||
// icons using font-awesome "old way"
|
||||
&.bt-create::before,
|
||||
&.bt-save::before,
|
||||
&.bt-new::before,
|
||||
@ -31,6 +32,14 @@
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
// icons using font-awesome "new svg way"
|
||||
&.bt-duplicate::before {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.bt-save::before {
|
||||
// add a floppy
|
||||
@ -60,6 +69,10 @@
|
||||
&.bt-show::before, &.bt-view::before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
&.bt-duplicate::before {
|
||||
content: url("./copy-solid.svg");
|
||||
}
|
||||
}
|
||||
|
||||
> i.fa {
|
||||
|
4
Resources/public/sass/custom/modules/copy-solid.svg
Normal file
4
Resources/public/sass/custom/modules/copy-solid.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
<!-- adapted from original work obtained from font-awesome CC-BY 4.0 International - https://fontawesome.com/license Last updated on July 12, 2018 -->
|
||||
<path fill="white" d="M320 448v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h72v296c0 30.879 25.121 56 56 56h168zm0-344V0H152c-13.255 0-24 10.745-24 24v368c0 13.255 10.745 24 24 24h272c13.255 0 24-10.745 24-24V128H344c-13.2 0-24-10.8-24-24zm120.971-31.029L375.029 7.029A24 24 0 0 0 358.059 0H352v96h96v-6.059a24 24 0 0 0-7.029-16.97z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 733 B |
@ -229,4 +229,23 @@ Log in with your new password: Connectez-vous avec votre nouveau mot de passe
|
||||
# impersonate
|
||||
Exit impersonation: Retour Administrateur
|
||||
Impersonate: Mode fantôme
|
||||
Impersonate mode: Mode fantôme
|
||||
Impersonate mode: Mode fantôme
|
||||
|
||||
crud:
|
||||
new:
|
||||
button_action_form: Créer
|
||||
link_edit: Modifier
|
||||
save_and_close: Créer & fermer
|
||||
save_and_show: Créer & voir
|
||||
save_and_new: Créer & nouveau
|
||||
success: Les données ont été créées
|
||||
edit:
|
||||
button_action_form: Enregistrer
|
||||
back_to_view: Voir
|
||||
save_and_close: Modifier & fermer
|
||||
save_and_show: Modifier & voir
|
||||
success: Les données ont été modifiées
|
||||
default:
|
||||
success: Les données ont été enregistrées
|
||||
view:
|
||||
link_duplicate: Dupliquer
|
@ -1,13 +1,13 @@
|
||||
<div class="{% block crud_content_main_div_class %}grid-10 centered{% endblock %}">
|
||||
{% block crud_content_header %}
|
||||
<h1>{{ 'crud.title.edit_of_%crud_name%'|trans({'%crud_name%' : crud_name }) }}</h1>
|
||||
<h1>{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
|
||||
{% endblock crud_content_header %}
|
||||
|
||||
{% block crud_content_form %}
|
||||
{{ form_start(form) }}
|
||||
|
||||
{% block crud_content_form_rows %}
|
||||
{% for f in form if f.vars.name != 'submit' %}
|
||||
{% for f in form %}
|
||||
{{ form_row(f) }}
|
||||
{% endfor %}
|
||||
{% endblock crud_content_form_rows %}
|
||||
@ -20,8 +20,30 @@
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_view %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'view'), entity) %}
|
||||
<li class="">
|
||||
<a class="sc-button bt-show" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_view', { 'id': entity.id }) }}">
|
||||
{{ 'crud.edit.back_to_view'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<li>{{ form_widget(form.submit, { 'attr': { 'class': 'sc-button bt-edit'} } ) }}</li>
|
||||
{% block content_form_actions_save_and_close %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-close" class="sc-button bt-update">
|
||||
{{ 'crud.edit.save_and_close'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_show %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-show" class="sc-button bt-update">
|
||||
{{ 'crud.edit.save_and_show'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{{ 'crud.title.edit_of_%crud_name%'|trans({'%crud_name%' : crud_name }) }}
|
||||
{{ ('crud.'~crud_name~'.title_edit')|trans }}
|
||||
|
@ -21,7 +21,27 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
<li>{{ form_widget(form.submit, { 'attr': { 'class': 'sc-button bt-new'} } ) }}</li>
|
||||
{% block content_form_actions_save_and_close %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-close" class="sc-button bt-create">
|
||||
{{ 'crud.new.save_and_close'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_show %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-show" class="sc-button bt-create">
|
||||
{{ 'crud.new.save_and_show'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_form_actions_save_and_new %}
|
||||
<li class="">
|
||||
<button type="submit" name="submit" value="save-and-new" class="sc-button bt-create">
|
||||
{{ 'crud.new.save_and_new'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
|
46
Resources/views/CRUD/_view_content.html.twig
Normal file
46
Resources/views/CRUD/_view_content.html.twig
Normal file
@ -0,0 +1,46 @@
|
||||
<div class="{% block crud_content_main_div_class %}grid-10 centered{% endblock %}">
|
||||
{% block crud_content_header %}
|
||||
<h1>{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }}</h1>
|
||||
{% endblock crud_content_header %}
|
||||
|
||||
{% block crud_content_view %}
|
||||
|
||||
{% block crud_content_view_details %}
|
||||
<dl class="chill_view_data">
|
||||
<dt>id</dt>
|
||||
<dd>{{ entity.id|default("No id") }}</dd>
|
||||
</dl>
|
||||
{% endblock crud_content_view_details %}
|
||||
|
||||
{% block crud_content_view_actions %}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% block content_view_actions_back %}
|
||||
<li class="cancel">
|
||||
<a class="sc-button bt-cancel" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_index') }}">
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_view_actions_duplicate_link %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %}
|
||||
<li>
|
||||
<a class="sc-button bt-duplicate" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_new', { 'duplicate_id': entity.id, 'duplicate': true }) }}">
|
||||
{{ 'crud.view.link_duplicate'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock content_view_actions_duplicate_link %}
|
||||
{% block content_view_actions_edit_link %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'view'), entity) %}
|
||||
<li>
|
||||
<a class="sc-button bt-edit" href="{{ chill_return_path_or('chill_crud_'~crud_name~'_edit', { 'id': entity.id }) }}">
|
||||
{{ 'crud.new.link_edit'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock content_view_actions_edit_link %}
|
||||
</ul>
|
||||
{% endblock crud_content_view_actions %}
|
||||
|
||||
{% endblock crud_content_view %}
|
||||
</div>
|
1
Resources/views/CRUD/_view_title.html.twig
Normal file
1
Resources/views/CRUD/_view_title.html.twig
Normal file
@ -0,0 +1 @@
|
||||
{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }}
|
10
Resources/views/CRUD/view.html.twig
Normal file
10
Resources/views/CRUD/view.html.twig
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_view_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% embed '@ChillMain/CRUD/_view_content.html.twig' %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user