Sélection par séance, réordonnancement et exclusion vie du labo

This commit is contained in:
2026-06-03 00:36:55 +02:00
parent f3970480c4
commit b48f14f0a0
7 changed files with 424 additions and 165 deletions
+18 -5
View File
@@ -23,13 +23,16 @@ Le workflow :
1. Sélection d'un mois (sélecteur année-mois) 1. Sélection d'un mois (sélecteur année-mois)
2. Chargement AJAX (`thalim_nl_load_month`) des contenus éligibles, regroupés par catégorie parente 2. Chargement AJAX (`thalim_nl_load_month`) des contenus éligibles, regroupés par catégorie parente
3. Cases à cocher pour inclure ou exclure chaque publication / séance 3. Cases à cocher pour inclure ou exclure chaque publication / séance. **Tout est coché par défaut** pour un mois sans newsletter existante (à l'ouverture d'une newsletter déjà enregistrée, c'est la sélection sauvegardée qui est restaurée)
4. Champs **intro**, **conclusion**, **URL d'inscription**, **URL de désinscription** 4. **Réordonnancement par glisser-déposer** : chaque item porte une poignée (`⋮`). Pour les catégories normales, on réordonne les annonces dans leur liste. Pour les séminaires, la poignée est sur le **titre du séminaire** : on réordonne les **séminaires entre eux** (les séances, elles, restent toujours triées par ordre chronologique). L'ordre choisi est repris tel quel dans le rendu HTML après sauvegarde — l'ordre de soumission des cases suit l'ordre du DOM, et l'export rend chaque section dans l'ordre stocké
5. Sauvegarde : crée un post WordPress dans la catégorie **Newsletter** (`20`) avec le HTML email complet en `post_content` 5. Champs **intro**, **conclusion**, **URL d'inscription**, **URL de désinscription**
6. Bouton **Exporter en HTML** (`thalim_nl_export_html`) → téléchargement du fichier `newsletter-THALIM-{mois}.html` 6. Sauvegarde : crée un post WordPress dans la catégorie **Newsletter** (`20`) avec le HTML email complet en `post_content`
7. Bouton **Exporter en HTML** (`thalim_nl_export_html`) → téléchargement du fichier `newsletter-THALIM-{mois}.html`
Une liste des newsletters déjà sauvegardées permet de revenir éditer un mois passé. Une liste des newsletters déjà sauvegardées permet de revenir éditer un mois passé.
> Les catégories **Vie du labo (intranet)** (`9`), **Séance de séminaire** (`12`), **Newsletter** (`20`) et **Non classé** (`31`) sont exclues de l'UI (`EXCLUDED_CATS` dans `includes/class-post-query.php`). Les séances (cat 12) restent listées, mais imbriquées sous leur séminaire — voir plus bas.
## Fenêtres d'éligibilité par catégorie ## Fenêtres d'éligibilité par catégorie
Les contenus pertinents d'un mois donné ne sont pas seulement « les posts publiés ce mois-ci » — chaque catégorie a sa propre logique de fenêtre temporelle (cf. `WINDOW_TYPES` dans `includes/class-post-query.php`) : Les contenus pertinents d'un mois donné ne sont pas seulement « les posts publiés ce mois-ci » — chaque catégorie a sa propre logique de fenêtre temporelle (cf. `WINDOW_TYPES` dans `includes/class-post-query.php`) :
@@ -37,12 +40,22 @@ Les contenus pertinents d'un mois donné ne sont pas seulement « les posts publ
| Catégorie | Fenêtre | | Catégorie | Fenêtre |
| -------------------------------------------------- | ---------------------- | | -------------------------------------------------- | ---------------------- |
| Appels (`8`), Soutenances (`14`) | `datetime_to_fin` (du `datetime` à `date_de_fin`) | | Appels (`8`), Soutenances (`14`) | `datetime_to_fin` (du `datetime` à `date_de_fin`) |
| Colloques (`10`), Séminaires (`11`) | `debut_minus35_to_fin` (de `date_de_debut - 35j` à `date_de_fin`) | | Colloques (`10`), Communications (`13`) | `debut_minus35_to_fin` (de `date_de_debut - 35j` à `date_de_fin`) |
| Ouvrages (`15`), Articles (`16`) | `datetime_plus3m` (du `datetime` à `datetime + 3 mois`) | | Ouvrages (`15`), Articles (`16`) | `datetime_plus3m` (du `datetime` à `datetime + 3 mois`) |
| **Toutes les autres** | `datetime_plus35d` (du `datetime` à `datetime + 35 jours`) | | **Toutes les autres** | `datetime_plus35d` (du `datetime` à `datetime + 35 jours`) |
Quand `datetime` (ou `date_de_debut`) est vide, le `post_date` sert de fallback. Cette logique permet par ex. à un appel à communication d'apparaître dans toutes les newsletters jusqu'à sa date de fin. Quand `datetime` (ou `date_de_debut`) est vide, le `post_date` sert de fallback. Cette logique permet par ex. à un appel à communication d'apparaître dans toutes les newsletters jusqu'à sa date de fin.
### Cas particulier : Séminaires (`11`) → sélection par séance
Le séminaire n'est **pas** sélectionnable en tant que tel. À la place, le plugin liste ses **séances** (cat 12) individuellement, chacune avec sa propre case à cocher, regroupées sous le titre (non cliquable) de leur séminaire parent.
- **Éligibilité** : une séance apparaît si sa `date_de_debut` tombe dans `[1er du mois, dernier jour du mois + 5 jours]` (marge `SEANCE_WINDOW_MARGIN_DAYS` dans `class-post-query.php`). Un séminaire sans séance dans cette fenêtre n'apparaît pas.
- **Découverte** : on parcourt les séminaires publiés (cat 11) et on lit leur meta `seances` (tableau d'IDs de séances). Le lien parent→séance vit donc sur le séminaire.
- **Sélection stockée** : `_newsletter_sections[11]` contient des **IDs de séances**, plus des IDs de séminaires.
- **Rendu HTML** (`class-html-exporter.php`) : les séances cochées sont regroupées par séminaire parent (lookup inverse via `Thalim_NL_Post_Query::get_seminar_id_for_seance()`, même requête que la redirection `#seance-{ID}` du thème). Le titre du séminaire est affiché **une seule fois**, suivi de la liste des séances sélectionnées (date · heure · lieu, lien vers `#seance-{id}`).
- **Ordre** : les **séminaires** sont réordonnables entre eux par glisser-déposer (poignée sur le titre) — leur ordre de premier appartenance dans la sélection stockée donne l'ordre de rendu. Les **séances** d'un séminaire sont toujours triées par `date_de_debut` croissante, dans l'UI comme à l'export.
## Catégories couvertes ## Catégories couvertes
La liste des catégories éligibles n'est **pas** codée en dur dans le plugin — elle est calculée dynamiquement via `Thalim_NL_Post_Query::get_eligible_categories()` (toutes les catégories WordPress, groupées par parent). Les constantes en haut de `thalim-newsletter.php` (`THALIM_NL_CAT_APPELS = 8`, etc.) ne servent qu'à associer les fenêtres temporelles spéciales aux catégories concernées : La liste des catégories éligibles n'est **pas** codée en dur dans le plugin — elle est calculée dynamiquement via `Thalim_NL_Post_Query::get_eligible_categories()` (toutes les catégories WordPress, groupées par parent). Les constantes en haut de `thalim-newsletter.php` (`THALIM_NL_CAT_APPELS = 8`, etc.) ne servent qu'à associer les fenêtres temporelles spéciales aux catégories concernées :
+60 -17
View File
@@ -138,6 +138,41 @@
margin-top: 2px; margin-top: 2px;
} }
/* ---- Drag handle + reordering ---- */
.thalim-nl-drag-handle {
flex-shrink: 0;
cursor: grab;
color: #c3c4c7;
font-size: 16px;
width: 16px;
height: 16px;
line-height: 1;
align-self: center;
}
.thalim-nl-post-row:hover .thalim-nl-drag-handle,
.thalim-nl-seminar-title:hover .thalim-nl-drag-handle {
color: #8c8f94;
}
.thalim-nl-drag-handle:active {
cursor: grabbing;
}
.thalim-nl-sortable-placeholder {
border: 1px dashed #2271b1;
background: #f0f6fc;
border-radius: 3px;
height: 26px;
margin: 2px 0;
}
.ui-sortable-helper {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 3px;
}
.thalim-nl-post-title { .thalim-nl-post-title {
flex: 1; flex: 1;
font-size: 13px; font-size: 13px;
@@ -166,29 +201,37 @@
white-space: nowrap; white-space: nowrap;
} }
/* ---- Séances list under seminars ---- */ /* ---- Seminars: séances grouped under their seminar ---- */
.thalim-nl-seances-list { .thalim-nl-seminar-group {
margin: 2px 0 6px 28px; margin-top: 18px;
padding: 0;
list-style: none;
border-left: 2px solid #dcdcde;
padding-left: 12px;
} }
.thalim-nl-seance-item { .thalim-nl-seminar-group:first-child {
padding: 3px 0; margin-top: 4px;
font-size: 12px;
color: #50575e;
} }
.thalim-nl-seance-title { .thalim-nl-seminar-title {
font-weight: 500; display: flex;
align-items: baseline;
gap: 6px;
font-weight: 600;
font-size: 13px;
color: #1d2327;
padding: 5px 10px;
background: #f0f3f5;
border-radius: 3px;
} }
.thalim-nl-seance-meta { .thalim-nl-seminar-title .thalim-nl-post-view {
display: block; margin-left: auto;
color: #888; }
font-size: 11px;
/* Séances hang under their seminar title, connected by a left rail */
.thalim-nl-seance-row {
margin-left: 9px;
padding-left: 17px;
border-bottom: none;
border-left: 2px solid #e2e6ea;
} }
/* ---- Empty state ---- */ /* ---- Empty state ---- */
+44
View File
@@ -2,6 +2,49 @@
(function ($) { (function ($) {
'use strict'; 'use strict';
// -------------------------------------------------------------------------
// Drag-and-drop reordering of items within each category list.
// The submit order of checkboxes follows DOM order, and the exporter
// renders each section in stored order — so reordering rows here is enough
// for the new order to appear in the generated HTML after saving.
// -------------------------------------------------------------------------
function makeSortable($container, itemSelector) {
if (!$.fn.sortable) return;
$container.sortable({
items: itemSelector,
handle: '.thalim-nl-drag-handle',
axis: 'y',
containment: 'parent',
tolerance: 'pointer',
cursor: 'grabbing',
opacity: 0.9,
placeholder: 'thalim-nl-sortable-placeholder',
forcePlaceholderSize: true,
});
}
function initSortable() {
var $root = $('#thalim-nl-sections-wrap');
$root.find('.thalim-nl-section-body').each(function () {
var $body = $(this);
if ($body.find('.thalim-nl-seminar-group').length) {
// Seminars: reorder whole seminars (séances stay chronological).
makeSortable($body, '> .thalim-nl-seminar-group');
} else {
// Normal categories: reorder post rows.
makeSortable($body, '> .thalim-nl-post-row');
}
});
}
// A click on the drag handle must not toggle its enclosing checkbox label.
$(document).on('click', '.thalim-nl-drag-handle', function (e) {
e.preventDefault();
});
$(initSortable);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Month change → AJAX reload sections + update TinyMCE intro // Month change → AJAX reload sections + update TinyMCE intro
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -24,6 +67,7 @@
if (!response.success) return; if (!response.success) return;
$wrap.html(response.data.html); $wrap.html(response.data.html);
initSortable();
// Update TinyMCE intro — do NOT re-render wp_editor // Update TinyMCE intro — do NOT re-render wp_editor
var introEditor = typeof tinymce !== 'undefined' && tinymce.get('thalim_nl_intro'); var introEditor = typeof tinymce !== 'undefined' && tinymce.get('thalim_nl_intro');
+130 -47
View File
@@ -118,7 +118,7 @@ class Thalim_NL_Admin_Page {
</div> </div>
<div id="thalim-nl-sections-wrap"> <div id="thalim-nl-sections-wrap">
<?php echo $this->render_sections_html($month_data, $existing_sections); ?> <?php echo $this->render_sections_html($month_data, $existing ? $existing_sections : null); ?>
</div> </div>
<div class="thalim-nl-conclusion"> <div class="thalim-nl-conclusion">
@@ -213,9 +213,18 @@ class Thalim_NL_Admin_Page {
// Sections HTML (shared between initial render and AJAX) // Sections HTML (shared between initial render and AJAX)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
public function render_sections_html(array $month_data, array $checked_ids = []): string { /**
* @param array $month_data Posts grouped by category for the month.
* @param array|null $checked_ids Saved selection (cat_id => [post_id,...]) for an
* existing newsletter. Pass null for a fresh month:
* every item is then checked by default.
*/
public function render_sections_html(array $month_data, ?array $checked_ids = null): string {
$groups = Thalim_NL_Post_Query::get_eligible_categories(); $groups = Thalim_NL_Post_Query::get_eligible_categories();
// No saved newsletter yet → everything checked by default.
$check_all = ($checked_ids === null);
ob_start(); ob_start();
$has_content = false; $has_content = false;
@@ -249,8 +258,17 @@ class Thalim_NL_Admin_Page {
continue; continue;
} }
$count = count($posts); $count = count($posts);
$checked_in_section = (array) ($checked_ids[$cat_id] ?? $checked_ids[(string) $cat_id] ?? []); $checked_in_section = $check_all
$has_checked = !empty($checked_in_section); ? []
: (array) ($checked_ids[$cat_id] ?? $checked_ids[(string) $cat_id] ?? []);
$has_checked = $check_all || !empty($checked_in_section);
// Restore the saved drag-and-drop order ($checked_in_section is
// stored in submit order). For seminars this also reorders the
// seminar groups, since grouping below follows first appearance.
if (!$check_all) {
$posts = $this->reorder_posts_by_saved($posts, $checked_in_section);
}
?> ?>
<details class="thalim-nl-section" <?php if ($has_checked) echo 'open'; ?>> <details class="thalim-nl-section" <?php if ($has_checked) echo 'open'; ?>>
<summary class="thalim-nl-section-summary"> <summary class="thalim-nl-section-summary">
@@ -258,49 +276,67 @@ class Thalim_NL_Admin_Page {
<span class="thalim-nl-count">(<?php echo $count; ?>)</span> <span class="thalim-nl-count">(<?php echo $count; ?>)</span>
</summary> </summary>
<div class="thalim-nl-section-body"> <div class="thalim-nl-section-body">
<?php foreach ($posts as $post): ?> <?php if ((int) $cat_id === THALIM_NL_CAT_SEMINAIRES): ?>
<label class="thalim-nl-post-row"> <?php
<input // Seminars: posts are séance items carrying their parent
type="checkbox" // seminar. Group them under a (non-selectable) seminar
name="nl_sections[<?php echo (int) $cat_id; ?>][]" // header; each séance is an individual checkbox.
value="<?php echo (int) $post['id']; ?>" $by_seminar = [];
<?php checked(in_array((int) $post['id'], $checked_in_section)); ?> foreach ($posts as $seance) {
> $sem_id = (int) $seance['seminar_id'];
<span class="thalim-nl-post-title"><?php echo esc_html($post['title']); ?></span> if (!isset($by_seminar[$sem_id])) {
<a href="<?php echo esc_url($post['permalink']); ?>" target="_blank" rel="noopener" $by_seminar[$sem_id] = [
class="thalim-nl-post-view" title="Voir sur le site"> 'title' => $seance['seminar_title'],
<span class="dashicons dashicons-visibility"></span> 'permalink' => $seance['seminar_permalink'],
</a> 'seances' => [],
<span class="thalim-nl-post-date"><?php echo esc_html($this->format_post_date_hint($post)); ?></span> ];
</label> }
<?php if (!empty($post['seances'])): ?> $by_seminar[$sem_id]['seances'][] = $seance;
<ul class="thalim-nl-seances-list"> }
<?php foreach ($post['seances'] as $seance): ?> ?>
<li class="thalim-nl-seance-item"> <?php foreach ($by_seminar as $sem): ?>
<span class="thalim-nl-seance-title"><?php echo esc_html($seance['title']); ?></span> <div class="thalim-nl-seminar-group">
<span class="thalim-nl-seance-meta"> <div class="thalim-nl-seminar-title">
<?php <span class="thalim-nl-drag-handle dashicons dashicons-menu" title="Glisser pour réordonner les séminaires" aria-hidden="true"></span>
$parts = []; <span class="thalim-nl-seminar-name"><?php echo esc_html($sem['title']); ?></span>
if ($seance['date_debut']) { <a href="<?php echo esc_url($sem['permalink']); ?>" target="_blank" rel="noopener"
$ts = strtotime($seance['date_debut']); class="thalim-nl-post-view" title="Voir sur le site">
$parts[] = $ts ? date_i18n('j M Y', $ts) : ''; <span class="dashicons dashicons-visibility"></span>
} </a>
if ($seance['heure_de_debut']) { </div>
$time = $seance['heure_de_debut']; <?php foreach ($sem['seances'] as $seance): ?>
if ($seance['heure_de_fin']) $time .= '–' . $seance['heure_de_fin']; <label class="thalim-nl-post-row thalim-nl-seance-row">
$parts[] = $time; <input
} type="checkbox"
if ($seance['lieu']) { name="nl_sections[<?php echo (int) $cat_id; ?>][]"
$parts[] = $seance['lieu']; value="<?php echo (int) $seance['id']; ?>"
} <?php checked($check_all || in_array((int) $seance['id'], $checked_in_section)); ?>
echo esc_html(implode(' · ', array_filter($parts))); >
?> <span class="thalim-nl-post-title"><?php echo esc_html($seance['title']); ?></span>
</span> <span class="thalim-nl-post-date"><?php echo esc_html($this->format_seance_hint($seance)); ?></span>
</li> </label>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </div>
<?php endif; ?> <?php endforeach; ?>
<?php endforeach; ?> <?php else: ?>
<?php foreach ($posts as $post): ?>
<label class="thalim-nl-post-row">
<span class="thalim-nl-drag-handle dashicons dashicons-menu" title="Glisser pour réordonner" aria-hidden="true"></span>
<input
type="checkbox"
name="nl_sections[<?php echo (int) $cat_id; ?>][]"
value="<?php echo (int) $post['id']; ?>"
<?php checked($check_all || in_array((int) $post['id'], $checked_in_section)); ?>
>
<span class="thalim-nl-post-title"><?php echo esc_html($post['title']); ?></span>
<a href="<?php echo esc_url($post['permalink']); ?>" target="_blank" rel="noopener"
class="thalim-nl-post-view" title="Voir sur le site">
<span class="dashicons dashicons-visibility"></span>
</a>
<span class="thalim-nl-post-date"><?php echo esc_html($this->format_post_date_hint($post)); ?></span>
</label>
<?php endforeach; ?>
<?php endif; ?>
</div> </div>
</details> </details>
<?php endforeach; ?> <?php endforeach; ?>
@@ -331,6 +367,53 @@ class Thalim_NL_Admin_Page {
return ''; return '';
} }
/**
* Reorder $posts so items whose 'id' appears in $order come first, in
* $order's sequence; the rest keep their query order, appended after.
* Restores the editor's saved drag-and-drop order in the admin UI.
*/
private function reorder_posts_by_saved(array $posts, array $order): array {
if (empty($order)) {
return $posts;
}
$rank = array_flip(array_map('intval', array_values($order)));
$keyed = [];
foreach ($posts as $idx => $post) {
$keyed[] = [
'post' => $post,
'rank' => $rank[(int) $post['id']] ?? PHP_INT_MAX,
'idx' => $idx,
];
}
// Sort by saved rank, then original index (stable for un-ranked items).
usort($keyed, fn($a, $b) => [$a['rank'], $a['idx']] <=> [$b['rank'], $b['idx']]);
return array_map(fn($x) => $x['post'], $keyed);
}
/**
* Date · heure · lieu hint shown next to each séance checkbox.
*/
private function format_seance_hint(array $seance): string {
$parts = [];
if (!empty($seance['date_debut']) && !str_starts_with($seance['date_debut'], '0000-00-00')) {
$ts = strtotime($seance['date_debut']);
if ($ts) {
$parts[] = date_i18n('j M Y', $ts);
}
}
if (!empty($seance['heure_de_debut'])) {
$time = $seance['heure_de_debut'];
if (!empty($seance['heure_de_fin'])) {
$time .= '–' . $seance['heure_de_fin'];
}
$parts[] = $time;
}
if (!empty($seance['lieu'])) {
$parts[] = $seance['lieu'];
}
return implode(' · ', $parts);
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Save // Save
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -490,7 +573,7 @@ class Thalim_NL_Admin_Page {
$unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: ''; $unsubscribe_url = get_post_meta($existing->ID, '_newsletter_unsubscribe_url', true) ?: '';
} }
$html = $this->render_sections_html($month_data, $existing_sections); $html = $this->render_sections_html($month_data, $existing ? $existing_sections : null);
wp_send_json_success([ wp_send_json_success([
'html' => $html, 'html' => $html,
+56 -41
View File
@@ -44,10 +44,6 @@ class Thalim_NL_HTML_Exporter {
/** /**
* Generate a complete email-ready HTML document from a saved newsletter post. * Generate a complete email-ready HTML document from a saved newsletter post.
*/ */
/** Month window timestamps — set by generate(), used by format_seminar_line() */
private int $month_start = 0;
private int $month_end = 0;
public function generate(WP_Post $post): string { public function generate(WP_Post $post): string {
$sections_json = get_post_meta($post->ID, '_newsletter_sections', true); $sections_json = get_post_meta($post->ID, '_newsletter_sections', true);
$sections = json_decode($sections_json, true) ?: []; $sections = json_decode($sections_json, true) ?: [];
@@ -57,13 +53,6 @@ class Thalim_NL_HTML_Exporter {
$subscribe_url = get_post_meta($post->ID, '_newsletter_subscribe_url', true) ?: ''; $subscribe_url = get_post_meta($post->ID, '_newsletter_subscribe_url', true) ?: '';
$unsubscribe_url = get_post_meta($post->ID, '_newsletter_unsubscribe_url', true) ?: ''; $unsubscribe_url = get_post_meta($post->ID, '_newsletter_unsubscribe_url', true) ?: '';
// Compute month window from stored newsletter month
$year_month = get_post_meta($post->ID, '_newsletter_month', true) ?: '';
if ($year_month) {
$this->month_start = strtotime($year_month . '-01 00:00:00') ?: 0;
$this->month_end = strtotime('last day of ' . $year_month . ' 23:59:59') ?: 0;
}
// Build ordered section blocks following the category hierarchy // Build ordered section blocks following the category hierarchy
$groups = Thalim_NL_Post_Query::get_eligible_categories(); $groups = Thalim_NL_Post_Query::get_eligible_categories();
$section_blocks = []; $section_blocks = [];
@@ -83,10 +72,16 @@ class Thalim_NL_HTML_Exporter {
continue; continue;
} }
$items = []; $items = [];
foreach ($post_ids as $pid) { if ((int) $cat_id === THALIM_NL_CAT_SEMINAIRES) {
$line = $this->format_post_line((int) $pid, $cat_id); // Selected items are séance IDs: group them by parent seminar,
if ($line) { // rendering each seminar's title once above its séances.
$items[] = $line; $items = $this->format_seminar_blocks(array_map('intval', $post_ids));
} else {
foreach ($post_ids as $pid) {
$line = $this->format_post_line((int) $pid, $cat_id);
if ($line) {
$items[] = $line;
}
} }
} }
if (!empty($items)) { if (!empty($items)) {
@@ -305,11 +300,6 @@ body { margin: 0 !important; padding: 0 !important; }
* Optional image on the left, title in serif, meta below in sans-serif. * Optional image on the left, title in serif, meta below in sans-serif.
*/ */
public function format_post_line(int $post_id, int $cat_id): string { public function format_post_line(int $post_id, int $cat_id): string {
// Seminars: special rendering with séances
if ($cat_id === THALIM_NL_CAT_SEMINAIRES) {
return $this->format_seminar_line($post_id);
}
$post = get_post($post_id); $post = get_post($post_id);
if (!$post) { if (!$post) {
return ''; return '';
@@ -394,11 +384,45 @@ body { margin: 0 !important; padding: 0 !important; }
} }
/** /**
* Format a seminar post: title (no dates) + list of séances with date, time, location. * Render the seminar section from a flat list of selected séance IDs.
* Each séance links to the seminar page. * Séances are grouped by their parent seminar (first-appearance order),
* and each seminar is rendered as one block with its title shown once.
*
* @return string[] One HTML block per seminar.
*/ */
private function format_seminar_line(int $post_id): string { private function format_seminar_blocks(array $seance_ids): array {
$post = get_post($post_id); // Group séance IDs by parent seminar, preserving order of first appearance.
$by_seminar = [];
foreach ($seance_ids as $sid) {
$sid = (int) $sid;
$s_post = get_post($sid);
if (!$s_post || $s_post->post_status !== 'publish') {
continue;
}
$sem_id = Thalim_NL_Post_Query::get_seminar_id_for_seance($sid);
if (!$sem_id) {
continue;
}
$by_seminar[$sem_id][] = $sid;
}
$blocks = [];
foreach ($by_seminar as $sem_id => $sids) {
$block = $this->format_seminar_block((int) $sem_id, $sids);
if ($block) {
$blocks[] = $block;
}
}
return $blocks;
}
/**
* Format one seminar block: title (no dates) shown once + the given
* séances with date, time, location. Each séance links to its anchor
* on the seminar page.
*/
private function format_seminar_block(int $seminar_id, array $seance_ids): string {
$post = get_post($seminar_id);
if (!$post) { if (!$post) {
return ''; return '';
} }
@@ -406,12 +430,12 @@ body { margin: 0 !important; padding: 0 !important; }
$font_serif = "Gelasio, Georgia, 'Times New Roman', serif"; $font_serif = "Gelasio, Georgia, 'Times New Roman', serif";
$font_sans = "Arial, Helvetica, sans-serif"; $font_sans = "Arial, Helvetica, sans-serif";
$url = get_permalink($post_id); $url = get_permalink($seminar_id);
$title = get_the_title($post_id); $title = get_the_title($seminar_id);
// Image // Image (first image from the seminar's documents_joints)
$image_url = ''; $image_url = '';
$doc_ids = get_post_meta($post_id, 'documents_joints', false); $doc_ids = get_post_meta($seminar_id, 'documents_joints', false);
foreach ($doc_ids as $doc_id) { foreach ($doc_ids as $doc_id) {
$mime = get_post_mime_type($doc_id); $mime = get_post_mime_type($doc_id);
if ($mime && str_starts_with($mime, 'image/')) { if ($mime && str_starts_with($mime, 'image/')) {
@@ -423,28 +447,19 @@ body { margin: 0 !important; padding: 0 !important; }
} }
} }
// Fetch séances within the newsletter month window // Build the selected séances. Within a seminar, séances always display
$seance_ids = get_post_meta($post_id, 'seances', false); // in chronological order (seminars themselves are reorderable, séances
// are not).
$seances = []; $seances = [];
foreach ($seance_ids as $sid) { foreach ($seance_ids as $sid) {
$sid = (int) $sid; $sid = (int) $sid;
$s_post = get_post($sid); $s_post = get_post($sid);
if (!$s_post || $s_post->post_status !== 'publish') continue; if (!$s_post || $s_post->post_status !== 'publish') continue;
$raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: '';
$ts = $raw_debut ? strtotime($raw_debut) : false;
// Filter: only séances within the month window
if ($this->month_start && $this->month_end) {
if (!$ts || $ts < $this->month_start || $ts > $this->month_end) {
continue;
}
}
$seances[] = [ $seances[] = [
'id' => $sid, 'id' => $sid,
'title' => get_the_title($sid), 'title' => get_the_title($sid),
'date_debut' => $raw_debut, 'date_debut' => get_post_meta($sid, 'date_de_debut', true) ?: '',
'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '', 'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '',
'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5), 'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5),
'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5), 'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5),
+115 -54
View File
@@ -21,7 +21,6 @@ class Thalim_NL_Post_Query {
private const SPECIAL_WINDOW_TYPES = [ private const SPECIAL_WINDOW_TYPES = [
THALIM_NL_CAT_APPELS => 'datetime_to_fin', THALIM_NL_CAT_APPELS => 'datetime_to_fin',
THALIM_NL_CAT_COLLOQUES => 'debut_minus35_to_fin', THALIM_NL_CAT_COLLOQUES => 'debut_minus35_to_fin',
THALIM_NL_CAT_SEMINAIRES => 'debut_minus35_to_fin',
THALIM_NL_CAT_COMMS => 'debut_minus35_to_fin', THALIM_NL_CAT_COMMS => 'debut_minus35_to_fin',
THALIM_NL_CAT_SOUTENANCES => 'datetime_to_fin', THALIM_NL_CAT_SOUTENANCES => 'datetime_to_fin',
THALIM_NL_CAT_OUVRAGES => 'datetime_plus3m', THALIM_NL_CAT_OUVRAGES => 'datetime_plus3m',
@@ -30,8 +29,17 @@ class Thalim_NL_Post_Query {
private const DEFAULT_WINDOW_TYPE = 'datetime_plus35d'; private const DEFAULT_WINDOW_TYPE = 'datetime_plus35d';
/**
* Séminaires (cat 11) are not selected as whole posts: instead, each of
* their séances (cat 12) is individually selectable. A séance is eligible
* when its date_de_debut falls within the newsletter month, extended by
* this margin (in days) past the end of the month.
*/
private const SEANCE_WINDOW_MARGIN_DAYS = 5;
/** Categories to exclude from the newsletter UI */ /** Categories to exclude from the newsletter UI */
private const EXCLUDED_CATS = [ private const EXCLUDED_CATS = [
9, // Vie du labo (intranet)
12, // Séance de séminaire 12, // Séance de séminaire
20, // Newsletter 20, // Newsletter
31, // Non classé 31, // Non classé
@@ -119,6 +127,15 @@ class Thalim_NL_Post_Query {
$result = []; $result = [];
foreach (self::get_all_eligible_cat_ids() as $cat_id) { foreach (self::get_all_eligible_cat_ids() as $cat_id) {
// Seminars: list individually-selectable séances grouped by seminar.
if ($cat_id === THALIM_NL_CAT_SEMINAIRES) {
$seances = $this->query_seminar_seances($month_start, $month_end);
if (!empty($seances)) {
$result[$cat_id] = $seances;
}
continue;
}
$window_type = self::get_window_type($cat_id); $window_type = self::get_window_type($cat_id);
$posts = $this->query_category($cat_id, $window_type, $month_start, $month_end); $posts = $this->query_category($cat_id, $window_type, $month_start, $month_end);
if (!empty($posts)) { if (!empty($posts)) {
@@ -128,6 +145,100 @@ class Thalim_NL_Post_Query {
return $result; return $result;
} }
/**
* Build the flat list of selectable séances for the seminar category.
*
* We walk every published seminar (cat 11), read its `seances` meta
* (array of séance post IDs), and keep the séances whose date_de_debut
* falls within [month_start, month_end + SEANCE_WINDOW_MARGIN_DAYS].
* Each returned item carries its parent seminar (id/title/permalink) so
* the UI and the exporter can group séances under their seminar.
*
* @return array Flat list of séance items (see shape below).
*/
private function query_seminar_seances(int $month_start, int $month_end): array {
global $wpdb;
$window_end = $month_end + (self::SEANCE_WINDOW_MARGIN_DAYS * DAY_IN_SECONDS);
$seminar_ids = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT p.ID
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
AND tt.taxonomy = 'category' AND tt.term_id = %d
WHERE p.post_type = 'post' AND p.post_status = 'publish'",
THALIM_NL_CAT_SEMINAIRES
));
$items = [];
foreach ($seminar_ids as $seminar_id) {
$seminar_id = (int) $seminar_id;
$seminar_title = get_the_title($seminar_id);
$seminar_permalink = get_permalink($seminar_id);
foreach (get_post_meta($seminar_id, 'seances', false) as $sid) {
$sid = (int) $sid;
$s_post = get_post($sid);
if (!$s_post || $s_post->post_status !== 'publish') {
continue;
}
$raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: '';
$ts = $raw_debut ? strtotime($raw_debut) : false;
if (!$ts || $ts < $month_start || $ts > $window_end) {
continue;
}
$items[] = [
'id' => $sid,
'title' => get_the_title($sid),
// Links straight to the séance anchor on the seminar page.
'permalink' => $seminar_permalink . '#seance-' . $sid,
'datetime' => '',
'date_debut' => $raw_debut,
'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '',
'post_date' => $s_post->post_date,
'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5),
'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5),
'lieu' => get_post_meta($sid, 'lieu', true) ?: '',
'membres' => $this->get_post_membres($sid),
'autrepersonnes' => get_post_meta($sid, 'autrepersonnes', true) ?: '',
'seminar_id' => $seminar_id,
'seminar_title' => $seminar_title,
'seminar_permalink' => $seminar_permalink,
];
}
}
// Sort by séance date: seminars naturally order by their earliest séance
// when grouped by first appearance (see render / export grouping).
usort($items, fn($a, $b) => strcmp($a['date_debut'], $b['date_debut']));
return $items;
}
/**
* Resolve the parent seminar (cat 11) that lists a given séance in its
* `seances` meta. Mirrors the reverse lookup used by the theme's
* #seance-{ID} redirect. Returns 0 when none is found.
*/
public static function get_seminar_id_for_seance(int $seance_id): int {
global $wpdb;
$parent_id = $wpdb->get_var($wpdb->prepare(
"SELECT pm.post_id
FROM {$wpdb->postmeta} pm
JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = 'seances' AND pm.meta_value = %s
AND p.post_status = 'publish'
LIMIT 1",
(string) $seance_id
));
return (int) $parent_id;
}
/** /**
* Query a single category with the appropriate window expression. * Query a single category with the appropriate window expression.
*/ */
@@ -190,7 +301,7 @@ class Thalim_NL_Post_Query {
$posts = []; $posts = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$posts[] = $this->build_post_data((int) $row->ID, $row->post_title, $row->post_date, $month_start, $month_end); $posts[] = $this->build_post_data((int) $row->ID, $row->post_title, $row->post_date);
} }
return $posts; return $posts;
} }
@@ -289,8 +400,8 @@ class Thalim_NL_Post_Query {
// Post data builder // Post data builder
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private function build_post_data(int $post_id, string $post_title, string $post_date, int $month_start = 0, int $month_end = 0): array { private function build_post_data(int $post_id, string $post_title, string $post_date): array {
$data = [ return [
'id' => $post_id, 'id' => $post_id,
'title' => $post_title, 'title' => $post_title,
'permalink' => get_permalink($post_id), 'permalink' => get_permalink($post_id),
@@ -300,57 +411,7 @@ class Thalim_NL_Post_Query {
'post_date' => $post_date, 'post_date' => $post_date,
'membres' => $this->get_post_membres($post_id), 'membres' => $this->get_post_membres($post_id),
'autrepersonnes' => get_post_meta($post_id, 'autrepersonnes', true) ?: '', 'autrepersonnes' => get_post_meta($post_id, 'autrepersonnes', true) ?: '',
'seances' => [],
]; ];
// For seminars, fetch séances within the month window
if ($month_start && $month_end && has_category(THALIM_NL_CAT_SEMINAIRES, $post_id)) {
$data['seances'] = $this->get_seances_in_window($post_id, $month_start, $month_end);
}
return $data;
}
/**
* Fetch séances linked to a seminar that fall within the given time window.
*/
private function get_seances_in_window(int $seminar_id, int $month_start, int $month_end): array {
$seance_ids = get_post_meta($seminar_id, 'seances', false);
if (empty($seance_ids)) {
return [];
}
$seances = [];
foreach ($seance_ids as $sid) {
$sid = (int) $sid;
$post = get_post($sid);
if (!$post || $post->post_status !== 'publish') {
continue;
}
$raw_debut = get_post_meta($sid, 'date_de_debut', true) ?: '';
$ts = $raw_debut ? strtotime($raw_debut) : false;
if (!$ts || $ts < $month_start || $ts > $month_end) {
continue;
}
$seances[] = [
'id' => $sid,
'title' => get_the_title($sid),
'date_debut' => $raw_debut,
'date_fin' => get_post_meta($sid, 'date_de_fin', true) ?: '',
'heure_de_debut' => substr(get_post_meta($sid, 'heure_de_debut', true) ?: '', 0, 5),
'heure_de_fin' => substr(get_post_meta($sid, 'heure_de_fin', true) ?: '', 0, 5),
'lieu' => get_post_meta($sid, 'lieu', true) ?: '',
];
}
// Sort by date_de_debut ascending
usort($seances, function ($a, $b) {
return strcmp($a['date_debut'], $b['date_debut']);
});
return $seances;
} }
/** /**
+1 -1
View File
@@ -88,7 +88,7 @@ class Thalim_Newsletter {
wp_enqueue_script( wp_enqueue_script(
'thalim-newsletter-admin', 'thalim-newsletter-admin',
THALIM_NL_PLUGIN_URL . 'assets/admin.js', THALIM_NL_PLUGIN_URL . 'assets/admin.js',
['jquery'], ['jquery', 'jquery-ui-sortable'],
THALIM_NL_VERSION, THALIM_NL_VERSION,
true true
); );