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",
"@types/dompurify": "^3.0.5",
"bindings": "^1.5.0",
"bootstrap": "^5.3.0",
"chokidar": "^3.5.1",
"fork-awesome": "^1.1.7",
"jquery": "^3.6.0",
@ -35,6 +34,7 @@
"webpack-cli": "^5.0.1"
},
"dependencies": {
"bootstrap": "~5.2.0",
"@fullcalendar/core": "^6.1.4",
"@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/interaction": "^6.1.4",

View File

@ -10,7 +10,7 @@
<php>
<ini name="error_reporting" value="-1" />
<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" />
<env name="KERNEL_CLASS" value="\App\Kernel" />
</php>

View File

@ -22,23 +22,24 @@ use Symfony\Component\Serializer\SerializerInterface;
class NewsItemApiController
{
public function __construct(
private NewsItemRepository $newsItemRepository,
private SerializerInterface $serializer,
private PaginatorFactory $paginatorFactory
) {}
private readonly NewsItemRepository $newsItemRepository,
private readonly SerializerInterface $serializer,
private readonly PaginatorFactory $paginatorFactory
) {
}
/**
* 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
{
$total = $this->newsItemRepository->countWithDateFilter();
$total = $this->newsItemRepository->countCurrentNews();
$paginator = $this->paginatorFactory->create($total);
$newsItems = $this->newsItemRepository->findWithDateFilter(
$limit = $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
$newsItems = $this->newsItemRepository->findCurrentNews(
$paginator->getItemsPerPage(),
$paginator->getCurrentPage()->getFirstItemNumber()
);
return new JsonResponse($this->serializer->serialize(

View File

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

View File

@ -562,10 +562,18 @@ class ChillMainExtension extends Extension implements
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/new.html.twig',
],
'view' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/NewsItem/view_admin.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'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;
/**
* @ORM\Column(type="json" "jsonb"=true, options={"default": "[]"})
* @ORM\Column(type="json", options={"default": "[]", "jsonb": true})
*
* @Serializer\Groups({"dashboardConfigItem:read"})
*/

View File

@ -22,7 +22,7 @@ class NewsItemRepository implements ObjectRepository
{
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);
}
@ -57,11 +57,14 @@ class NewsItemRepository implements ObjectRepository
return NewsItem::class;
}
public function buildBaseQuery(
private function buildBaseQuery(
string $pattern = null
): QueryBuilder {
$qb = $this->createQueryBuilder('n');
$qb->where('n.startDate <= :now');
$qb->setParameter('now', $this->clock->now());
if (null !== $pattern && '' !== $pattern) {
$qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))'))
->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))'))
@ -71,24 +74,28 @@ class NewsItemRepository implements ObjectRepository
return $qb;
}
public function findAllFilteredByUser(string $pattern = null)
public function findAllFilteredBySearchTerm(string $pattern = null)
{
$qb = $this->buildBaseQuery($pattern);
$qb->addOrderBy('n.startDate', 'DESC')
$qb
->addOrderBy('n.startDate', 'DESC')
->addOrderBy('n.id', 'DESC');
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);
}
if ($offset) {
if (null !== $offset) {
$qb->setFirstResult($offset);
}
@ -97,7 +104,7 @@ class NewsItemRepository implements ObjectRepository
->getResult();
}
public function countAllFilteredByUser(string $pattern = null)
public function countAllFilteredBySearchTerm(string $pattern = null)
{
$qb = $this->buildBaseQuery($pattern);
@ -107,15 +114,15 @@ class NewsItemRepository implements ObjectRepository
->getSingleScalarResult();
}
public function countWithDateFilter()
public function countCurrentNews()
{
return $this->buildQueryWithDateFilter()
return $this->buildQueryCurrentNews()
->select('COUNT(n)')
->getQuery()
->getSingleScalarResult();
}
public function buildQueryWithDateFilter(): QueryBuilder
private function buildQueryCurrentNews(): QueryBuilder
{
$now = $this->clock->now();

View File

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

View File

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

View File

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

View File

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

View File

@ -40,9 +40,9 @@
</div>
<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'"/>
</div>
</template>
</div>
</div>
@ -64,7 +64,8 @@ export default {
counterClass: {
counter: true //hack to pass class 'counter' in i18n-t
},
dashboardItems: []
dashboardItems: [],
masonry: null,
}
},
computed: {
@ -75,7 +76,7 @@ export default {
},
mounted() {
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
makeFetch('GET', '/api/1.0/main/dashboard-config-item.json')
.then((response) => {
@ -84,8 +85,13 @@ export default {
})
.catch((error) => {
throw error
})
}
});
},
updated() {
this.masonry.layout();
}
}
</script>

View File

@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue";
export interface AddressModalContentProps {
address_id: number;
address_ref_status: AddressRefStatus | null;
address_ref_status: AddressRefStatus;
}
const data = reactive<{
loading: boolean,
working_address: Address | null,
working_ref_status: AddressRefStatus | null,
}>({
interface AddressModalData {
loading: boolean,
working_address: Address | null,
working_ref_status: AddressRefStatus | null,
}
const data: AddressModalData = reactive({
loading: false,
working_address: null,
working_ref_status: null,
});
} as AddressModalData);
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 %}
<tr>
<td>{{ entity.title }}</td>
<td>{{ entity.startDate|date }}</td>
<td>{{ entity.startDate|format_date('long') }}</td>
{% if entity.endDate is not null %}
<td>{{ entity.endDate|date }}</td>
<td>{{ entity.endDate|format_date('long') }}</td>
{% else %}
<td>{{ 'news.noDate'|trans }}</td>
{% endif %}
<td>
<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>
<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>
</td>
</tr>

View File

@ -21,54 +21,41 @@
{% for entity in entities %}
<div class="item-bloc">
<div class="item-row wrap-header">
<div class="item-col">
<h3>
{{ entity.title }}
</h3>
<div>
{% if entity.startDate %}
<span>{{ entity.startDate|format_date('long') }}</span>
{% endif %}
{% if entity.endDate %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</div>
</div>
<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 class="item-row">
<div class="wrap-list">
<div class="wl-row">
<div class="wl-col">
<h2>{{ entity.title }}</h2>
</div>
<div class="wl-col">
<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>
</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>
{% endfor %}
</div>
{{ 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 %}
</div>
{% endblock %}

View File

@ -3,19 +3,17 @@
{% block title 'news.show_details'|trans %}
{% 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">
<div class="item-row">
<div>
<span>{{ entity.startDate|format_date('long') }}</span>
{% if entity.endDate is not null %}
<span> - {{ entity.endDate|format_date('long') }}</span>
{% endif %}
</div>
</div>
<div class="item-row separator">
{{ entity.content|chill_markdown_to_html }}
</div>
{% 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_main_news_items_history') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
</li>
</ul>
</div>
</div>
{% 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
{
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker) {}
public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker)
{
}
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',
])
->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:
description: "Unauthorized"
/1.0/main/news.json:
/1.0/main/news/current.json:
get:
tags:
- news items
summary: Returns a list of news items
summary: Returns a list of news items which are valid
responses:
200:
description: "ok"

View File

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

View File

@ -442,7 +442,11 @@ crud:
title: Liste des actualités
add_new: Créer une nouvelle actualité
title_new: Nouvelle actualité
title_view: Voir l'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
@ -693,10 +697,11 @@ admin:
news:
noDate: Pas de date de fin
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
menu: Actualités
no_data: Aucune actualité
read_more: Lire la suite
show_details: Voir l'actualité