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 ) ?: '—' ); }, 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 ? '✓' : '—'; }, 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 ); }