diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 945d13532..43f687649 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,7 @@ variables: # force a timezone TZ: Europe/Brussels # avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations - SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=0 + SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1 stages: - Composer install diff --git a/src/Bundle/ChillMainBundle/Controller/AdminController.php b/src/Bundle/ChillMainBundle/Controller/AdminController.php index 7d3826823..46fbfb351 100644 --- a/src/Bundle/ChillMainBundle/Controller/AdminController.php +++ b/src/Bundle/ChillMainBundle/Controller/AdminController.php @@ -47,4 +47,12 @@ class AdminController extends AbstractController { return $this->render('@ChillMain/Admin/indexUser.html.twig'); } + + /** + * @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin") + */ + public function indexDashboardAction() + { + return $this->render('@ChillMain/Admin/indexDashboard.html.twig'); + } } diff --git a/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php new file mode 100644 index 000000000..8a6027ff3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php @@ -0,0 +1,52 @@ +newsItemRepository->countCurrentNews()) { + // show news only if we have news + // NOTE: maybe this should be done in the frontend... + $data[] = + [ + 'position' => 'top-left', + 'id' => 1, + 'type' => 'news', + 'metadata' => [ + // arbitrary data that will be store "some time" + 'only_unread' => false, + ], + ]; + } + + return new JsonResponse($data, JsonResponse::HTTP_OK, []); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php new file mode 100644 index 000000000..786a7e0f1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php @@ -0,0 +1,53 @@ +newsItemRepository->countCurrentNews(); + $paginator = $this->paginatorFactory->create($total); + $newsItems = $this->newsItemRepository->findCurrentNews( + $paginator->getItemsPerPage(), + $paginator->getCurrentPage()->getFirstItemNumber() + ); + + return new JsonResponse($this->serializer->serialize( + new Collection(array_values($newsItems), $paginator), + 'json', + [ + AbstractNormalizer::GROUPS => ['read'], + ] + ), JsonResponse::HTTP_OK, [], true); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php new file mode 100644 index 000000000..a94dd55b9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php @@ -0,0 +1,27 @@ +addOrderBy('e.startDate', 'DESC'); + $query->addOrderBy('e.id', 'DESC'); + + return parent::orderQuery($action, $query, $request, $paginator); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php new file mode 100644 index 000000000..cf1f4922b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php @@ -0,0 +1,73 @@ +buildFilterOrder(); + $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString()); + $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString()); + + $pagination = $this->paginatorFactory->create($total); + + 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(NewsItem $newsItem, Request $request): Response + { + return new Response($this->environment->render( + '@ChillMain/NewsItem/show.html.twig', + [ + 'entity' => $newsItem, + ] + )); + } + + private function buildFilterOrder(): FilterOrderHelper + { + $filterBuilder = $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox(); + + return $filterBuilder->build(); + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index b62f1f2d7..6dfae2fbd 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Controller\CountryController; use Chill\MainBundle\Controller\LanguageController; use Chill\MainBundle\Controller\LocationController; use Chill\MainBundle\Controller\LocationTypeController; +use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserJobApiController; @@ -53,6 +54,7 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer; use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\LocationType; +use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -62,6 +64,7 @@ use Chill\MainBundle\Form\CountryType; use Chill\MainBundle\Form\LanguageType; use Chill\MainBundle\Form\LocationFormType; use Chill\MainBundle\Form\LocationTypeType; +use Chill\MainBundle\Form\NewsItemType; use Chill\MainBundle\Form\RegroupmentType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; @@ -544,6 +547,35 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => NewsItem::class, + 'name' => 'news_item', + 'base_path' => '/admin/news_item', + 'form_class' => NewsItemType::class, + 'controller' => NewsItemController::class, + 'actions' => [ + 'index' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/index.html.twig', + ], + 'new' => [ + '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', + ], + ], + ], ], 'apis' => [ [ diff --git a/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php new file mode 100644 index 000000000..ed9cc07bf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php @@ -0,0 +1,112 @@ +id; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getPosition(): string + { + return $this->position; + } + + public function setPosition(string $position): void + { + $this->position = $position; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function getMetadata(): array + { + return $this->metadata; + } + + public function setMetadata(array $metadata): void + { + $this->metadata = $metadata; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/NewsItem.php b/src/Bundle/ChillMainBundle/Entity/NewsItem.php new file mode 100644 index 000000000..604c58c5f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/NewsItem.php @@ -0,0 +1,128 @@ +title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getStartDate(): ?\DateTimeImmutable + { + return $this->startDate; + } + + public function setStartDate(?\DateTimeImmutable $startDate): void + { + $this->startDate = $startDate; + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/src/Bundle/ChillMainBundle/Form/NewsItemType.php b/src/Bundle/ChillMainBundle/Form/NewsItemType.php new file mode 100644 index 000000000..b6a93a0a0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/NewsItemType.php @@ -0,0 +1,56 @@ +add('title', TextType::class, [ + 'required' => true, + ]) + ->add('content', ChillTextareaType::class, [ + 'required' => false, + ]) + ->add( + 'startDate', + ChillDateType::class, + [ + 'required' => true, + 'input' => 'datetime_immutable', + 'label' => 'news.startDate', + ] + ) + ->add('endDate', ChillDateType::class, [ + 'required' => false, + 'input' => 'datetime_immutable', + 'label' => 'news.endDate', + ]); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', NewsItem::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php new file mode 100644 index 000000000..4552e2a92 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php @@ -0,0 +1,145 @@ +repository = $entityManager->getRepository(NewsItem::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll() + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName() + { + return NewsItem::class; + } + + 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))')) + ->setParameter('pattern', '%'.$pattern.'%'); + } + + return $qb; + } + + public function findAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + $qb + ->addOrderBy('n.startDate', 'DESC') + ->addOrderBy('n.id', 'DESC'); + + return $qb->getQuery()->getResult(); + } + + /** + * @return list + */ + public function findCurrentNews(?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryCurrentNews(); + $qb->addOrderBy('n.startDate', 'DESC'); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb + ->getQuery() + ->getResult(); + } + + public function countAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + + return $qb + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function countCurrentNews() + { + return $this->buildQueryCurrentNews() + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + private function buildQueryCurrentNews(): QueryBuilder + { + $now = $this->clock->now(); + + $qb = $this->createQueryBuilder('n'); + $qb + ->where( + $qb->expr()->andX( + $qb->expr()->lte('n.startDate', ':now'), + $qb->expr()->orX( + $qb->expr()->gt('n.endDate', ':now'), + $qb->expr()->isNull('n.endDate') + ) + ) + ) + ->setParameter('now', $now); + + return $qb; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js new file mode 100644 index 000000000..67aac616f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js @@ -0,0 +1 @@ +import './index.scss'; diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss new file mode 100644 index 000000000..7b65cda80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss @@ -0,0 +1,7 @@ +div.flex-table { + .news-content { + p { + margin-top: 1rem; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index b31b70897..2e33b8248 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -160,3 +160,11 @@ export interface LocationType { contactData: "optional" | "required"; title: TranslatableString; } + +export interface NewsItemType { + id: number; + title: string; + content: string; + startDate: DateTime; + endDate: DateTime | null; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue index 315fd863f..02763221a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue @@ -97,6 +97,8 @@ import MyNotifications from './MyNotifications'; import MyWorkflows from './MyWorkflows.vue'; import TabCounter from './TabCounter'; import { mapState } from "vuex"; +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; + export default { name: "App", @@ -112,7 +114,7 @@ export default { }, data() { return { - activeTab: 'MyCustoms' + activeTab: 'MyCustoms', } }, computed: { @@ -126,8 +128,11 @@ export default { }, methods: { selectTab(tab) { - this.$store.dispatch('getByTab', { tab: tab }); + if (tab !== 'MyCustoms') { + this.$store.dispatch('getByTab', { tab: tab }); + } this.activeTab = tab; + console.log(this.activeTab) } }, mounted() { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue new file mode 100644 index 000000000..ff1cae89b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue new file mode 100644 index 000000000..bbad4315c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue index 5e9cb79df..43ad31c45 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue @@ -1,76 +1,73 @@ @@ -98,4 +103,10 @@ span.counter { background-color: unset; } } - \ No newline at end of file + +div.news { + max-height: 22rem; + overflow: hidden; + overflow-y: scroll; +} + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js index c4c97e5c6..ddc1b55ef 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js @@ -63,7 +63,15 @@ const appMessages = { }, emergency: "Urgent", confidential: "Confidentiel", - automatic_notification: "Notification automatique" + automatic_notification: "Notification automatique", + widget: { + news: { + title: "Actualités", + readMore: "Lire la suite", + date: "Date", + none: "Aucune actualité" + } + } } }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js index 088cb93b7..1579a3d0c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js @@ -96,13 +96,11 @@ const store = createStore({ }, catchError(state, error) { state.errorMsg.push(error); - } + }, }, actions: { getByTab({ commit, getters }, { tab, param }) { switch (tab) { - case 'MyCustoms': - break; // case 'MyWorks': // if (!getters.isWorksLoaded) { // commit('setLoading', true); @@ -221,8 +219,8 @@ const store = createStore({ default: throw 'tab '+ tab; } - } + }, }, }); -export { store }; \ No newline at end of file +export { store }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue index f5e914d27..9a1550d7d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -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(); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts index c509ac10f..f81699a7c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts @@ -51,7 +51,7 @@ const messages = { years_old: "1 an | {n} an | {n} ans", residential_address: "Adresse de résidence", located_at: "réside chez" - } + }, } }; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig new file mode 100644 index 000000000..9c7513d4c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig @@ -0,0 +1,13 @@ +{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %} + +{% block vertical_menu_content %} + {{ chill_menu('admin_news_item', { + 'layout': '@ChillMain/Admin/menu_admin_section.html.twig', + }) }} +{% endblock %} + +{% block layout_wvm_content %} + {% block admin_content %} +

{{ 'admin.dashboard.description' | trans }}

+ {% endblock %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig index 3473dd298..00688ecd9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig @@ -1 +1 @@ -{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }} \ No newline at end of file +{{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig new file mode 100644 index 000000000..11ca95995 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig @@ -0,0 +1,30 @@ +
+
+

+ {{ entity.title }} +

+
+
+

+ {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ 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 }} +
+
+
+
    +
  • + +
  • +
+
+
+ diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig new file mode 100644 index 000000000..5cd830bbc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig @@ -0,0 +1,15 @@ +
+
+

+ {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ entity.content|chill_markdown_to_html }} +
+
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig new file mode 100644 index 000000000..28efd4748 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig @@ -0,0 +1,6 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_delete_content.html.twig' %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig new file mode 100644 index 000000000..4d55c480c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig new file mode 100644 index 000000000..0a197353b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig @@ -0,0 +1,43 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_index.html.twig' %} + {% block table_entities_thead_tr %} + {{ 'Title'|trans }} + {{ 'news.startDate'|trans }} + {{ 'news.endDate'|trans }} + {% endblock %} + {% block table_entities_tbody %} + {% for entity in entities %} + + {{ entity.title }} + {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + {{ entity.endDate|format_date('long') }} + {% else %} + {{ 'news.noDate'|trans }} + {% endif %} + + + + + {% endfor %} + {% endblock %} + + {% block actions_before %} +
  • + {{'Back to the admin'|trans}} +
  • + {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig new file mode 100644 index 000000000..7c204dddd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig new file mode 100644 index 000000000..ff93a9e09 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig @@ -0,0 +1,70 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %} + {{ 'news.title'|trans }} +{% endblock title %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +

    {{ 'news.title'|trans }}

    + + {{ filter_order|chill_render_filter_order_helper }} + + {% if entities|length == 0 %} +

    + {{ "news.no_data"|trans }} +

    + {% else %} + +
    + + {% for entity in entities %} + +
    +
    +
    +
    +
    +

    {{ entity.title }}

    +
    +
    +

    + {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

    +
    +
    +
    +
    + +
    +
    + {{ entity.content|u.truncate(350, '…', false)|chill_markdown_to_html }} +
    +
    + {% if entity.content|length > 350 %} + + {% endif %} +
    + {% endfor %} +
    + + {{ chill_pagination(paginator) }} + {% endif %} +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig new file mode 100644 index 000000000..a718a2121 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig @@ -0,0 +1,24 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title 'news.show_details'|trans %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig new file mode 100644 index 000000000..92ed2c235 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig @@ -0,0 +1,34 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_view_title.html.twig') %} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block admin_content %} + +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    + +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php new file mode 100644 index 000000000..0ce6a9824 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php @@ -0,0 +1,47 @@ +authorizationChecker->isGranted('ROLE_ADMIN')) { + return; + } + + $menu->addChild('admin.dashboard.title', [ + 'route' => 'chill_main_dashboard_admin', + ]) + ->setAttribute('class', 'list-group-item-header') + ->setExtras([ + 'order' => 9000, + ]); + + $menu->addChild('admin.dashboard.news', [ + 'route' => 'chill_crud_news_item_index', + ])->setExtras(['order' => 9000]); + } + + public static function getMenuIds(): array + { + return ['admin_section', 'admin_news_item']; + } +} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php index 6247cf769..a8deba828 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php @@ -60,6 +60,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface 'order' => 20, ]); } + + $menu->addChild($this->translator->trans('news.menu'), [ + 'route' => 'chill_main_news_items_history', + ]) + ->setExtras([ + 'icons' => ['newspaper-o'], + 'order' => 5, + ]); } public static function getMenuIds(): array diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php new file mode 100644 index 000000000..fd2e2e211 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php @@ -0,0 +1,35 @@ + + */ +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; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php new file mode 100644 index 000000000..f8f7aafea --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php @@ -0,0 +1,39 @@ +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]); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php new file mode 100644 index 000000000..5aa515fd1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php @@ -0,0 +1,96 @@ + + */ + private static array $entitiesToDelete = []; + + private readonly EntityManagerInterface $em; + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (self::$entitiesToDelete as [$class, $id]) { + $entity = $em->find($class, $id); + + if (null !== $entity) { + $em->remove($entity); + } + } + + $em->flush(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $newsItem = new NewsItem(); + $newsItem->setTitle('Lorem Ipsum'); + $newsItem->setContent('some text'); + $newsItem->setStartDate(new \DateTimeImmutable('now')); + + $em->persist($newsItem); + $em->flush(); + + self::$entitiesToDelete[] = [NewsItem::class, $newsItem]; + + self::ensureKernelShutdown(); + + yield [$newsItem]; + } + + public function testList() + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', '/fr/admin/news_item'); + + self::assertResponseIsSuccessful('News item admin page shows'); + } + + /** + * @dataProvider generateNewsItemIds + */ + public function testShowSingleItem(NewsItem $newsItem) + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view"); + + self::assertResponseIsSuccessful('Single news item admin page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php new file mode 100644 index 000000000..19da9ac18 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php @@ -0,0 +1,97 @@ + + */ + private static array $toDelete = []; + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (static::$toDelete as [$class, $entity]) { + $query = $em->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + static::$toDelete = []; + + self::ensureKernelShutdown(); + } + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $news = new NewsItem(); + + $news->setContent('test content'); + $news->setTitle('Title'); + $news->setStartDate(new \DateTimeImmutable('yesterday')); + + $em->persist($news); + $em->flush(); + + static::$toDelete[] = [NewsItem::class, $news]; + + self::ensureKernelShutdown(); + + yield [$news->getId()]; + } + + public function testList() + { + self::ensureKernelShutdown(); + $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) + { + self::ensureKernelShutdown(); + $client = $this->getClientAuthenticated(); + + $client->request('GET', "/fr/news-items/{$newsItemId}"); + + $this->assertResponseIsSuccessful('test that single news item page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php new file mode 100644 index 000000000..7aab97e48 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php @@ -0,0 +1,108 @@ + + */ + private array $toDelete = []; + + protected function setUp(): void + { + self::bootKernel(); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + } + + protected function tearDown(): void + { + foreach ($this->toDelete as [$class, $entity]) { + $query = $this->entityManager->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + $this->toDelete = []; + } + + private function getNewsItemsRepository(ClockInterface $clock): NewsItemRepository + { + return new NewsItemRepository($this->entityManager, $clock); + } + + public function testFindCurrentNews() + { + $clock = new MockClock($now = new \DateTimeImmutable('2023-01-10')); + $repository = $this->getNewsItemsRepository($clock); + + $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-01-01')); + $newsItem2->setEndDate($now->add(new \DateInterval('P1D'))); + + $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); + + $newsItem4 = new NewsItem(); + $newsItem4->setTitle('This is a mock news item'); + $newsItem4->setContent('We are testing that the repository returns the correct news items'); + $newsItem4->setStartDate(new \DateTimeImmutable('2023-01-03')); + $newsItem4->setEndDate(null); + + $this->entityManager->persist($newsItem1); + $this->entityManager->persist($newsItem2); + $this->entityManager->persist($newsItem3); + $this->entityManager->persist($newsItem4); + $this->entityManager->flush(); + + $this->toDelete = [ + [NewsItem::class, $newsItem1], + [NewsItem::class, $newsItem2], + [NewsItem::class, $newsItem3], + [NewsItem::class, $newsItem4], + ]; + + // 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($newsItem4, $result); + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 98e0e915e..f37ee723d 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -10,6 +10,12 @@ servers: components: schemas: + Date: + type: object + properties: + datetime: + type: string + format: date-time User: type: object properties: @@ -131,6 +137,35 @@ components: id: type: integer + DashboardConfigItem: + type: object + properties: + id: + type: integer + type: + type: string + metadata: + type: object + userId: + type: integer + position: + type: string + + NewsItem: + type: object + properties: + id: + type: integer + title: + type: string + content: + type: string + startDate: + $ref: "#/components/schemas/Date" + endDate: + $ref: "#/components/schemas/Date" + + paths: /1.0/search.json: get: @@ -842,4 +877,34 @@ paths: $ref: '#/components/schemas/Workflow' 403: description: "Unauthorized" + /1.0/main/dashboard-config-item.json: + get: + tags: + - dashboard config item + summary: Returns the dashboard configuration for the current user. + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardConfigItem' + 403: + description: "Unauthorized" + /1.0/main/news/current.json: + get: + tags: + - news items + summary: Returns a list of news items which are valid + responses: + 200: + description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NewsItem' + 403: + description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 792d0a27e..7e0060892 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -76,6 +76,7 @@ module.exports = function(encore, entries) encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js'); encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js'); encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index'); + encore.addEntry('mod_news', __dirname + '/Resources/public/module/news/index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index e69700732..0baa91b69 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -47,6 +47,8 @@ services: Chill\MainBundle\Templating\Entity\AddressRender: ~ + Chill\MainBundle\Templating\Entity\NewsItemRender: ~ + Chill\MainBundle\Templating\Entity\UserRender: ~ Chill\MainBundle\Templating\Listing\: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php new file mode 100644 index 000000000..d4fe9b561 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php @@ -0,0 +1,55 @@ +addSql('CREATE SEQUENCE chill_main_dashboard_config_item_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_main_news_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_dashboard_config_item (id INT NOT NULL, user_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, position VARCHAR(255) NOT NULL, metadata JSONB DEFAULT \'{}\'::jsonb, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CF59DFD6A76ED395 ON chill_main_dashboard_config_item (user_id)'); + $this->addSql('CREATE TABLE chill_main_news (id INT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_96922AFB3174800F ON chill_main_news (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_96922AFB65FF1AEC ON chill_main_news (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_news.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item ADD CONSTRAINT FK_CF59DFD6A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_dashboard_config_item_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_main_news_id_seq CASCADE'); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item DROP CONSTRAINT FK_CF59DFD6A76ED395'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB3174800F'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB65FF1AEC'); + $this->addSql('DROP TABLE chill_main_dashboard_config_item'); + $this->addSql('DROP TABLE chill_main_news'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 91068275f..421cac473 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -82,7 +82,6 @@ Comment: Commentaire Comments: Commentaires Pinned comment: Commentaire épinglé Any comment: Aucun commentaire -Read more: Lire la suite (more...): (suite...) # comment embeddable @@ -438,6 +437,16 @@ crud: add_new: Ajouter un centre title_new: Nouveau centre title_edit: Modifier un centre + news_item: + index: + 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 @@ -679,3 +688,20 @@ admin: undefined: non défini user: Utilisateur scope: Service + dashboard: + title: Tableau de bord + news: Actualités + description: Configuration du tableau de bord + + +news: + noDate: Pas de date de fin + startDate: Date de début de publication + 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é + +