From a91e10a84a06cd668b710b478438917b39bf7bfc Mon Sep 17 00:00:00 2001 From: meyerlor Date: Sun, 22 Feb 2026 21:44:18 +0100 Subject: [PATCH] Add project announcements feature Allows admins to create popup announcements shown to users when loading maps. Supports targeting by repository, project, and user group with configurable display count limits. Includes admin CRUD UI, frontend modal display via Bootstrap 5, and DB schema for both PostgreSQL and SQLite. --- assets/src/modules/Announcement.js | 149 +++++++++++ assets/src/modules/Lizmap.js | 4 + .../modules/admin/classes/config.listener.php | 11 + .../controllers/announcement.classic.php | 238 ++++++++++++++++++ lizmap/modules/admin/install/install.php | 2 + .../install/upgrade_announcement_acl.php | 24 ++ .../locales/en_US/admin.UTF-8.properties | 42 ++++ .../locales/en_US/jacl2.UTF-8.properties | 1 + .../admin/templates/announcement_form.tpl | 64 +++++ .../admin/templates/announcement_list.tpl | 55 ++++ lizmap/modules/admin/urls.xml | 7 + .../controllers/announcement.classic.php | 128 ++++++++++ .../modules/lizmap/daos/announcement.dao.xml | 34 +++ .../lizmap/daos/announcementView.dao.xml | 23 ++ lizmap/modules/lizmap/install/install.php | 3 + .../install/sql/lizannouncement.pgsql.sql | 21 ++ .../install/sql/lizannouncement.sqlite.sql | 19 ++ .../lizmap/install/upgrade_announcement.php | 17 ++ lizmap/modules/lizmap/urls.xml | 3 + .../view/controllers/lizAjax.classic.php | 2 + .../view/controllers/lizMap.classic.php | 2 + 21 files changed, 849 insertions(+) create mode 100644 assets/src/modules/Announcement.js create mode 100644 lizmap/modules/admin/controllers/announcement.classic.php create mode 100644 lizmap/modules/admin/install/upgrade_announcement_acl.php create mode 100644 lizmap/modules/admin/templates/announcement_form.tpl create mode 100644 lizmap/modules/admin/templates/announcement_list.tpl create mode 100644 lizmap/modules/lizmap/controllers/announcement.classic.php create mode 100644 lizmap/modules/lizmap/daos/announcement.dao.xml create mode 100644 lizmap/modules/lizmap/daos/announcementView.dao.xml create mode 100644 lizmap/modules/lizmap/install/sql/lizannouncement.pgsql.sql create mode 100644 lizmap/modules/lizmap/install/sql/lizannouncement.sqlite.sql create mode 100644 lizmap/modules/lizmap/install/upgrade_announcement.php diff --git a/assets/src/modules/Announcement.js b/assets/src/modules/Announcement.js new file mode 100644 index 0000000000..546032f047 --- /dev/null +++ b/assets/src/modules/Announcement.js @@ -0,0 +1,149 @@ +/** + * @module modules/Announcement.js + * @name Announcement + * @copyright 2026 3Liz + * @license MPL-2.0 + */ + +/** + * Fetches pending announcements from the server and displays them + * sequentially in the #lizmap-modal Bootstrap 5 modal. + */ +export default class Announcement { + + init() { + const announcementUrl = globalThis['lizUrls']?.announcement; + if (!announcementUrl) { + return; + } + + const params = globalThis['lizUrls'].params || {}; + + const url = new URL(announcementUrl, window.location.origin); + if (params.repository) { + url.searchParams.set('repository', params.repository); + } + if (params.project) { + url.searchParams.set('project', params.project); + } + + fetch(url.toString()) + .then(response => response.json()) + .then(data => { + if (data.announcements && data.announcements.length > 0) { + this._showAnnouncements(data.announcements); + } + }) + .catch(err => { + console.warn('Failed to fetch announcements:', err); + }); + } + + /** + * Show announcements one at a time. + * @param {Array} announcements + * @param {number} index + */ + _showAnnouncements(announcements, index = 0) { + if (index >= announcements.length) { + return; + } + + const announcement = announcements[index]; + this._showModal(announcement, () => { + this._markSeen(announcement.id); + // Show next announcement after a short delay + if (index + 1 < announcements.length) { + setTimeout(() => { + this._showAnnouncements(announcements, index + 1); + }, 500); + } + }); + } + + /** + * Display a single announcement in the #lizmap-modal. + * @param {Object} announcement + * @param {Function} onClose + */ + _showModal(announcement, onClose) { + const modalEl = document.getElementById('lizmap-modal'); + if (!modalEl) { + return; + } + + modalEl.innerHTML = ` + + `; + + const handleHidden = () => { + modalEl.removeEventListener('hidden.bs.modal', handleHidden); + if (onClose) { + onClose(); + } + }; + modalEl.addEventListener('hidden.bs.modal', handleHidden); + + // Use Bootstrap 5 Modal API if available, fallback to jQuery + if (globalThis.bootstrap && globalThis.bootstrap.Modal) { + const modal = new globalThis.bootstrap.Modal(modalEl); + modal.show(); + } else if (globalThis.jQuery) { + globalThis.jQuery(modalEl).modal('show'); + } + } + + /** + * Tell the server the user has seen this announcement. + * @param {number} announcementId + */ + _markSeen(announcementId) { + const markSeenUrl = globalThis['lizUrls']?.announcementMarkSeen; + if (!markSeenUrl) { + // Store in sessionStorage as fallback for anonymous users + try { + const key = 'lizmap_announcement_seen'; + const seen = JSON.parse(sessionStorage.getItem(key) || '[]'); + if (!seen.includes(announcementId)) { + seen.push(announcementId); + sessionStorage.setItem(key, JSON.stringify(seen)); + } + } catch (e) { + // sessionStorage not available + } + return; + } + + const url = new URL(markSeenUrl, window.location.origin); + url.searchParams.set('id', announcementId); + + fetch(url.toString(), { method: 'POST' }) + .catch(err => { + console.warn('Failed to mark announcement as seen:', err); + }); + } + + /** + * Escape HTML special characters for safe text display. + * @param {string} text + * @returns {string} + */ + _escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} diff --git a/assets/src/modules/Lizmap.js b/assets/src/modules/Lizmap.js index 881428d4d4..da4015f68b 100644 --- a/assets/src/modules/Lizmap.js +++ b/assets/src/modules/Lizmap.js @@ -28,6 +28,7 @@ import Permalink from './Permalink.js'; import Search from './Search.js'; import Tooltip from './Tooltip.js'; import LocateByLayer from './LocateByLayer.js'; +import Announcement from './Announcement.js'; import WMSCapabilities from 'ol/format/WMSCapabilities.js'; import WFSCapabilities from 'ol-wfs-capabilities'; @@ -199,6 +200,9 @@ export default class Lizmap { * ['lizmap.modules.initialized'] * ); */ + this.announcement = new Announcement(); + this.announcement.init(); + eventDispatcher.dispatch('lizmap.modules.initialized'); } }; diff --git a/lizmap/modules/admin/classes/config.listener.php b/lizmap/modules/admin/classes/config.listener.php index bc3c62a217..00bd62acb7 100644 --- a/lizmap/modules/admin/classes/config.listener.php +++ b/lizmap/modules/admin/classes/config.listener.php @@ -51,6 +51,17 @@ public function onmasteradminGetMenuContent($event) ); } + // Configure project announcements + if (jAcl2::check('lizmap.admin.announcement.manage')) { + $bloc->childItems[] = new masterAdminMenuItem( + 'lizmap_announcements', + jLocale::get('admin~admin.menu.lizmap.announcements.label'), + jUrl::get('admin~announcement:index'), + 116, + 'lizmap' + ); + } + // Configure the theme if (jAcl2::check('lizmap.admin.theme.update')) { $bloc->childItems[] = new masterAdminMenuItem( diff --git a/lizmap/modules/admin/controllers/announcement.classic.php b/lizmap/modules/admin/controllers/announcement.classic.php new file mode 100644 index 0000000000..55438ff24f --- /dev/null +++ b/lizmap/modules/admin/controllers/announcement.classic.php @@ -0,0 +1,238 @@ + array('jacl2.right' => 'lizmap.admin.announcement.manage'), + ); + + /** + * List all announcements. + * + * @return jResponseHtml + */ + public function index() + { + /** @var jResponseHtml $rep */ + $rep = $this->getResponse('html'); + + $dao = jDao::get('lizmap~announcement', 'lizlog'); + $announcements = $dao->findAll(); + $conditions = jDao::createConditions(); + $announcementCount = $dao->countBy($conditions); + + $tpl = new jTpl(); + $tpl->assign('announcements', $announcements); + $tpl->assign('announcementCount', $announcementCount); + $rep->body->assign('MAIN', $tpl->fetch('announcement_list')); + $rep->body->assign('selectedMenuItem', 'lizmap_announcements'); + + return $rep; + } + + /** + * Display the create form. + * + * @return jResponseHtml + */ + public function create() + { + /** @var jResponseHtml $rep */ + $rep = $this->getResponse('html'); + + $tpl = new jTpl(); + $tpl->assign('id', 0); + $tpl->assign('title', ''); + $tpl->assign('content', ''); + $tpl->assign('target_repository', ''); + $tpl->assign('target_project', ''); + $tpl->assign('target_groups', ''); + $tpl->assign('selectedGroupsArray', array()); + $tpl->assign('max_display_count', 1); + $tpl->assign('is_active', true); + $tpl->assign('repositories', lizmap::getRepositoryList()); + $tpl->assign('groups', jAcl2DbUserGroup::getGroupList()); + $rep->body->assign('MAIN', $tpl->fetch('announcement_form')); + $rep->body->assign('selectedMenuItem', 'lizmap_announcements'); + + return $rep; + } + + /** + * Display the edit form. + * + * @return jResponseHtml|jResponseRedirect + */ + public function edit() + { + $id = $this->intParam('id'); + if (!$id) { + /** @var jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + $rep->action = 'admin~announcement:index'; + + return $rep; + } + + /** @var jResponseHtml $rep */ + $rep = $this->getResponse('html'); + + $dao = jDao::get('lizmap~announcement', 'lizlog'); + $record = $dao->get($id); + + if (!$record) { + /** @var jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + jMessage::add(jLocale::get('admin~admin.announcement.error.notfound'), 'error'); + $rep->action = 'admin~announcement:index'; + + return $rep; + } + + $tpl = new jTpl(); + $selectedGroupsArray = !empty($record->target_groups) + ? array_map('trim', explode(',', $record->target_groups)) + : array(); + + $tpl->assign('id', $record->id); + $tpl->assign('title', $record->title); + $tpl->assign('content', $record->content); + $tpl->assign('target_repository', $record->target_repository); + $tpl->assign('target_project', $record->target_project); + $tpl->assign('target_groups', $record->target_groups); + $tpl->assign('selectedGroupsArray', $selectedGroupsArray); + $tpl->assign('max_display_count', $record->max_display_count); + $tpl->assign('is_active', $record->is_active); + $tpl->assign('repositories', lizmap::getRepositoryList()); + $tpl->assign('groups', jAcl2DbUserGroup::getGroupList()); + $rep->body->assign('MAIN', $tpl->fetch('announcement_form')); + $rep->body->assign('selectedMenuItem', 'lizmap_announcements'); + + return $rep; + } + + /** + * Save an announcement (create or update). + * + * @return jResponseRedirect + */ + public function save() + { + /** @var jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + + $id = $this->intParam('id'); + $title = $this->param('title'); + $content = $this->param('content'); + + if (!$title || !$content) { + jMessage::add(jLocale::get('admin~admin.announcement.error.required'), 'error'); + $rep->action = 'admin~announcement:index'; + + return $rep; + } + + $dao = jDao::get('lizmap~announcement', 'lizlog'); + + if ($id) { + // Update existing + $record = $dao->get($id); + if (!$record) { + jMessage::add(jLocale::get('admin~admin.announcement.error.notfound'), 'error'); + $rep->action = 'admin~announcement:index'; + + return $rep; + } + } else { + // Create new + $record = jDao::createRecord('lizmap~announcement', 'lizlog'); + } + + $record->title = htmlspecialchars($title); + $record->content = $content; + $record->target_repository = $this->param('target_repository', ''); + $record->target_project = $this->param('target_project', ''); + + // Handle groups multi-select + $groups = $this->param('target_groups'); + if (is_array($groups)) { + $record->target_groups = implode(',', $groups); + } else { + $record->target_groups = $groups ?: ''; + } + + $record->max_display_count = $this->intParam('max_display_count', 1); + $record->is_active = $this->param('is_active') ? true : false; + + if ($id) { + $dao->update($record); + jMessage::add(jLocale::get('admin~admin.announcement.saved')); + } else { + $dao->insert($record); + jMessage::add(jLocale::get('admin~admin.announcement.created')); + } + + $rep->action = 'admin~announcement:index'; + + return $rep; + } + + /** + * Toggle announcement active status. + * + * @return jResponseRedirect + */ + public function toggle() + { + /** @var jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + + $id = $this->intParam('id'); + if ($id) { + $dao = jDao::get('lizmap~announcement', 'lizlog'); + $record = $dao->get($id); + if ($record) { + $record->is_active = !$record->is_active; + $dao->update($record); + jMessage::add(jLocale::get('admin~admin.announcement.toggled')); + } + } + + $rep->action = 'admin~announcement:index'; + + return $rep; + } + + /** + * Delete an announcement. + * + * @return jResponseRedirect + */ + public function delete() + { + /** @var jResponseRedirect $rep */ + $rep = $this->getResponse('redirect'); + + $id = $this->intParam('id'); + if ($id) { + $dao = jDao::get('lizmap~announcement', 'lizlog'); + $dao->delete($id); + jMessage::add(jLocale::get('admin~admin.announcement.deleted')); + } + + $rep->action = 'admin~announcement:index'; + + return $rep; + } +} diff --git a/lizmap/modules/admin/install/install.php b/lizmap/modules/admin/install/install.php index 7cbdef4995..c9bbc99516 100644 --- a/lizmap/modules/admin/install/install.php +++ b/lizmap/modules/admin/install/install.php @@ -34,6 +34,7 @@ public function install() jAcl2DbManager::createRight('lizmap.admin.server.information.view', 'admin~jacl2.lizmap.admin.server.information.view', 'lizmap.admin.grp'); jAcl2DbManager::createRight('lizmap.admin.lizmap.log.view', 'admin~jacl2.lizmap.admin.lizmap.log.view', 'lizmap.admin.grp'); jAcl2DbManager::createRight('lizmap.admin.lizmap.log.delete', 'admin~jacl2.lizmap.admin.lizmap.log.delete', 'lizmap.admin.grp'); + jAcl2DbManager::createRight('lizmap.admin.announcement.manage', 'admin~jacl2.lizmap.admin.announcement.manage', 'lizmap.admin.grp'); jAcl2DbManager::createRight('lizmap.repositories.view', 'admin~jacl2.lizmap.repositories.view', 'lizmap.grp'); // the right code could be lizmap.view.repository.projects jAcl2DbManager::createRight('lizmap.tools.edition.use', 'admin~jacl2.lizmap.tools.edition.use', 'lizmap.grp'); @@ -57,6 +58,7 @@ public function install() jAcl2DbManager::addRight('admins', 'lizmap.admin.server.information.view'); jAcl2DbManager::addRight('admins', 'lizmap.admin.lizmap.log.view'); jAcl2DbManager::addRight('admins', 'lizmap.admin.lizmap.log.delete'); + jAcl2DbManager::addRight('admins', 'lizmap.admin.announcement.manage'); // Create a new publishers group jAcl2DbUserGroup::createGroup('Publishers', 'publishers'); diff --git a/lizmap/modules/admin/install/upgrade_announcement_acl.php b/lizmap/modules/admin/install/upgrade_announcement_acl.php new file mode 100644 index 0000000000..27198c6f11 --- /dev/null +++ b/lizmap/modules/admin/install/upgrade_announcement_acl.php @@ -0,0 +1,24 @@ +firstExec('acl2_announcement')) { + $this->useDbProfile('auth'); + + // Create announcement management right + jAcl2DbManager::createRight('lizmap.admin.announcement.manage', 'admin~jacl2.lizmap.admin.announcement.manage', 'lizmap.admin.grp'); + + // Assign to admins group + if (jAcl2DbUserGroup::getGroup('admins')) { + jAcl2DbManager::addRight('admins', 'lizmap.admin.announcement.manage'); + } + } + } +} diff --git a/lizmap/modules/admin/locales/en_US/admin.UTF-8.properties b/lizmap/modules/admin/locales/en_US/admin.UTF-8.properties index cec9fa2e83..f0ab7d0ee7 100644 --- a/lizmap/modules/admin/locales/en_US/admin.UTF-8.properties +++ b/lizmap/modules/admin/locales/en_US/admin.UTF-8.properties @@ -9,6 +9,7 @@ menu.lizmap.landingPageContent.label=Landing page content menu.lizmap.logs.label=Lizmap Logs menu.lizmap.logs.view.label=View logs menu.lizmap.search.label=Lizmap search +menu.lizmap.announcements.label=Project Announcements menu.server.label=Server @@ -419,6 +420,47 @@ landingPageContent.unauthed.position.before=Before content for authenticated landingPageContent.unauthed.position.after=After content for authenticated landingPageContent.unauthed.position.no=No +announcement.list.title=Project Announcements +announcement.list.empty=No announcements found. +announcement.create=Create announcement +announcement.create.title=Create announcement +announcement.edit.title=Edit announcement +announcement.col.title=Title +announcement.col.repository=Repository +announcement.col.project=Project +announcement.col.groups=Target groups +announcement.col.maxviews=Max views +announcement.col.status=Status +announcement.col.created=Created +announcement.col.actions=Actions +announcement.status.active=Active +announcement.status.inactive=Inactive +announcement.btn.edit=Edit +announcement.btn.activate=Activate +announcement.btn.deactivate=Deactivate +announcement.btn.delete=Delete +announcement.btn.save=Save +announcement.confirm.delete=Are you sure you want to delete this announcement? +announcement.form.title=Title +announcement.form.content=Content (HTML) +announcement.form.content.help=HTML content that will be displayed in the popup modal. +announcement.form.repository=Target repository +announcement.form.repository.help=Leave empty to target all repositories. +announcement.form.project=Target project +announcement.form.project.help=Leave empty to target all projects in the selected repository. +announcement.form.groups=Target groups +announcement.form.groups.help=Select groups to target. Leave empty to target all users. +announcement.form.maxviews=Max display count +announcement.form.maxviews.help=Maximum number of times this announcement will be shown to each user. 0 means unlimited. +announcement.form.active=Active +announcement.form.all=All +announcement.saved=Announcement updated successfully. +announcement.created=Announcement created successfully. +announcement.toggled=Announcement status toggled. +announcement.deleted=Announcement deleted successfully. +announcement.error.notfound=Announcement not found. +announcement.error.required=Title and content are required. + landingPageContent.error.save=Error while saving the landing page contents landingPageContent.error.submit=An error occurred while saving the content. Your session has been terminated, please retry. landingPageContent.saved=Content of the landing page has been saved diff --git a/lizmap/modules/admin/locales/en_US/jacl2.UTF-8.properties b/lizmap/modules/admin/locales/en_US/jacl2.UTF-8.properties index 6c978aadbd..afa7e3c9f2 100644 --- a/lizmap/modules/admin/locales/en_US/jacl2.UTF-8.properties +++ b/lizmap/modules/admin/locales/en_US/jacl2.UTF-8.properties @@ -15,6 +15,7 @@ lizmap.admin.theme.update=Update the application theme lizmap.admin.server.information.view=View the detailed server information lizmap.admin.lizmap.log.view=View the lizmap logs lizmap.admin.lizmap.log.delete=Delete the lizmap logs +lizmap.admin.announcement.manage=Manage project announcements lizmap.repositories.view=View projects in the repository lizmap.tools.edition.use=Use the Edition tool diff --git a/lizmap/modules/admin/templates/announcement_form.tpl b/lizmap/modules/admin/templates/announcement_form.tpl new file mode 100644 index 0000000000..57ee32fd1e --- /dev/null +++ b/lizmap/modules/admin/templates/announcement_form.tpl @@ -0,0 +1,64 @@ + +

{if $id}{@admin~admin.announcement.edit.title@}{else}{@admin~admin.announcement.create.title@}{/if}

+ +
+
+ + +
+ + +
+ +
+ + +
{@admin~admin.announcement.form.content.help@}
+
+ +
+ + +
{@admin~admin.announcement.form.repository.help@}
+
+ +
+ + +
{@admin~admin.announcement.form.project.help@}
+
+ +
+ + +
{@admin~admin.announcement.form.groups.help@}
+
+ +
+ + +
{@admin~admin.announcement.form.maxviews.help@}
+
+ +
+ + +
+ +
+ + {@admin~admin.configuration.button.back.label@} +
+
+
diff --git a/lizmap/modules/admin/templates/announcement_list.tpl b/lizmap/modules/admin/templates/announcement_list.tpl new file mode 100644 index 0000000000..d7995a11d2 --- /dev/null +++ b/lizmap/modules/admin/templates/announcement_list.tpl @@ -0,0 +1,55 @@ + +

{@admin~admin.announcement.list.title@}

+ +{ifacl2 'lizmap.admin.announcement.manage'} +
+ + + + {if $announcementCount == 0} +

{@admin~admin.announcement.list.empty@}

+ {else} + + + + + + + + + + + + + + + {foreach $announcements as $item} + + + + + + + + + + + {/foreach} + +
{@admin~admin.announcement.col.title@}{@admin~admin.announcement.col.repository@}{@admin~admin.announcement.col.project@}{@admin~admin.announcement.col.groups@}{@admin~admin.announcement.col.maxviews@}{@admin~admin.announcement.col.status@}{@admin~admin.announcement.col.created@}{@admin~admin.announcement.col.actions@}
{$item->title}{$item->target_repository}{$item->target_project}{$item->target_groups}{$item->max_display_count} + {if $item->is_active} + {@admin~admin.announcement.status.active@} + {else} + {@admin~admin.announcement.status.inactive@} + {/if} + {$item->created_at|jdatetime:'db_datetime','lang_datetime'} + {@admin~admin.announcement.btn.edit@} + {if $item->is_active}{@admin~admin.announcement.btn.deactivate@}{else}{@admin~admin.announcement.btn.activate@}{/if} + {@admin~admin.announcement.btn.delete@} +
+ {/if} + +
+{/ifacl2} diff --git a/lizmap/modules/admin/urls.xml b/lizmap/modules/admin/urls.xml index 27b708ad9d..ec6ef357a2 100644 --- a/lizmap/modules/admin/urls.xml +++ b/lizmap/modules/admin/urls.xml @@ -54,4 +54,11 @@ + + + + + + + diff --git a/lizmap/modules/lizmap/controllers/announcement.classic.php b/lizmap/modules/lizmap/controllers/announcement.classic.php new file mode 100644 index 0000000000..fca56cd366 --- /dev/null +++ b/lizmap/modules/lizmap/controllers/announcement.classic.php @@ -0,0 +1,128 @@ +getResponse('json'); + + $repository = $this->param('repository', ''); + $project = $this->param('project', ''); + + $dao = jDao::get('lizmap~announcement', 'lizlog'); + $activeAnnouncements = $dao->findActive(); + + $isConnected = jAuth::isConnected(); + $usr_login = ''; + $userGroups = array(); + + if ($isConnected) { + $usr_login = jAuth::getUserSession()->login; + $groups = jAcl2DbUserGroup::getGroupList($usr_login); + foreach ($groups as $group) { + $userGroups[] = $group->id_aclgrp; + } + } + + $viewDao = jDao::get('lizmap~announcementView', 'lizlog'); + + $pending = array(); + foreach ($activeAnnouncements as $announcement) { + // Filter by repository + if (!empty($announcement->target_repository) && $announcement->target_repository !== $repository) { + continue; + } + + // Filter by project + if (!empty($announcement->target_project) && $announcement->target_project !== $project) { + continue; + } + + // Filter by groups + if (!empty($announcement->target_groups)) { + $targetGroups = explode(',', $announcement->target_groups); + $targetGroups = array_map('trim', $targetGroups); + if (!$isConnected || empty(array_intersect($targetGroups, $userGroups))) { + continue; + } + } + + // Check view count for authenticated users + if ($isConnected && $announcement->max_display_count > 0) { + $view = $viewDao->getByAnnouncementAndUser($announcement->id, $usr_login); + if ($view && $view->view_count >= $announcement->max_display_count) { + continue; + } + } + + $pending[] = array( + 'id' => (int) $announcement->id, + 'title' => $announcement->title, + 'content' => $announcement->content, + ); + } + + $rep->data = array('announcements' => $pending); + + return $rep; + } + + /** + * Mark an announcement as seen by the current user. + * + * @return jResponseJson + */ + public function markSeen() + { + /** @var jResponseJson $rep */ + $rep = $this->getResponse('json'); + + if (!jAuth::isConnected()) { + $rep->data = array('status' => 'ok'); + + return $rep; + } + + $announcementId = $this->intParam('id'); + if (!$announcementId) { + $rep->data = array('status' => 'error', 'message' => 'Missing announcement id'); + + return $rep; + } + + $usr_login = jAuth::getUserSession()->login; + $viewDao = jDao::get('lizmap~announcementView', 'lizlog'); + + $view = $viewDao->getByAnnouncementAndUser($announcementId, $usr_login); + if ($view) { + $view->view_count = $view->view_count + 1; + $viewDao->update($view); + } else { + $record = jDao::createRecord('lizmap~announcementView', 'lizlog'); + $record->announcement_id = $announcementId; + $record->usr_login = $usr_login; + $record->view_count = 1; + $viewDao->insert($record); + } + + $rep->data = array('status' => 'ok'); + + return $rep; + } +} diff --git a/lizmap/modules/lizmap/daos/announcement.dao.xml b/lizmap/modules/lizmap/daos/announcement.dao.xml new file mode 100644 index 0000000000..50c6cd365d --- /dev/null +++ b/lizmap/modules/lizmap/daos/announcement.dao.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lizmap/modules/lizmap/daos/announcementView.dao.xml b/lizmap/modules/lizmap/daos/announcementView.dao.xml new file mode 100644 index 0000000000..17bd99e468 --- /dev/null +++ b/lizmap/modules/lizmap/daos/announcementView.dao.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lizmap/modules/lizmap/install/install.php b/lizmap/modules/lizmap/install/install.php index c4d3fff88d..166ae1313d 100644 --- a/lizmap/modules/lizmap/install/install.php +++ b/lizmap/modules/lizmap/install/install.php @@ -43,6 +43,9 @@ public function install() $this->useDbProfile('lizlog'); $this->execSQLScript('sql/lizlog'); + // Add announcement tables + $this->execSQLScript('sql/lizannouncement'); + // Add geobookmark table $this->useDbProfile('jauth'); $this->execSQLScript('sql/lizgeobookmark'); diff --git a/lizmap/modules/lizmap/install/sql/lizannouncement.pgsql.sql b/lizmap/modules/lizmap/install/sql/lizannouncement.pgsql.sql new file mode 100644 index 0000000000..494fe671dc --- /dev/null +++ b/lizmap/modules/lizmap/install/sql/lizannouncement.pgsql.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS lizmap_announcement ( + id SERIAL PRIMARY KEY, + title character varying(255) NOT NULL, + content TEXT NOT NULL, + target_repository character varying(100), + target_project character varying(100), + target_groups TEXT, + max_display_count INTEGER DEFAULT 1, + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS lizmap_announcement_views ( + id SERIAL PRIMARY KEY, + announcement_id INTEGER NOT NULL REFERENCES lizmap_announcement(id) ON DELETE CASCADE, + usr_login character varying(100) NOT NULL, + view_count INTEGER DEFAULT 0, + last_viewed_at timestamp DEFAULT CURRENT_TIMESTAMP, + UNIQUE(announcement_id, usr_login) +); diff --git a/lizmap/modules/lizmap/install/sql/lizannouncement.sqlite.sql b/lizmap/modules/lizmap/install/sql/lizannouncement.sqlite.sql new file mode 100644 index 0000000000..9af6a9e1b6 --- /dev/null +++ b/lizmap/modules/lizmap/install/sql/lizannouncement.sqlite.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "lizmap_announcement" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, + "title" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + "target_repository" VARCHAR, + "target_project" VARCHAR, + "target_groups" TEXT, + "max_display_count" INTEGER DEFAULT 1, + "created_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + "is_active" INTEGER DEFAULT 1); + +CREATE TABLE IF NOT EXISTS "lizmap_announcement_views" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, + "announcement_id" INTEGER NOT NULL REFERENCES "lizmap_announcement"("id") ON DELETE CASCADE, + "usr_login" VARCHAR NOT NULL, + "view_count" INTEGER DEFAULT 0, + "last_viewed_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE("announcement_id", "usr_login")); diff --git a/lizmap/modules/lizmap/install/upgrade_announcement.php b/lizmap/modules/lizmap/install/upgrade_announcement.php new file mode 100644 index 0000000000..0b755c3f95 --- /dev/null +++ b/lizmap/modules/lizmap/install/upgrade_announcement.php @@ -0,0 +1,17 @@ +firstExec('announcement')) { + $this->useDbProfile('lizlog'); + $this->execSQLScript('sql/lizannouncement'); + } + } +} diff --git a/lizmap/modules/lizmap/urls.xml b/lizmap/modules/lizmap/urls.xml index 38c18b1d4d..84115d3f13 100644 --- a/lizmap/modules/lizmap/urls.xml +++ b/lizmap/modules/lizmap/urls.xml @@ -22,4 +22,7 @@ + + + diff --git a/lizmap/modules/view/controllers/lizAjax.classic.php b/lizmap/modules/view/controllers/lizAjax.classic.php index 2e73da1979..c53016f636 100644 --- a/lizmap/modules/view/controllers/lizAjax.classic.php +++ b/lizmap/modules/view/controllers/lizAjax.classic.php @@ -173,6 +173,8 @@ public function map() 'nominatim' => jUrl::getFull('lizmap~osm:nominatim'), 'edition' => jUrl::getFull('lizmap~edition:getFeature'), 'permalink' => jUrl::getFull('view~map:index'), + 'announcement' => jUrl::getFull('lizmap~announcement:pending'), + 'announcementMarkSeen' => jUrl::getFull('lizmap~announcement:markSeen'), ); // Get optional WMS public url list diff --git a/lizmap/modules/view/controllers/lizMap.classic.php b/lizmap/modules/view/controllers/lizMap.classic.php index 35472a8424..014ea46759 100644 --- a/lizmap/modules/view/controllers/lizMap.classic.php +++ b/lizmap/modules/view/controllers/lizMap.classic.php @@ -218,6 +218,8 @@ public function index() 'basepath' => $bp, 'geobookmark' => jUrl::get('lizmap~geobookmark:index'), 'service' => jUrl::get('lizmap~service:index').'?repository='.$repository.'&project='.$project, + 'announcement' => jUrl::get('lizmap~announcement:pending'), + 'announcementMarkSeen' => jUrl::get('lizmap~announcement:markSeen'), 'resourceUrlReplacement' => array(), );