Files
thalim-theme/functions.php
2026-05-12 23:33:46 +02:00

1376 lines
52 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/vendor/autoload.php';
Timber\Timber::init();
Timber::$dirname = 'templates';
add_filter('timber/context', 'add_to_context');
// ── Language detection (replaces Polylang) ───────────────────────
define( 'THALIM_ORIGINAL_URI', $_SERVER['REQUEST_URI'] ?? '/' );
function thalim_current_language(): string {
if ( isset( $GLOBALS['thalim_lang_override'] ) ) return $GLOBALS['thalim_lang_override'];
$uri = THALIM_ORIGINAL_URI;
return ( strpos( $uri, '/en/' ) === 0 || rtrim( $uri, '/' ) === '/en' ) ? 'en' : 'fr';
}
// Strip /en/ prefix before WordPress resolves the URL
add_filter( 'do_parse_request', function ( $do_parse ) {
$uri = THALIM_ORIGINAL_URI;
if ( strpos( $uri, '/en/' ) === 0 ) {
$_SERVER['REQUEST_URI'] = substr( $uri, 3 );
} elseif ( rtrim( $uri, '/' ) === '/en' ) {
$_SERVER['REQUEST_URI'] = '/';
}
return $do_parse;
}, 1 );
// Prevent WP canonical redirect from overriding /en/ URLs
add_filter( 'redirect_canonical', function ( $redirect ) {
if ( strpos( THALIM_ORIGINAL_URI, '/en/' ) === 0 ) return false;
return $redirect;
}, 10, 2 );
// Split "FR // EN" bilingual fields — returns the right part for the given lang
function thalim_bilingual( string $value, string $lang = null ): string {
if ( $lang === null ) $lang = thalim_current_language();
if ( strpos( $value, ' // ' ) === false ) return $value;
$parts = explode( ' // ', $value, 2 );
return ( $lang === 'en' && trim( $parts[1] ?? '' ) !== '' ) ? trim( $parts[1] ) : trim( $parts[0] );
}
// Prepend /en to any internal URL when current language is EN.
// Idempotent: safe to call multiple times on the same URL.
/**
* Returns the avatar URL for a user:
* 1. Simple Local Avatar (media library upload) if set
* 2. Gravatar if the user has one (result cached 1 week per user)
* 3. Empty string → templates fall back to initials/placeholder
*/
function thalim_get_user_avatar_url( int $user_id ): string {
// 1. Simple Local Avatar plugin
$meta = get_user_meta( $user_id, 'simple_local_avatar', true );
if ( is_array( $meta ) && ! empty( $meta['full'] ) ) {
// Use media_id for a dynamic URL that works across environments
if ( ! empty( $meta['media_id'] ) ) {
$url = wp_get_attachment_url( (int) $meta['media_id'] );
if ( $url ) return $url;
}
// Fallback: rewrite stored URL to current site domain
$pos = strpos( $meta['full'], '/wp-content/' );
if ( $pos !== false ) {
return rtrim( site_url(), '/' ) . substr( $meta['full'], $pos );
}
return $meta['full'];
}
// 2. Gravatar — check existence with a HEAD request, cached per user
$user = get_userdata( $user_id );
if ( ! $user ) return '';
$cache_key = 'thalim_gravatar_' . $user_id;
$cached = get_transient( $cache_key );
if ( $cached !== false ) return $cached;
$hash = md5( strtolower( trim( $user->user_email ) ) );
$response = wp_remote_head(
'https://www.gravatar.com/avatar/' . $hash . '?d=404',
[ 'timeout' => 3 ]
);
if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
$url = 'https://www.gravatar.com/avatar/' . $hash . '?s=300';
set_transient( $cache_key, $url, WEEK_IN_SECONDS );
return $url;
}
// No Gravatar — cache the negative result too
set_transient( $cache_key, '', WEEK_IN_SECONDS );
return '';
}
function thalim_en_url( string $url ): string {
if ( thalim_current_language() !== 'en' ) return $url;
$home = rtrim( home_url(), '/' );
$path = substr( $url, strlen( $home ) );
if ( str_starts_with( $path, '/en/' ) || $path === '/en' ) return $url;
return $home . '/en' . $path;
}
// Auto-prefix all WP-generated internal URLs in EN mode.
// Safe on admin: THALIM_ORIGINAL_URI starts with /wp-admin/ there,
// so thalim_current_language() returns 'fr' and thalim_en_url() is a no-op.
add_filter( 'term_link', 'thalim_en_url' ); // get_term_link(), get_category_link(), get_tag_link()
add_filter( 'post_link', 'thalim_en_url' ); // get_permalink() on regular posts
add_filter( 'page_link', 'thalim_en_url' ); // get_permalink() on pages
add_filter( 'post_type_link', 'thalim_en_url' ); // get_permalink() on CPTs
add_filter( 'author_link', 'thalim_en_url' ); // get_author_posts_url()
// Return the translated category name if viewing in EN and titre_anglais is set.
// Accepts a WP_Term, Timber\Term, term_id (int), or a name string + term_id.
function thalim_cat_name( $cat, string $lang = null ): string {
if ( $lang === null ) $lang = thalim_current_language();
if ( is_object( $cat ) ) {
$term_id = $cat->term_id ?? ( $cat->id ?? 0 );
$fallback = $cat->name;
} elseif ( is_numeric( $cat ) ) {
$term_id = (int) $cat;
$term = get_term( $term_id, 'category' );
$fallback = $term && ! is_wp_error( $term ) ? $term->name : (string) $cat;
} else {
return (string) $cat;
}
if ( $lang !== 'en' ) return $fallback;
$en = get_term_meta( $term_id, 'titre_anglais', true );
return ( $en !== '' && $en !== false ) ? $en : $fallback;
}
// Register bilingual and en_url as Twig filters
add_filter( 'timber/twig', function ( $twig ) {
$twig->addFilter( new \Twig\TwigFilter( 'bilingual', 'thalim_bilingual' ) );
$twig->addFilter( new \Twig\TwigFilter( 'en_url', 'thalim_en_url' ) );
$twig->addFilter( new \Twig\TwigFilter( 'cat_name', 'thalim_cat_name' ) );
return $twig;
} );
// Language switcher data (replaces pll_the_languages)
// Output matches the structure header.twig expects: slug, url, current_lang
function thalim_language_switcher(): array {
$uri = THALIM_ORIGINAL_URI;
$path = parse_url( $uri, PHP_URL_PATH ) ?? '/';
$query = ( $q = parse_url( $uri, PHP_URL_QUERY ) ) ? '?' . $q : '';
$is_en = thalim_current_language() === 'en';
$fr_path = $is_en ? ( substr( $path, 3 ) ?: '/' ) : $path;
$en_path = $is_en ? $path : '/en' . $path;
return [
'fr' => [ 'slug' => 'fr', 'url' => home_url( $fr_path ) . $query, 'current_lang' => ! $is_en ],
'en' => [ 'slug' => 'en', 'url' => home_url( $en_path ) . $query, 'current_lang' => $is_en ],
];
}
function theme_enqueue_assets() {
wp_enqueue_style(
'main-styles',
get_template_directory_uri() . '/css/style.css',
[],
filemtime(get_template_directory() . '/css/style.css')
);
wp_enqueue_style(
'iconoir',
'https://cdn.jsdelivr.net/gh/iconoir-icons/iconoir@main/css/iconoir.css',
[],
null
);
wp_enqueue_script(
'overlay',
get_template_directory_uri() . '/js/overlay.js',
[],
filemtime(get_template_directory() . '/js/overlay.js'),
true
);
if (is_front_page() || is_404()) {
wp_enqueue_script(
'animatedLogo',
get_template_directory_uri() . '/js/animatedLogo.js',
[],
filemtime(get_template_directory() . '/js/animatedLogo.js'),
true
);
wp_add_inline_script( 'animatedLogo', 'var themeDirURI = ' . wp_json_encode( get_template_directory_uri() ) . ';', 'before' );
}
if ( is_category() ) {
wp_enqueue_style( 'swiper', 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css', [], null );
wp_enqueue_script( 'swiper', 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js', [], null, true );
wp_enqueue_script( 'agendaView', get_template_directory_uri() . '/js/agendaView.js', ['swiper'], filemtime( get_template_directory() . '/js/agendaView.js' ), true );
wp_localize_script( 'agendaView', 'agendaViewData', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'load_more_posts' ),
'lang' => thalim_current_language(),
]);
}
if (is_front_page()) {
wp_enqueue_script(
'coloredWordsHero',
get_template_directory_uri() . '/js/coloredWordsHero.js',
[],
filemtime(get_template_directory() . '/js/coloredWordsHero.js'),
true
);
wp_enqueue_style(
'swiper',
'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css',
[],
null
);
wp_enqueue_script(
'swiper',
'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js',
[],
null,
true
);
wp_enqueue_script(
'annoncesSwiper',
get_template_directory_uri() . '/js/annoncesSwiper.js',
['swiper'],
filemtime(get_template_directory() . '/js/annoncesSwiper.js'),
true
);
wp_enqueue_script(
'messageLabo',
get_template_directory_uri() . '/js/messageLabo.js',
[],
filemtime(get_template_directory() . '/js/messageLabo.js'),
true
);
wp_enqueue_script(
'keywordCloud',
get_template_directory_uri() . '/js/keywordCloud.js',
[],
filemtime(get_template_directory() . '/js/keywordCloud.js'),
true
);
wp_enqueue_script(
'quickLinks',
get_template_directory_uri() . '/js/quickLinks.js',
[],
filemtime(get_template_directory() . '/js/quickLinks.js'),
true
);
$kw_tags = get_terms([
'taxonomy' => 'post_tag',
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
'lang' => '',
]);
if (!is_wp_error($kw_tags) && !empty($kw_tags)) {
$kw_lang = thalim_current_language();
$kw_tags = array_filter($kw_tags, function ($tag) {
return !get_term_meta($tag->term_id, 'ne_pas_afficher_dans_le_nuage', true);
});
wp_localize_script('keywordCloud', 'thalimTags', array_values(array_map(function ($tag) use ($kw_lang) {
return ['name' => html_entity_decode(thalim_bilingual($tag->name, $kw_lang), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'url' => get_term_link($tag)];
}, $kw_tags)));
}
}
wp_enqueue_script(
'navAxesToggle',
get_template_directory_uri() . '/js/navAxesToggle.js',
[],
filemtime(get_template_directory() . '/js/navAxesToggle.js'),
true
);
wp_enqueue_script(
'stickyHeader',
get_template_directory_uri() . '/js/stickyHeader.js',
[],
filemtime(get_template_directory() . '/js/stickyHeader.js'),
true
);
wp_enqueue_script(
'frenchTypography',
get_template_directory_uri() . '/js/frenchTypography.js',
[],
filemtime(get_template_directory() . '/js/frenchTypography.js'),
true
);
if (is_page('membres')) {
wp_enqueue_script(
'membresFilters',
get_template_directory_uri() . '/js/membresFilters.js',
[],
filemtime(get_template_directory() . '/js/membresFilters.js'),
true
);
wp_enqueue_script(
'membresPopover',
get_template_directory_uri() . '/js/membresPopover.js',
[],
filemtime(get_template_directory() . '/js/membresPopover.js'),
true
);
}
if (is_single() || is_author() || is_page('membres') || is_page('le-laboratoire') || is_page('programmes-de-recherche')) {
wp_enqueue_script(
'seanceToggle',
get_template_directory_uri() . '/js/seanceToggle.js',
[],
filemtime(get_template_directory() . '/js/seanceToggle.js'),
true
);
}
if (is_single()) {
wp_enqueue_style(
'swiper',
'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css',
[],
null
);
wp_enqueue_script(
'swiper',
'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js',
[],
null,
true
);
wp_enqueue_script(
'imageSwiper',
get_template_directory_uri() . '/js/imageSwiper.js',
['swiper'],
filemtime(get_template_directory() . '/js/imageSwiper.js'),
true
);
}
wp_enqueue_script(
'fitPostCardTitle',
get_template_directory_uri() . '/js/fitPostCardTitle.js',
['frenchTypography'],
filemtime(get_template_directory() . '/js/fitPostCardTitle.js'),
true
);
$is_archive_page = is_category() || is_tax() || is_tag() || is_page(['annonces', 'announcements']) || is_search();
if ($is_archive_page) {
wp_enqueue_script(
'infiniteScroll',
get_template_directory_uri() . '/js/infiniteScroll.js',
[],
filemtime(get_template_directory() . '/js/infiniteScroll.js'),
true
);
wp_localize_script('infiniteScroll', 'infiniteScrollData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('load_more_posts'),
'lang' => thalim_current_language(),
]);
}
if ($is_archive_page) {
wp_enqueue_script(
'categoryFilters',
get_template_directory_uri() . '/js/categoryFilters.js',
[],
filemtime(get_template_directory() . '/js/categoryFilters.js'),
true
);
}
// wp_enqueue_style('wp-block-library');
}
add_action('wp_enqueue_scripts', 'theme_enqueue_assets');
// Auto-sync display_name from first_name + last_name on every profile save
add_action( 'profile_update', 'thalim_sync_display_name' );
add_action( 'user_register', 'thalim_sync_display_name' );
add_action( 'pods_api_post_save_pod_item_user', function( $pieces ) {
$user_id = isset( $pieces['id'] ) ? intval( $pieces['id'] ) : 0;
if ( $user_id ) thalim_sync_display_name( $user_id );
} );
function thalim_sync_display_name( int $user_id ): void {
$first = get_user_meta( $user_id, 'first_name', true );
$last = get_user_meta( $user_id, 'last_name', true );
$name = trim( "$first $last" );
if ( ! $name ) return;
// Bypass the filter to avoid infinite loop
remove_action( 'profile_update', 'thalim_sync_display_name' );
wp_update_user( [ 'ID' => $user_id, 'display_name' => $name ] );
add_action( 'profile_update', 'thalim_sync_display_name' );
}
add_action( 'admin_enqueue_scripts', 'enqueue_admin_js' );
function enqueue_admin_js(){
wp_enqueue_style(
'adminDashboardStyles',
get_template_directory_uri() . '/css/admin.css',
[],
filemtime(get_template_directory() . '/css/admin.css')
);
wp_enqueue_script(
'adminDashboardMods',
get_template_directory_uri() . '/js/adminDashboardMods.js',
[],
filemtime(get_template_directory() . '/js/adminDashboardMods.js'),
true
);
$axes_groups = thalim_get_axes_filter_groups();
if ( current_user_can( 'contributor' ) && ! current_user_can( 'edit_others_posts' ) ) {
$axes_groups = array_slice( $axes_groups, 0, 1 );
}
wp_localize_script( 'adminDashboardMods', 'thalimAxesGroups', $axes_groups );
// [DÉSACTIVÉ] adminFormRestore — décommenter pour réactiver
// $screen = get_current_screen();
// if ( $screen && 'post' === $screen->base ) {
// wp_enqueue_script(
// 'adminFormRestore',
// get_template_directory_uri() . '/js/adminFormRestore.js',
// [ 'jquery' ],
// filemtime(get_template_directory() . '/js/adminFormRestore.js'),
// true
// );
// }
}
add_theme_support('title-tag');
// Apply bilingual split to the browser tab title + translate category names
add_filter('document_title_parts', function ($title_parts) {
if (!empty($title_parts['title'])) {
$title_parts['title'] = thalim_bilingual($title_parts['title']);
// On category archives, replace the title with the translated category name
if ( is_category() ) {
$cat = get_queried_object();
if ( $cat ) {
$title_parts['title'] = thalim_cat_name( $cat );
}
}
}
return $title_parts;
});
function add_to_context($context) {
$current_lang = thalim_current_language();
// menus
$menu_slug = ($current_lang === 'en') ? 'Navigation-en' : 'Navigation';
$context['menu'] = Timber::get_menu($menu_slug);
$footer_menu_slug = ($current_lang === 'en') ? 'Footer-en' : 'Footer';
$context['footer_menu'] = Timber::get_menu($footer_menu_slug);
// contenus généraux (single post, bilingual)
$gc_posts = Timber::get_posts([
'post_type' => 'contenu_general',
'posts_per_page' => 1,
'orderby' => 'ID',
'order' => 'ASC',
]);
$gc_post = $gc_posts[0] ?? null;
if ( $gc_post ) {
$context['gc'] = [
'umr' => thalim_bilingual( $gc_post->umr ?: '', $current_lang ),
'thalim' => thalim_bilingual( $gc_post->thalim ?: '', $current_lang ),
'siecles' => thalim_bilingual( $gc_post->siecles ?: '', $current_lang ),
'presentation' => ( $current_lang === 'en' && $gc_post->presentation_en ) ? $gc_post->presentation_en : $gc_post->presentation,
'presentation_detail' => ( $current_lang === 'en' && $gc_post->presentation_detail_en ) ? $gc_post->presentation_detail_en : $gc_post->presentation_detail,
];
} else {
$context['gc'] = [];
}
$context['current_language'] = $current_lang;
// Axes thématiques courants (annee_fin >= current year) for navigation dropdown
$current_year = (int) date('Y');
$all_axes = get_terms(['taxonomy' => 'axe_thematique', 'hide_empty' => false, 'orderby' => 'name']);
$axes_courants = [];
if (!is_wp_error($all_axes)) {
foreach ($all_axes as $axe) {
$fin = (int) get_term_meta($axe->term_id, 'annee_fin', true);
if ($fin >= $current_year) {
$link = get_term_link($axe);
if (!is_wp_error($link)) {
$axes_courants[] = [
'name' => thalim_bilingual($axe->name, $current_lang),
'link' => $link,
'ordre' => (int) get_term_meta($axe->term_id, 'ordre_daffichage', true),
];
}
}
}
usort($axes_courants, fn($a, $b) => $a['ordre'] <=> $b['ordre']);
}
$context['axes_courants'] = $axes_courants;
// Annonces page URL (language-aware)
$annonces_page = get_page_by_path('annonces');
$annonces_base = $annonces_page ? get_permalink($annonces_page->ID) : home_url('/annonces/');
$context['annonces_url'] = $annonces_base;
// Language switcher
$context['languages'] = thalim_language_switcher();
return $context;
}
// Restrict Contributors to see only their own posts in admin,
// but also include posts where they appear in membres/autre_membres.
function restrict_contributor_posts( $query ) {
if ( ! is_admin() || ! $query->is_main_query() || current_user_can( 'edit_others_posts' ) ) {
return;
}
global $user_ID, $wpdb;
// Posts where the user is listed as membre or autre_membre.
$membre_ids = array_map( 'intval', (array) $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT post_id FROM {$wpdb->postmeta}
WHERE meta_key IN ('membres', 'autre_membres') AND meta_value = %s",
$user_ID
)
) );
if ( empty( $membre_ids ) ) {
// Fast path: no membre posts, use simple author filter.
$query->set( 'author', $user_ID );
return;
}
// Posts authored by this user.
$authored_ids = array_map( 'intval', (array) $wpdb->get_col(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_author = %d",
$user_ID
)
) );
$all_ids = array_unique( array_merge( $authored_ids, $membre_ids ) );
// post__in with [0] returns nothing when the combined set is empty.
$query->set( 'post__in', empty( $all_ids ) ? [ 0 ] : $all_ids );
}
add_action( 'pre_get_posts', 'restrict_contributor_posts' );
/**
* Let contributors listed as membres/autre_membres on a post edit it.
*
* user_has_cap modifies capabilities only for this single check —
* it does not permanently alter the user's capability set.
*/
function thalim_membres_can_edit_post( $allcaps, $caps, $args, $user ) {
// Editors and above already have edit_others_posts — nothing to do.
if ( ! empty( $allcaps['edit_others_posts'] ) ) {
return $allcaps;
}
if ( empty( $args[0] ) ) {
return $allcaps;
}
$cap = $args[0];
// Meta caps that carry a post ID in $args[2] (e.g. wp-admin/post.php load).
$meta_caps_with_id = [ 'edit_post', 'edit_page' ];
// Primitive caps called during the admin save/publish flow *without* a
// post_id (e.g. wp-admin/includes/post.php:76 checks edit_others_posts
// directly when $post_author !== current user). We infer the post_id from
// the request so we can still authorize membres per-post.
$primitive_caps_in_save_flow = [
'edit_others_posts',
'edit_others_pages',
'edit_published_posts',
'edit_published_pages',
'publish_posts',
'publish_pages',
];
$post_id = 0;
if ( in_array( $cap, $meta_caps_with_id, true ) && ! empty( $args[2] ) ) {
$post_id = (int) $args[2];
} elseif ( in_array( $cap, $primitive_caps_in_save_flow, true ) ) {
$post_id = (int) ( $_POST['post_ID'] ?? $_REQUEST['post'] ?? 0 );
}
if ( ! $post_id ) {
return $allcaps;
}
$user_id = $user->ID;
$membre_ids = array_map(
'intval',
array_merge(
(array) get_post_meta( $post_id, 'membres', false ),
(array) get_post_meta( $post_id, 'autre_membres', false )
)
);
if ( in_array( $user_id, $membre_ids, true ) ) {
// Grant every primitive cap mapped for this check
// (e.g. edit_others_posts, edit_published_posts).
foreach ( $caps as $c ) {
$allcaps[ $c ] = true;
}
}
return $allcaps;
}
add_filter( 'user_has_cap', 'thalim_membres_can_edit_post', 10, 4 );
// Prevent WP_Posts_List_Table from auto-redirecting contributors to the "Mine"
// view. The constructor sets $_GET['author'] = current_user_id() when the user
// lacks edit_others_posts and no other filter is active. Setting all_posts=1
// before the list table is constructed short-circuits that condition.
add_action( 'load-edit.php', function () {
if ( current_user_can( 'edit_others_posts' ) ) {
return;
}
if (
empty( $_REQUEST['post_status'] )
&& empty( $_REQUEST['all_posts'] )
&& empty( $_REQUEST['author'] )
&& empty( $_REQUEST['show_sticky'] )
) {
$_GET['all_posts'] = 1;
$_REQUEST['all_posts'] = 1;
}
} );
// Adjust post-status counts so contributors see only posts they can access
// (posts they authored + posts listed in membres/autre_membres), not all posts.
// The wp_count_posts filter runs even on cached values, so it won't pollute
// the shared cache.
add_filter( 'wp_count_posts', function ( $counts, $type, $perm ) {
if ( ! is_admin() || current_user_can( 'edit_others_posts' ) ) {
return $counts;
}
global $wpdb;
$user_id = get_current_user_id();
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT post_status, COUNT(*) AS num_posts
FROM {$wpdb->posts}
WHERE post_type = %s
AND ID IN (
SELECT ID FROM {$wpdb->posts}
WHERE post_author = %d AND post_type = %s
UNION
SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key IN ('membres', 'autre_membres') AND meta_value = %s
)
GROUP BY post_status",
$type,
$user_id,
$type,
(string) $user_id
),
ARRAY_A
);
$new_counts = array_fill_keys( get_post_stati(), 0 );
foreach ( $results as $row ) {
$new_counts[ $row['post_status'] ] = (int) $row['num_posts'];
}
return (object) $new_counts;
}, 10, 3 );
// Show Pods user meta fields on the Add New User page (user-new.php).
// Fires the same hook Pods listens to on profile.php / user-edit.php.
add_action('user_new_form', function($operation) {
if ($operation === 'add-existing-user') return;
$user = new WP_User(0);
do_action('show_user_profile', $user);
});
// Save Pods user meta fields when a new user is registered.
// Re-fires Pods' personal_options_update callback with the new user ID
// while $_POST still contains the submitted form data.
add_action('user_register', function($user_id) {
do_action('personal_options_update', $user_id);
}, 20);
// Remove the Pods autocomplete 30-item cap for the "étiquettes" (post_tag) pick field
// so all tags are available in the dropdown, not just the first 30 alphabetically.
add_filter( 'pods_form_ui_field_pick_autocomplete_limit', function( $limit, $name ) {
if ( 'etiquettes' === $name ) {
return -1;
}
return $limit;
}, 10, 2 );
// Admin "Programmes de recherche" taxonomy list:
// - Filter by "Type de programme" (select injected via JS into the search form).
// - Add a "Type de programme" column to the list table.
// Server-side filtering: read the GET param and add a meta_query on pre_get_terms.
add_action( 'pre_get_terms', function( $query ) {
if ( ! is_admin() ) {
return;
}
if ( ! in_array( 'programme_de_recherche', (array) $query->query_vars['taxonomy'], true ) ) {
return;
}
$type = isset( $_GET['type_de_programme'] ) ? sanitize_text_field( $_GET['type_de_programme'] ) : '';
if ( '' === $type ) {
return;
}
$query->query_vars['meta_query'] = [
[
'key' => 'type_de_programme',
'value' => $type,
'compare' => '=',
],
];
} );
// Column: add "Type de programme" to the list table, replacing "Description".
add_filter( 'manage_edit-programme_de_recherche_columns', function( $columns ) {
unset( $columns['description'] );
$columns['type_de_programme'] = __( 'Type de programme', 'thalim' );
return $columns;
} );
add_filter( 'manage_programme_de_recherche_custom_column', function( $output, $column_name, $term_id ) {
if ( 'type_de_programme' !== $column_name ) {
return $output;
}
return esc_html( get_term_meta( (int) $term_id, 'type_de_programme', true ) ?: '&mdash;' );
}, 10, 3 );
// Admin tag list: replace the "Description" column with the custom boolean field
// "ne_pas_afficher_dans_le_nuage" (Ne pas afficher dans le nuage de mots-clé).
add_filter( 'manage_edit-post_tag_columns', function( $columns ) {
unset( $columns['description'] );
$columns['nuage_exclus'] = __( 'Exclure du nuage', 'thalim' );
return $columns;
} );
add_filter( 'manage_post_tag_custom_column', function( $output, $column_name, $term_id ) {
if ( 'nuage_exclus' !== $column_name ) {
return $output;
}
$val = get_term_meta( (int) $term_id, 'ne_pas_afficher_dans_le_nuage', true );
return $val ? '&#10003;' : '&mdash;';
}, 10, 3 );
require_once __DIR__ . '/inc/admin-users-filter.php';
require_once __DIR__ . '/inc/pods-conditional-required.php';
require_once __DIR__ . '/inc/pods-save-error-handler.php';
require_once __DIR__ . '/inc/post-title-required.php';
require_once __DIR__ . '/inc/post-card-helpers.php';
require_once __DIR__ . '/inc/single-helpers.php';
require_once __DIR__ . '/inc/author-helpers.php';
// In admin post list, filter by exact category only (exclude subcategories)
function thalim_exact_category_filter( $query ) {
if ( is_admin() && $query->is_main_query() ) {
$cat = $query->get( 'cat' );
if ( $cat ) {
$query->set( 'cat', '' );
$query->set( 'tax_query', [
[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [ (int) $cat ],
'include_children' => false,
],
] );
}
}
}
add_action( 'pre_get_posts', 'thalim_exact_category_filter' );
// ── "Vie du labo" (cat 9) — restricted to logged-in users ────
add_action( 'pre_get_posts', function( $query ) {
if ( is_user_logged_in() ) return;
$excluded = $query->get( 'category__not_in' );
if ( ! is_array( $excluded ) ) $excluded = $excluded ? [ $excluded ] : [];
if ( ! in_array( 9, $excluded ) ) {
$excluded[] = 9;
$query->set( 'category__not_in', $excluded );
}
} );
add_action( 'template_redirect', function() {
if ( ! is_user_logged_in() && is_category( 9 ) ) {
wp_safe_redirect( home_url( '/' ) );
exit;
}
} );
// Séance de séminaire (cat 12): redirect to parent séminaire with #seance-{ID} anchor
add_action( 'template_redirect', function() {
if ( ! is_single() ) return;
if ( ! has_category( 12 ) ) return;
global $wpdb;
$seance_id = get_the_ID();
$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
) );
if ( $parent_id ) {
wp_redirect( get_permalink( (int) $parent_id ) . '#seance-' . $seance_id, 301 );
exit;
}
} );
add_filter( 'wp_nav_menu_objects', function( $items, $args ) {
if ( is_user_logged_in() ) return $items;
return array_values( array_filter( $items, function( $item ) {
if ( $item->object === 'category' && (int) $item->object_id === 9 ) return false;
if ( strpos( $item->url, 'vie-du-labo' ) !== false ) return false;
return true;
} ) );
}, 10, 2 );
// Rewrite rule for /category/{slug}/autres → posts directly in parent category
add_action('init', function() {
add_rewrite_rule(
'category/([^/]+)/autres/?$',
'index.php?category_name=$matches[1]&thalim_direct_posts=1',
'top'
);
});
add_filter('query_vars', function($vars) {
$vars[] = 'thalim_direct_posts';
return $vars;
});
// Admin bar customizations (front + back)
add_action('admin_bar_menu', function($wp_admin_bar) {
$wp_admin_bar->remove_node('wp-logo');
$wp_admin_bar->remove_node('customize');
foreach ($wp_admin_bar->get_nodes() as $node) {
if (empty($node->title) || stripos($node->title, 'article') === false) continue;
$node->title = preg_replace_callback('/article/i', function($m) {
$w = $m[0];
if ($w === strtoupper($w)) return 'ANNONCE';
if ($w[0] === strtoupper($w[0])) return 'Annonce';
return 'annonce';
}, $node->title);
$wp_admin_bar->add_node((array) $node);
}
}, 999);
add_action('wp_before_admin_bar_render', function() {
global $wp_admin_bar;
$wp_admin_bar->remove_node('wpforms-menu');
});
// Force visual (TinyMCE) editor for all users — our admin CSS hides the
// Visual/Code tabs, so if a user has "Disable the visual editor" checked
// in their profile they get stuck in code mode with no way to switch back.
add_filter( 'user_can_richedit', '__return_true' );
// Non-admins: hide dashboard and tools menu, redirect to posts list
add_action( 'admin_menu', function() {
if ( ! current_user_can( 'manage_options' ) ) {
remove_menu_page( 'tools.php' );
remove_menu_page( 'index.php' );
}
} );
// Redirect non-admins away from dashboard to their posts list
add_action( 'admin_init', function() {
if ( current_user_can( 'manage_options' ) ) return;
global $pagenow;
if ( $pagenow === 'index.php' ) {
wp_safe_redirect( admin_url( 'edit.php' ) );
exit;
}
} );
// After login, send non-admins to posts list instead of dashboard
add_filter( 'login_redirect', function( $redirect_to, $requested, $user ) {
if ( ! is_wp_error( $user ) && ! $user->has_cap( 'manage_options' ) ) {
return admin_url( 'edit.php' );
}
return $redirect_to;
}, 10, 3 );
// AJAX handler for infinite scroll on category pages
function thalim_load_more_posts() {
check_ajax_referer('load_more_posts', 'nonce');
$GLOBALS['thalim_lang_override'] = sanitize_key( $_POST['lang'] ?? 'fr' );
$page = intval($_POST['page']);
$category = isset($_POST['category']) ? intval($_POST['category']) : 0;
$axe = isset($_POST['axe']) ? intval($_POST['axe']) : 0;
$date_from = isset($_POST['date_from']) ? sanitize_text_field($_POST['date_from']) : '';
$date_to = isset($_POST['date_to']) ? sanitize_text_field($_POST['date_to']) : '';
$term_taxonomy = isset($_POST['taxonomy']) ? sanitize_key($_POST['taxonomy']) : '';
$term_id = isset($_POST['term']) ? intval($_POST['term']) : 0;
$cat_filter = isset($_POST['filter_cat']) ? intval($_POST['filter_cat']) : 0;
$filter_autres = isset($_POST['filter_autres']) ? intval($_POST['filter_autres']) : 0;
$exclude_cats = isset($_POST['exclude_cats']) ? sanitize_text_field($_POST['exclude_cats']) : '';
$search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
$query_args = [
'post_type' => 'post',
'posts_per_page' => 12,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
];
if ($search) {
$query_args['s'] = $search;
$query_args['relevanssi'] = true;
$query_args['orderby'] = 'relevance';
}
// Build tax_query — may combine category page filter, taxonomy term, and cat filter
$tax_clauses = [];
if ($category) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$category],
'include_children' => false,
];
}
if ($term_taxonomy && $term_id) {
$tax_clauses[] = [
'taxonomy' => $term_taxonomy,
'field' => 'term_id',
'terms' => [$term_id],
];
// Exclure les séances de séminaire sur les pages de taxonomie
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [12],
'operator' => 'NOT IN',
];
}
if ($cat_filter) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$cat_filter],
'include_children' => !$filter_autres,
];
}
if ($exclude_cats) {
$ids = array_filter(array_map('intval', explode(',', $exclude_cats)));
if (!empty($ids)) {
$tax_clauses[] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $ids,
'operator' => 'NOT IN',
];
}
}
if (!empty($tax_clauses)) {
$query_args['tax_query'] = count($tax_clauses) > 1
? array_merge(['relation' => 'AND'], $tax_clauses)
: $tax_clauses;
}
if ($axe) {
$query_args['meta_query'] = [[
'key' => 'axes_thematiques',
'value' => $axe,
'type' => 'NUMERIC',
]];
}
if ($date_from || $date_to) {
$query_args['thalim_event_date_filter'] = ['from' => $date_from, 'to' => $date_to];
}
// Exclude pinned posts on category pages to avoid duplicates (they already appear at the top)
if ($category) {
$today = date( 'Y-m-d' );
$pinned_query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'lang' => '',
'tax_query' => [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => [$category],
'include_children' => false,
]],
'meta_query' => [[
'key' => 'epingler_dans_la_categorie',
'value' => '1',
]],
]);
$pinned_ids = [];
foreach ( $pinned_query->posts as $pid ) {
$fin = get_post_meta( $pid, 'date_de_fin_depinglage', true );
if ( empty( $fin ) || $fin === '0000-00-00' || $fin >= $today ) {
$pinned_ids[] = $pid;
}
}
if ( ! empty( $pinned_ids ) ) {
$query_args['post__not_in'] = $pinned_ids;
}
}
$posts = Timber::get_posts($query_args);
if (empty($posts)) {
wp_send_json_success(['html' => '']);
return;
}
$cards = thalim_get_cards_data($posts);
$html = '';
foreach ($posts as $post) {
$html .= Timber::compile('partials/post-card.twig', [
'post' => $post,
'card' => $cards[$post->ID],
'show_category' => true,
'type_only' => $category > 0, // category pages: type chip only; everywhere else (taxonomy, search, annonces): type or category name
]);
}
wp_send_json_success(['html' => $html]);
}
add_action('wp_ajax_load_more_posts', 'thalim_load_more_posts');
add_action('wp_ajax_nopriv_load_more_posts', 'thalim_load_more_posts');
/**
* Build structured data for one agenda slider card.
*/
function thalim_get_agenda_card_data( $post_id, $lang = 'fr' ) {
$months_fr = ['jan.','fév.','mars','avr.','mai','juin','juil.','août','sept.','oct.','nov.','déc.'];
$months_en = ['Jan.','Feb.','Mar.','Apr.','May','Jun.','Jul.','Aug.','Sep.','Oct.','Nov.','Dec.'];
$months = $lang === 'en' ? $months_en : $months_fr;
$raw_debut = get_post_meta( $post_id, 'date_de_debut', true );
$raw_datetime = get_post_meta( $post_id, 'datetime', true );
if ( $raw_debut && ! str_starts_with( $raw_debut, '0000' ) ) {
$ts = strtotime( $raw_debut );
} elseif ( $raw_datetime && ! str_starts_with( $raw_datetime, '0000' ) ) {
$ts = strtotime( $raw_datetime );
} else {
$ts = get_post_timestamp( $post_id );
}
$raw_fin = get_post_meta( $post_id, 'date_de_fin', true );
$ts_debut = ( $raw_debut && ! str_starts_with( $raw_debut, '0000' ) ) ? strtotime( $raw_debut ) : 0;
$ts_fin = ( $raw_fin && ! str_starts_with( $raw_fin, '0000' ) ) ? strtotime( $raw_fin ) : 0;
// Build date_label — same rules as single.twig sidebar
$fmt_debut = $ts_debut ? thalim_format_date( $raw_debut, $lang ) : '';
$fmt_fin = $ts_fin ? thalim_format_date( $raw_fin, $lang ) : '';
$fmt_dt = ( $raw_datetime && ! str_starts_with( $raw_datetime, '0000' ) )
? thalim_format_date( $raw_datetime, $lang ) : '';
$h_debut = substr( get_post_meta( $post_id, 'heure_de_debut', true ) ?: '', 0, 5 );
$h_fin = substr( get_post_meta( $post_id, 'heure_de_fin', true ) ?: '', 0, 5 );
if ( $fmt_debut || $fmt_fin ) {
if ( $ts_debut && $ts_fin && date( 'Y-m-d', $ts_debut ) === date( 'Y-m-d', $ts_fin ) ) {
// Same day
if ( $h_debut && $h_fin ) {
$date_label = ( $lang === 'en' ? 'On ' : 'Le ' ) . $fmt_debut
. ' ' . ( $lang === 'en' ? 'from ' : 'de ' ) . $h_debut
. ' ' . ( $lang === 'en' ? 'to ' : 'à ' ) . $h_fin;
} elseif ( $h_debut ) {
$date_label = $fmt_debut . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut;
} else {
$date_label = $fmt_debut;
}
} elseif ( $fmt_debut && $fmt_fin ) {
$date_label = ( $lang === 'en' ? 'From ' : 'Du ' ) . $fmt_debut
. ' ' . ( $lang === 'en' ? 'to ' : 'au ' ) . $fmt_fin;
} elseif ( $fmt_debut ) {
$date_label = $h_debut
? $fmt_debut . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut
: $fmt_debut;
} else {
$date_label = ( $lang === 'en' ? 'Until ' : "Jusqu'au " ) . $fmt_fin;
}
} elseif ( $fmt_dt ) {
$date_label = $h_debut
? $fmt_dt . ( $lang === 'en' ? ' at ' : ' à ' ) . $h_debut
: $fmt_dt;
} else {
$date_label = '';
}
$type_fields = [
'type_colloque_journee_d_etude', 'type_soutenance', 'type_evenement_culturel',
'type_media', 'type_captation', 'type_revue_collection', 'type_autre',
];
$type_label = '';
foreach ( $type_fields as $f ) {
$v = get_post_meta( $post_id, $f, true );
if ( $v ) { $type_label = thalim_bilingual( $v, $lang ); break; }
}
if ( ! $type_label ) {
foreach ( get_the_category( $post_id ) as $cat ) {
if ( $cat->parent ) { $type_label = thalim_cat_name( $cat, $lang ); break; }
}
}
$end_day = $end_month = $end_year = null;
if ( $ts_fin && date( 'Ymd', $ts_fin ) !== date( 'Ymd', $ts ) ) {
$end_day = (int) date( 'j', $ts_fin );
$end_month = $months[ (int) date( 'n', $ts_fin ) - 1 ];
$end_year = (int) date( 'Y', $ts_fin );
}
// Séance de séminaire (cat 12): link to parent séminaire at #seance-{ID}
$link = get_permalink( $post_id );
if ( in_array( 12, wp_list_pluck( get_the_category( $post_id ), 'term_id' ) ) ) {
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) $post_id
) );
if ( $parent_id ) {
$link = get_permalink( (int) $parent_id ) . '#seance-' . $post_id;
}
}
return [
'day' => (int) date( 'j', $ts ),
'month' => $months[ (int) date( 'n', $ts ) - 1 ],
'year' => (int) date( 'Y', $ts ),
'end_day' => $end_day,
'end_month' => $end_month,
'end_year' => $end_year,
'type_label' => $type_label,
'date_label' => $date_label,
'lieu' => thalim_bilingual( get_post_meta( $post_id, 'lieu', true ) ?: '', $lang ),
'link' => $link,
];
}
function thalim_load_more_agenda() {
check_ajax_referer( 'load_more_posts', 'nonce' );
$lang = sanitize_key( $_POST['lang'] ?? 'fr' );
$GLOBALS['thalim_lang_override'] = $lang;
$page = intval( $_POST['page'] );
$category = isset( $_POST['category'] ) ? intval( $_POST['category'] ) : 0;
$axe = isset( $_POST['axe'] ) ? intval( $_POST['axe'] ) : 0;
$date_from = isset( $_POST['date_from'] ) ? sanitize_text_field( $_POST['date_from'] ) : '';
$date_to = isset( $_POST['date_to'] ) ? sanitize_text_field( $_POST['date_to'] ) : '';
$term_taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( $_POST['taxonomy'] ) : '';
$term_id = isset( $_POST['term'] ) ? intval( $_POST['term'] ) : 0;
$cat_filter = isset( $_POST['filter_cat'] ) ? intval( $_POST['filter_cat'] ) : 0;
$filter_autres = isset( $_POST['filter_autres'] ) ? intval( $_POST['filter_autres'] ) : 0;
$exclude_cats = isset( $_POST['exclude_cats'] ) ? sanitize_text_field( $_POST['exclude_cats'] ) : '';
$query_args = [
'post_type' => 'post',
'posts_per_page' => 12,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
'lang' => '',
'thalim_event_date_order' => true,
];
$include_children = ! empty( $_POST['include_children'] );
$tax_clauses = [];
if ( $category ) {
$tax_clauses[] = [ 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => [ $category ], 'include_children' => $include_children ];
}
if ( $term_taxonomy && $term_id ) {
$tax_clauses[] = [ 'taxonomy' => $term_taxonomy, 'field' => 'term_id', 'terms' => [ $term_id ] ];
$tax_clauses[] = [ 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => [ 12 ], 'operator' => 'NOT IN' ];
}
if ( $cat_filter ) {
$tax_clauses[] = [ 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => [ $cat_filter ], 'include_children' => ! $filter_autres ];
}
if ( $exclude_cats ) {
$ids = array_filter( array_map( 'intval', explode( ',', $exclude_cats ) ) );
if ( ! empty( $ids ) ) {
$tax_clauses[] = [ 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => $ids, 'operator' => 'NOT IN' ];
}
}
if ( ! empty( $tax_clauses ) ) {
$query_args['tax_query'] = count( $tax_clauses ) > 1
? array_merge( [ 'relation' => 'AND' ], $tax_clauses )
: $tax_clauses;
}
if ( $axe ) {
$query_args['meta_query'] = [[ 'key' => 'axes_thematiques', 'value' => $axe, 'type' => 'NUMERIC' ]];
}
if ( $date_from || $date_to ) {
$query_args['thalim_event_date_filter'] = [ 'from' => $date_from, 'to' => $date_to ];
}
// On first page, count future events to find today's anchor position
$today_offset = 0;
if ( (int) $page === 1 ) {
$offset_args = $query_args;
$offset_args['posts_per_page'] = 1;
$offset_args['no_found_rows'] = false;
$offset_args['paged'] = 1;
$today_str = current_time( 'Y-m-d' );
$existing_filter = $offset_args['thalim_event_date_filter'] ?? [];
$offset_args['thalim_event_date_filter'] = array_merge(
$existing_filter,
[ 'from' => $today_str ]
);
$future_query = new \WP_Query( $offset_args );
$today_offset = (int) $future_query->found_posts;
}
$posts = Timber::get_posts( $query_args );
if ( empty( $posts ) ) {
$response = [ 'html' => '' ];
if ( (int) $page === 1 ) $response['today_offset'] = $today_offset;
wp_send_json_success( $response );
return;
}
$html = '';
foreach ( $posts as $post ) {
$data = thalim_get_agenda_card_data( $post->ID, $lang );
$html .= Timber::compile( 'partials/agenda-card.twig', array_merge( $data, [ 'post' => $post ] ) );
}
$response = [ 'html' => $html ];
if ( (int) $page === 1 ) $response['today_offset'] = $today_offset;
wp_send_json_success( $response );
}
add_action( 'wp_ajax_load_more_agenda', 'thalim_load_more_agenda' );
add_action( 'wp_ajax_nopriv_load_more_agenda', 'thalim_load_more_agenda' );
// Event date ordering: LEFT JOIN + COALESCE(date_de_debut, post_date)
// Activated by adding 'thalim_event_date_order' => true to WP_Query args.
// Event date ordering: COALESCE(date_de_debut, datetime, post_date)
// Activated by adding 'thalim_event_date_order' => true to WP_Query args.
add_filter('posts_join', function ($join, $query) {
if (!$query->get('thalim_event_date_order') && !$query->get('thalim_event_date_filter')) return $join;
global $wpdb;
$join .= " LEFT JOIN {$wpdb->postmeta} AS thalim_ed"
. " ON (thalim_ed.post_id = {$wpdb->posts}.ID"
. " AND thalim_ed.meta_key = 'date_de_debut') ";
$join .= " LEFT JOIN {$wpdb->postmeta} AS thalim_dt"
. " ON (thalim_dt.post_id = {$wpdb->posts}.ID"
. " AND thalim_dt.meta_key = 'datetime') ";
return $join;
}, 10, 2);
add_filter('posts_orderby', function ($orderby, $query) {
if (!$query->get('thalim_event_date_order')) return $orderby;
global $wpdb;
$valid = "IS NOT NULL AND %s != '' AND %s NOT LIKE '0000-00-00%%'";
return "CASE"
. " WHEN thalim_ed.meta_value " . sprintf($valid, 'thalim_ed.meta_value', 'thalim_ed.meta_value') . " THEN thalim_ed.meta_value"
. " WHEN thalim_dt.meta_value " . sprintf($valid, 'thalim_dt.meta_value', 'thalim_dt.meta_value') . " THEN thalim_dt.meta_value"
. " ELSE {$wpdb->posts}.post_date"
. " END DESC";
}, 10, 2);
// Event date range filter: uses same CASE logic as ordering so date_de_debut/datetime take priority over post_date.
// Activated by adding 'thalim_event_date_filter' => ['from' => $date_from, 'to' => $date_to] to WP_Query args.
add_filter('posts_where', function ($where, $query) {
$filter = $query->get('thalim_event_date_filter');
if (empty($filter) || (!isset($filter['from']) && !isset($filter['to']))) return $where;
global $wpdb;
$effective = "CASE"
. " WHEN thalim_ed.meta_value IS NOT NULL AND thalim_ed.meta_value != '' AND thalim_ed.meta_value NOT LIKE '0000-00-00%' THEN thalim_ed.meta_value"
. " WHEN thalim_dt.meta_value IS NOT NULL AND thalim_dt.meta_value != '' AND thalim_dt.meta_value NOT LIKE '0000-00-00%' THEN thalim_dt.meta_value"
. " ELSE {$wpdb->posts}.post_date"
. " END";
if (!empty($filter['from'])) {
$from = $wpdb->prepare('%s', $filter['from']);
$where .= " AND ({$effective}) >= {$from}";
}
if (!empty($filter['to'])) {
$to = $wpdb->prepare('%s', $filter['to'] . ' 23:59:59');
$where .= " AND ({$effective}) <= {$to}";
}
return $where;
}, 10, 2);
// ── Axes thématiques groupés pour les filtres ──────────────────
// Retourne un tableau de groupes triés par période (plus récent en premier,
// "passés" toujours en dernier). Chaque terme contient id, name, ordre.
function thalim_get_axes_filter_groups() {
$terms = get_terms( [ 'taxonomy' => 'axe_thematique', 'hide_empty' => false ] );
$axes_map = [];
foreach ( $terms as $term ) {
$debut = trim( get_term_meta( $term->term_id, 'annee_debut', true ) );
$fin = trim( get_term_meta( $term->term_id, 'annee_fin', true ) );
if ( $debut && $fin ) {
$key = $debut . '-' . $fin;
$label = $debut . ' ' . $fin;
} else {
$key = 'passes';
$label = 'Axes antérieurs';
}
if ( ! isset( $axes_map[ $key ] ) ) {
$axes_map[ $key ] = [ 'label' => $label, 'debut' => intval( $debut ), 'terms' => [] ];
}
$ordre = trim( get_term_meta( $term->term_id, 'ordre_daffichage', true ) );
$axes_map[ $key ]['terms'][] = [
'id' => $term->term_id,
'name' => $term->name,
'ordre' => $ordre !== '' ? intval( $ordre ) : null,
'href' => get_term_link( $term ),
];
}
// Tri des groupes : plus récent en premier, passés toujours en dernier
uasort( $axes_map, function ( $a, $b ) {
if ( $a['label'] === 'Axes antérieurs' ) return 1;
if ( $b['label'] === 'Axes antérieurs' ) return -1;
return $b['debut'] - $a['debut'];
} );
// Tri des termes dans chaque groupe : ordre_daffichage d'abord, puis alphabétique
foreach ( $axes_map as &$group ) {
usort( $group['terms'], function ( $a, $b ) {
$a_has = $a['ordre'] !== null;
$b_has = $b['ordre'] !== null;
if ( $a_has && $b_has ) return $a['ordre'] - $b['ordre'];
if ( $a_has ) return -1;
if ( $b_has ) return 1;
return strcmp( $a['name'], $b['name'] );
} );
}
unset( $group );
return array_values( $axes_map );
}