Merge branch 'issue159_page_acceuil' into testing-202401

This commit is contained in:
Julien Fastré 2024-01-15 21:02:50 +01:00
commit dd62581226
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
31 changed files with 530 additions and 131 deletions

View File

@ -16,7 +16,6 @@
"@tsconfig/node14": "^1.0.1", "@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"bindings": "^1.5.0", "bindings": "^1.5.0",
"bootstrap": "^5.3.0",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"fork-awesome": "^1.1.7", "fork-awesome": "^1.1.7",
"jquery": "^3.6.0", "jquery": "^3.6.0",
@ -35,6 +34,7 @@
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },
"dependencies": { "dependencies": {
"bootstrap": "~5.2.0",
"@fullcalendar/core": "^6.1.4", "@fullcalendar/core": "^6.1.4",
"@fullcalendar/daygrid": "^6.1.4", "@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/interaction": "^6.1.4", "@fullcalendar/interaction": "^6.1.4",

View File

@ -10,7 +10,7 @@
<php> <php>
<ini name="error_reporting" value="-1" /> <ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" /> <server name="APP_ENV" value="test" force="true" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[direct]=0&amp;max[indirect]=999999" /> <env name="SYMFONY_DEPRECATIONS_HELPER" value="logFile=var/log/deprecations.log" />
<server name="SHELL_VERBOSITY" value="-1" /> <server name="SHELL_VERBOSITY" value="-1" />
<env name="KERNEL_CLASS" value="\App\Kernel" /> <env name="KERNEL_CLASS" value="\App\Kernel" />
</php> </php>

View File

@ -22,23 +22,24 @@ use Symfony\Component\Serializer\SerializerInterface;
class NewsItemApiController class NewsItemApiController
{ {
public function __construct( public function __construct(
private NewsItemRepository $newsItemRepository, private readonly NewsItemRepository $newsItemRepository,
private SerializerInterface $serializer, private readonly SerializerInterface $serializer,
private PaginatorFactory $paginatorFactory private readonly PaginatorFactory $paginatorFactory
) {} ) {
}
/** /**
* Get list of news items filtered on start and end date. * Get list of news items filtered on start and end date.
* *
* @Route("/api/1.0/main/news.json", methods={"get"}) * @Route("/api/1.0/main/news/current.json", methods={"get"})
*/ */
public function listCurrentNewsItems(): JsonResponse public function listCurrentNewsItems(): JsonResponse
{ {
$total = $this->newsItemRepository->countWithDateFilter(); $total = $this->newsItemRepository->countCurrentNews();
$paginator = $this->paginatorFactory->create($total); $paginator = $this->paginatorFactory->create($total);
$newsItems = $this->newsItemRepository->findWithDateFilter( $newsItems = $this->newsItemRepository->findCurrentNews(
$limit = $paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber() $paginator->getCurrentPage()->getFirstItemNumber()
); );
return new JsonResponse($this->serializer->serialize( return new JsonResponse($this->serializer->serialize(

View File

@ -11,57 +11,58 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NewsItemRepository; use Chill\MainBundle\Repository\NewsItemRepository;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
class NewsItemHistoryController extends AbstractController final readonly class NewsItemHistoryController
{ {
public function __construct( public function __construct(
private readonly NewsItemRepository $newsItemRepository, private readonly NewsItemRepository $newsItemRepository,
private readonly PaginatorFactory $paginatorFactory, private readonly PaginatorFactory $paginatorFactory,
private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory, private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
) {} private readonly Environment $environment,
) {
}
/** /**
* @Route("/{_locale}/news-items/history", name="chill_main_news_items_history") * @Route("/{_locale}/news-items/history", name="chill_main_news_items_history")
*/ */
public function listAction(Request $request): Response public function list(): Response
{ {
$filter = $this->buildFilterOrder(false); $filter = $this->buildFilterOrder();
$total = $this->newsItemRepository->countAllFilteredByUser($filter->getQueryString()); $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString());
$newsItems = $this->newsItemRepository->findAllFilteredByUser($filter->getQueryString()); $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString());
$pagination = $this->paginatorFactory->create($total); $pagination = $this->paginatorFactory->create($total);
return $this->render('@ChillMain/NewsItem/news_items_history.html.twig', [ return new Response($this->environment->render('@ChillMain/NewsItem/news_items_history.html.twig', [
'entities' => $newsItems, 'entities' => $newsItems,
'paginator' => $pagination, 'paginator' => $pagination,
'filter_order' => $filter, 'filter_order' => $filter,
]); ]));
} }
/** /**
* @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item") * @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item")
*/ */
public function showSingleItem(int $id, Request $request): Response public function showSingleItem(NewsItem $newsItem, Request $request): Response
{ {
$newsItem = $this->newsItemRepository->findOneBy(['id' => $id]); return new Response($this->environment->render(
return $this->render(
'@ChillMain/NewsItem/show.html.twig', '@ChillMain/NewsItem/show.html.twig',
[ [
'entity' => $newsItem, 'entity' => $newsItem,
] ]
); ));
} }
private function buildFilterOrder($includeFilterByUser = true, $includeMissionType = false): FilterOrderHelper private function buildFilterOrder(): FilterOrderHelper
{ {
$filterBuilder = $this->filterOrderHelperFactory $filterBuilder = $this->filterOrderHelperFactory
->create(self::class) ->create(self::class)

View File

@ -562,10 +562,18 @@ class ChillMainExtension extends Extension implements
'role' => 'ROLE_ADMIN', 'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/new.html.twig', 'template' => '@ChillMain/NewsItem/new.html.twig',
], ],
'view' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/view_admin.html.twig',
],
'edit' => [ 'edit' => [
'role' => 'ROLE_ADMIN', 'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/edit.html.twig', 'template' => '@ChillMain/NewsItem/edit.html.twig',
], ],
'delete' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/delete.html.twig',
],
], ],
], ],
], ],

View File

@ -57,7 +57,7 @@ class DashboardConfigItem
private ?User $user = null; private ?User $user = null;
/** /**
* @ORM\Column(type="json" "jsonb"=true, options={"default": "[]"}) * @ORM\Column(type="json", options={"default": "[]", "jsonb": true})
* *
* @Serializer\Groups({"dashboardConfigItem:read"}) * @Serializer\Groups({"dashboardConfigItem:read"})
*/ */

View File

@ -22,7 +22,7 @@ class NewsItemRepository implements ObjectRepository
{ {
private readonly EntityRepository $repository; private readonly EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager, private ClockInterface $clock) public function __construct(EntityManagerInterface $entityManager, private readonly ClockInterface $clock)
{ {
$this->repository = $entityManager->getRepository(NewsItem::class); $this->repository = $entityManager->getRepository(NewsItem::class);
} }
@ -57,11 +57,14 @@ class NewsItemRepository implements ObjectRepository
return NewsItem::class; return NewsItem::class;
} }
public function buildBaseQuery( private function buildBaseQuery(
string $pattern = null string $pattern = null
): QueryBuilder { ): QueryBuilder {
$qb = $this->createQueryBuilder('n'); $qb = $this->createQueryBuilder('n');
$qb->where('n.startDate <= :now');
$qb->setParameter('now', $this->clock->now());
if (null !== $pattern && '' !== $pattern) { if (null !== $pattern && '' !== $pattern) {
$qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))')) $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))'))
->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))')) ->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))'))
@ -71,24 +74,28 @@ class NewsItemRepository implements ObjectRepository
return $qb; return $qb;
} }
public function findAllFilteredByUser(string $pattern = null) public function findAllFilteredBySearchTerm(string $pattern = null)
{ {
$qb = $this->buildBaseQuery($pattern); $qb = $this->buildBaseQuery($pattern);
$qb->addOrderBy('n.startDate', 'DESC') $qb
->addOrderBy('n.startDate', 'DESC')
->addOrderBy('n.id', 'DESC'); ->addOrderBy('n.id', 'DESC');
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function findWithDateFilter($limit = null, $offset = null) /**
* @return list<NewsItem>
*/
public function findCurrentNews(int $limit = null, int $offset = null): array
{ {
$qb = $this->buildQueryWithDateFilter(); $qb = $this->buildQueryCurrentNews();
if ($limit) { if (null !== $limit) {
$qb->setMaxResults($limit); $qb->setMaxResults($limit);
} }
if ($offset) { if (null !== $offset) {
$qb->setFirstResult($offset); $qb->setFirstResult($offset);
} }
@ -97,7 +104,7 @@ class NewsItemRepository implements ObjectRepository
->getResult(); ->getResult();
} }
public function countAllFilteredByUser(string $pattern = null) public function countAllFilteredBySearchTerm(string $pattern = null)
{ {
$qb = $this->buildBaseQuery($pattern); $qb = $this->buildBaseQuery($pattern);
@ -107,15 +114,15 @@ class NewsItemRepository implements ObjectRepository
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function countWithDateFilter() public function countCurrentNews()
{ {
return $this->buildQueryWithDateFilter() return $this->buildQueryCurrentNews()
->select('COUNT(n)') ->select('COUNT(n)')
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function buildQueryWithDateFilter(): QueryBuilder private function buildQueryCurrentNews(): QueryBuilder
{ {
$now = $this->clock->now(); $now = $this->clock->now();

View File

@ -11,7 +11,6 @@
// 3. Include remainder of required Bootstrap stylesheets // 3. Include remainder of required Bootstrap stylesheets
@import "bootstrap/scss/variables"; @import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
// 4. Include any default map overrides here // 4. Include any default map overrides here
@import "custom/_maps"; @import "custom/_maps";

View File

@ -165,6 +165,6 @@ export interface NewsItemType {
id: number; id: number;
title: string; title: string;
content: string; content: string;
startdate: { date: DateTime }; startDate: DateTime;
enddate: { date: DateTime | null} endDate: DateTime | null;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div v-if="newsItems.length > 0">
<h1>{{ $t('widget.news.title') }}</h1> <h1>{{ $t('widget.news.title') }}</h1>
<ul class="scrollable"> <ul class="scrollable">
<NewsItem v-for="item in newsItems" :item="item" :key="item.id" /> <NewsItem v-for="item in newsItems" :item="item" :key="item.id" />
@ -9,7 +9,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { makeFetch } from '../../../lib/api/apiMethods'; import { fetchResults } from '../../../lib/api/apiMethods';
import Modal from '../../_components/Modal.vue'; import Modal from '../../_components/Modal.vue';
import { NewsItemType } from '../../../types'; import { NewsItemType } from '../../../types';
import NewsItem from './NewsItem.vue'; import NewsItem from './NewsItem.vue';
@ -17,10 +17,12 @@ import NewsItem from './NewsItem.vue';
const newsItems = ref<NewsItemType[]>([]) const newsItems = ref<NewsItemType[]>([])
onMounted(() => { onMounted(() => {
makeFetch('GET', '/api/1.0/main/news.json') fetchResults<NewsItemType>('/api/1.0/main/news/current.json')
.then((response: { results: NewsItemType[] }) => { .then((news): Promise<void> => {
// console.log('news articles', response.results) // console.log('news articles', response.results)
newsItems.value = response.results newsItems.value = news;
return Promise.resolve();
}) })
.catch((error: string) => { .catch((error: string) => {
console.error('Error fetching news items', error); console.error('Error fetching news items', error);

View File

@ -15,7 +15,7 @@
</template> </template>
<template #body> <template #body>
<p class="news-date"> <p class="news-date">
{{ $t('widget.news.date') }}: <span>{{ formatDate(item.startdate.date) }}</span> <span>{{ $d(newsItemStartDate(), 'short') }}</span>
</p> </p>
<div v-html="convertMarkdownToHtml(item.content)"></div> <div v-html="convertMarkdownToHtml(item.content)"></div>
</template> </template>
@ -30,12 +30,17 @@ import DOMPurify from 'dompurify';
import { DateTime, NewsItemType } from "../../../types"; import { DateTime, NewsItemType } from "../../../types";
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { defineProps, ref } from "vue"; import { defineProps, ref } from "vue";
import {dateToISO} from "../../../chill/js/date"; import {ISOToDatetime} from "../../../chill/js/date";
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object as PropType<NewsItemType>, type: Object as PropType<NewsItemType>,
required: true required: true
},
maxLength: {
type: Number,
required: false,
default: 200,
} }
}) })
@ -52,13 +57,13 @@ const closeModal = () => {
showModal.value = false; showModal.value = false;
}; };
const shouldTruncate = (content: string, maxLength = 100): boolean => { const shouldTruncate = (content: string): boolean => {
return content.length > maxLength; return content.length > props.maxLength;
}; };
const truncateContent = (content: string, maxLength = 100): string => { const truncateContent = (content: string): string => {
if (shouldTruncate(content, maxLength)) { if (shouldTruncate(content)) {
let truncatedContent = content.slice(0, maxLength); let truncatedContent = content.slice(0, props.maxLength);
let linkDepth = 0; let linkDepth = 0;
let linkStartIndex = -1; let linkStartIndex = -1;
@ -103,13 +108,13 @@ const convertMarkdownToHtml = (markdown: string): string => {
return DOMPurify.sanitize(rawHtml) return DOMPurify.sanitize(rawHtml)
}; };
const prepareContent = (content: string, maxLength = 200): string => { const prepareContent = (content: string): string => {
const htmlContent = convertMarkdownToHtml(content); const htmlContent = convertMarkdownToHtml(content);
return truncateContent(htmlContent, maxLength); return truncateContent(htmlContent);
}; };
const formatDate = (datetime: DateTime): string|null => { const newsItemStartDate = (): null|Date => {
return dateToISO(new Date(datetime.toString())) return ISOToDatetime(props.item?.startDate.datetime);
} }
</script> </script>

View File

@ -40,9 +40,9 @@
</div> </div>
<div class="mbloc col col-lg-12 col-lg-4" v-if="this.dashboardItems"> <div class="mbloc col col-lg-12 col-lg-4" v-if="this.dashboardItems">
<div v-for="dashboardItem in this.dashboardItems"> <template v-for="dashboardItem in this.dashboardItems">
<News v-if="dashboardItem.type === 'news'"/> <News v-if="dashboardItem.type === 'news'"/>
</div> </template>
</div> </div>
</div> </div>
@ -64,7 +64,8 @@ export default {
counterClass: { counterClass: {
counter: true //hack to pass class 'counter' in i18n-t counter: true //hack to pass class 'counter' in i18n-t
}, },
dashboardItems: [] dashboardItems: [],
masonry: null,
} }
}, },
computed: { computed: {
@ -75,7 +76,7 @@ export default {
}, },
mounted() { mounted() {
const elem = document.querySelector('#dashboards'); const elem = document.querySelector('#dashboards');
const masonry = new Masonry(elem, {}); this.masonry = new Masonry(elem, {});
//Fetch the dashboard items configured for user. Currently response is still hardcoded //Fetch the dashboard items configured for user. Currently response is still hardcoded
makeFetch('GET', '/api/1.0/main/dashboard-config-item.json') makeFetch('GET', '/api/1.0/main/dashboard-config-item.json')
.then((response) => { .then((response) => {
@ -84,8 +85,13 @@ export default {
}) })
.catch((error) => { .catch((error) => {
throw error throw error
}) });
}
},
updated() {
this.masonry.layout();
}
} }
</script> </script>

View File

@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue";
export interface AddressModalContentProps { export interface AddressModalContentProps {
address_id: number; address_id: number;
address_ref_status: AddressRefStatus | null; address_ref_status: AddressRefStatus;
} }
const data = reactive<{ interface AddressModalData {
loading: boolean, loading: boolean,
working_address: Address | null, working_address: Address | null,
working_ref_status: AddressRefStatus | null, working_ref_status: AddressRefStatus | null,
}>({ }
const data: AddressModalData = reactive({
loading: false, loading: false,
working_address: null, working_address: null,
working_ref_status: null, working_ref_status: null,
}); } as AddressModalData);
const props = defineProps<AddressModalContentProps>(); const props = defineProps<AddressModalContentProps>();

View File

@ -1 +1 @@
{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }} {{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }}

View File

@ -0,0 +1,30 @@
<div class="item-bloc">
<div class="item-row">
<h3>
{{ entity.title }}
</h3>
</div>
<div class="item-row">
<p>
{% if entity.startDate %}
<span>{{ entity.startDate|format_date('long') }}</span>
{% endif %}
{% if entity.endDate %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</p>
</div>
<div class="item-row separator">
<div>
{{ entity.content|u.truncate(350, '… [' ~ ('news.read_more'|trans) ~ '](' ~ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) ~ ')', false)|chill_markdown_to_html }}
</div>
</div>
<div class="item-row">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) }}" class="btn btn-show btn-mini"></a>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div class="flex-table">
<div class="item-bloc">
<p class="date-label">
<span>{{ entity.startDate|format_date('long') }}</span>
{% if entity.endDate is not null %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</p>
</div>
<div class="item-bloc">
{{ entity.content|chill_markdown_to_html }}
</div>
</div>

View File

@ -0,0 +1,6 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/delete.html.twig' %}
{% endembed %}
{% endblock admin_content %}

View File

@ -11,17 +11,23 @@
{% for entity in entities %} {% for entity in entities %}
<tr> <tr>
<td>{{ entity.title }}</td> <td>{{ entity.title }}</td>
<td>{{ entity.startDate|date }}</td> <td>{{ entity.startDate|format_date('long') }}</td>
{% if entity.endDate is not null %} {% if entity.endDate is not null %}
<td>{{ entity.endDate|date }}</td> <td>{{ entity.endDate|format_date('long') }}</td>
{% else %} {% else %}
<td>{{ 'news.noDate'|trans }}</td> <td>{{ 'news.noDate'|trans }}</td>
{% endif %} {% endif %}
<td> <td>
<ul class="record_actions"> <ul class="record_actions">
<li>
<a href="{{ path('chill_crud_news_item_view', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li> <li>
<a href="{{ path('chill_crud_news_item_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a> <a href="{{ path('chill_crud_news_item_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li> </li>
<li>
<a href="{{ path('chill_crud_news_item_delete', { 'id': entity.id }) }}" class="btn btn-delete" title="{{ 'delete'|trans }}"></a>
</li>
</ul> </ul>
</td> </td>
</tr> </tr>

View File

@ -21,54 +21,41 @@
{% for entity in entities %} {% for entity in entities %}
<div class="item-bloc"> <div class="item-bloc">
<div class="item-row wrap-header"> <div class="item-row">
<div class="wrap-list">
<div class="item-col"> <div class="wl-row">
<h3> <div class="wl-col">
{{ entity.title }} <h2>{{ entity.title }}</h2>
</h3> </div>
<div> <div class="wl-col">
{% if entity.startDate %} <p>
<span>{{ entity.startDate|format_date('long') }}</span> {% if entity.startDate %}
{% endif %} <span>{{ entity.startDate|format_date('long') }}</span>
{% if entity.endDate %} {% endif %}
<span> - {{ entity.endDate|format_date('long') }}</span> {% if entity.endDate %}
{% endif %} <span> - {{ entity.endDate|format_date('long') }}</span>
</div> {% endif %}
</div> </p>
<div class="item-col" style="justify-content: flex-end;">
<div class="box">
<div>
{# <blockquote class="chill-user-quote">#}
{{ entity.content|u.truncate(350, '…', false)|chill_markdown_to_html }}
{# {% if entity.content|length > 350 %}#}
{# <a href="{{ chill_path_add_return_path('chill_main_news_items_history', {'news_item_id': entity.id}) }}">{{ 'news.read_more'|trans }}</a>#}
{# {% endif %}#}
{# </blockquote>#}
<div class="action">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) }}" class="btn btn-show btn-mini"></a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="item-row separator">
{{ entity.content|u.truncate(350, '… [' ~ ('news.read_more'|trans) ~ '](' ~ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) ~ ')', false)|chill_markdown_to_html }}
</div>
<div class="item-row">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) }}" class="btn btn-show btn-mini"></a>
</li>
</ul>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{{ chill_pagination(paginator) }} {{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_aside_activity_new') }}" class="btn btn-create">
{{ 'Create'|trans }}
</a>
</li>
</ul>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,19 +3,17 @@
{% block title 'news.show_details'|trans %} {% block title 'news.show_details'|trans %}
{% block content %} {% block content %}
<h1>{{ entity.title }}</h1> <div class="col-md-10 col-xxl">
<div class="news-item-show">
<h1>{{ entity.title }}</h1>
<div class="flex-table"> {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %}
<div class="item-row">
<div> <ul class="record_actions sticky-form-buttons">
<span>{{ entity.startDate|format_date('long') }}</span> <li class="cancel">
{% if entity.endDate is not null %} <a href="{{ chill_return_path_or('chill_main_news_items_history') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
<span> - {{ entity.endDate|format_date('long') }}</span> </li>
{% endif %} </ul>
</div> </div>
</div>
<div class="item-row separator">
{{ entity.content|chill_markdown_to_html }}
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,29 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_view_title.html.twig') %}
{% endblock %}
{% block admin_content %}
<div class="col-md-10 col-xxl">
<div class="news-item-show">
<h1>{{ entity.title }}</h1>
{% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_crud_news_item_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_crud_news_item_delete', { 'id': entity.id }) }}" class="btn btn-delete" title="{{ 'delete'|trans }}"></a>
</li>
</ul>
</div>
</div>
{% endblock %}

View File

@ -17,7 +17,9 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class AdminNewsMenuBuilder implements LocalMenuBuilderInterface class AdminNewsMenuBuilder implements LocalMenuBuilderInterface
{ {
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker) {} public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker)
{
}
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters)
{ {

View File

@ -61,7 +61,7 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
]); ]);
} }
$menu->addChild($this->translator->trans('news_history.menu'), [ $menu->addChild($this->translator->trans('news.menu'), [
'route' => 'chill_main_news_items_history', 'route' => 'chill_main_news_items_history',
]) ])
->setExtras([ ->setExtras([

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\NewsItem;
/**
* @implements ChillEntityRenderInterface<NewsItem>
*/
final readonly class NewsItemRender implements ChillEntityRenderInterface
{
public function renderBox($entity, array $options): string
{
return '';
}
public function renderString($entity, array $options): string
{
return $entity->getTitle();
}
public function supports($newsItem, array $options): bool
{
return $newsItem instanceof NewsItem;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
*
* @coversNothing
*/
class NewsItemApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function testListCurrentNewsItems()
{
$client = $this->getClientAuthenticated();
$client->request('GET', '/api/1.0/main/news/current.json');
$this->assertResponseIsSuccessful('Testing whether the GET request to the news item Api endpoint was successful');
$responseContent = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!empty($responseContent['data'][0])) {
$this->assertArrayHasKey('title', $responseContent['data'][0]);
}
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Controller;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Tests the admin pages for news items.
*
* @internal
*
* @coversNothing
*/
class NewsItemControllerTest extends WebTestCase
{
use PrepareClientTrait;
protected function tearDown(): void
{
self::ensureKernelShutdown();
}
public function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$qb = $em->createQueryBuilder();
$newsItems = $qb->select('n')->from(NewsItem::class, 'n')
->setMaxResults(2)
->getQuery()
->getResult();
foreach ($newsItems as $n) {
yield [$n->getId()];
}
self::ensureKernelShutdown();
}
public function testList()
{
$client = $this->getClientAuthenticated();
$client->request('GET', '/fr/admin/news_item');
self::assertResponseIsSuccessful('Test that news item admin page shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(int $newsItemId)
{
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/amdin/news_item/{$newsItemId}/view");
$this->assertResponseIsSuccessful('test that single news item admin page loads successfully');
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Controller;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
*
* @coversNothing
*/
class NewsItemsHistoryControllerTest extends WebTestCase
{
use PrepareClientTrait;
protected function tearDown(): void
{
self::ensureKernelShutdown();
}
public function generateNewsItemIds(): iterable
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$qb = $em->createQueryBuilder();
$newsItems = $qb->select('n')->from(NewsItem::class, 'n')
->setMaxResults(2)
->getQuery()
->getResult();
foreach ($newsItems as $n) {
yield [$n->getId()];
}
self::ensureKernelShutdown();
}
public function testList()
{
$client = $this->getClientAuthenticated();
$client->request('GET', '/fr/news-items/history');
self::assertResponseIsSuccessful('Test that /fr/news-items history shows');
}
/**
* @dataProvider generateNewsItemIds
*/
public function testShowSingleItem(int $newsItemId)
{
$client = $this->getClientAuthenticated();
$client->request('GET', "/fr/news-items/{$newsItemId}");
$this->assertResponseIsSuccessful('test that single news item page loads successfully');
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Repository;
use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Repository\NewsItemRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Clock\ClockInterface;
/**
* @internal
*
* @coversNothing
*/
class NewsItemRepositoryTest extends KernelTestCase
{
private function getNewsItemsRepository(): NewsItemRepository
{
return self::$container->get(NewsItemRepository::class);
}
public function testFindCurrentNews()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$repository = $this->getNewsItemsRepository();
$mockClock = $this->createMock(ClockInterface::class);
$mockClock->expects($this->once())->method('now')->willReturn(new \DateTime('2023-01-10'));
$newsItem1 = new NewsItem();
$newsItem1->setTitle('This is a mock news item');
$newsItem1->setContent('We are testing that the repository returns the correct news items');
$newsItem1->setStartDate(new \DateTimeImmutable('2023-01-01'));
$newsItem1->setEndDate(new \DateTimeImmutable('2023-01-05'));
$newsItem2 = new NewsItem();
$newsItem2->setTitle('This is a mock news item');
$newsItem2->setContent('We are testing that the repository returns the correct news items');
$newsItem2->setStartDate(new \DateTimeImmutable('2023-12-15'));
$newsItem2->setEndDate($mockClock->now());
$newsItem3 = new NewsItem();
$newsItem3->setTitle('This is a mock news item');
$newsItem3->setContent('We are testing that the repository returns the correct news items');
$newsItem3->setStartDate(new \DateTimeImmutable('2033-11-03'));
$newsItem3->setEndDate(null);
$em->persist($newsItem1);
$em->persist($newsItem2);
$em->persist($newsItem3);
$em->flush();
// Call the method to test
$result = $repository->findCurrentNews();
// Assertions
$this->assertCount(2, $result);
$this->assertInstanceOf(NewsItem::class, $result[0]);
$this->assertContains($newsItem2, $result);
$this->assertContains($newsItem3, $result);
}
}

View File

@ -892,11 +892,11 @@ paths:
403: 403:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/news.json: /1.0/main/news/current.json:
get: get:
tags: tags:
- news items - news items
summary: Returns a list of news items summary: Returns a list of news items which are valid
responses: responses:
200: 200:
description: "ok" description: "ok"

View File

@ -47,6 +47,8 @@ services:
Chill\MainBundle\Templating\Entity\AddressRender: ~ Chill\MainBundle\Templating\Entity\AddressRender: ~
Chill\MainBundle\Templating\Entity\NewsItemRender: ~
Chill\MainBundle\Templating\Entity\UserRender: ~ Chill\MainBundle\Templating\Entity\UserRender: ~
Chill\MainBundle\Templating\Listing\: Chill\MainBundle\Templating\Listing\:

View File

@ -442,7 +442,11 @@ crud:
title: Liste des actualités title: Liste des actualités
add_new: Créer une nouvelle actualité add_new: Créer une nouvelle actualité
title_new: Nouvelle actualité title_new: Nouvelle actualité
title_view: Voir l'actualité
title_edit: Modifier une actualité title_edit: Modifier une actualité
title_delete: Supprimer une actualité
button_delete: Supprimer
confirm_message_delete: Êtes-vous sûr de vouloir supprimer l'actualité, "%as_string%" ?
No entities: Aucun élément No entities: Aucun élément
@ -693,10 +697,11 @@ admin:
news: news:
noDate: Pas de date de fin noDate: Pas de date de fin
startDate: Date de début startDate: Date de début
endDate: Date de fin endDate: Date de fin de publication sur la page d'accueil
title: Historique des actualités title: Historique des actualités
menu: Actualités menu: Actualités
no_data: Aucune actualité no_data: Aucune actualité
read_more: Lire la suite read_more: Lire la suite
show_details: Voir l'actualité