Plugin Directory

Changeset 3386046


Ignore:
Timestamp:
10/28/2025 05:39:52 PM (5 months ago)
Author:
grocerslist
Message:

10/28/2025 - 1.9.0 - Feature: ability to gate posts by category, ability to apply gating defaults to categories that get inherited by posts

Location:
grocerslist
Files:
205 added
5 edited

Legend:

Unmodified
Added
Removed
  • grocerslist/trunk/README.md

    r3382109 r3386046  
    44Author: Grocers List, Engineering
    55Tested up to: 6.8
    6 Stable tag: 1.8.0
     6Stable tag: 1.9.0
    77Requires PHP: 7.0
    88License: GPLv3
     
    6464#### - 10/21/2025 - 1.8.0 - Add noopener/noreferrer to external links, dependency updates
    6565
     66#### - 10/28/2025 - 1.9.0 - Feature: ability to gate posts by category, ability to apply gating defaults to categories that get inherited by posts
     67
    6668### Resources:
    6769
  • grocerslist/trunk/grocerslist.php

    r3382109 r3386046  
    77Requires PHP: 7.0
    88Tested up to: 6.8
    9 Version: 1.8.0
    10 Stable tag: 1.8.0
     9Version: 1.9.0
     10Stable tag: 1.9.0
    1111Author: Grocers List Engineering
    1212License: GPLv3
     
    1818if (!defined('ABSPATH')) exit;
    1919
    20 define('GROCERS_LIST_VERSION', '1.8.0');
     20define('GROCERS_LIST_VERSION', '1.9.0');
    2121define('GROCERS_LIST_PLUGIN_FILE', __FILE__);
    2222define('GROCERS_LIST_PLUGIN_DIR', __DIR__);
  • grocerslist/trunk/includes/Admin/PostGating.php

    r3370641 r3386046  
    99    private const META_POST_GATED = 'grocers_list_post_gated';
    1010    private const META_RECIPE_CARD_GATED = 'grocers_list_recipe_card_gated';
     11    private const META_CATEGORY_GATING_TYPE = 'grocers_list_category_gating_type';
     12    private const META_NO_GATING = 'grocers_list_no_gating';
    1113
    1214    public function register(): void
     
    1517        add_action('save_post', [$this, 'savePostMeta']);
    1618        add_action('init', [$this, 'registerPostMeta']);
     19        add_action('category_add_form_fields', [$this, 'addCategoryFields']);
     20        add_action('category_edit_form_fields', [$this, 'editCategoryFields'], 10, 2);
     21        add_action('created_category', [$this, 'saveCategoryFields']);
     22        add_action('edited_category', [$this, 'saveCategoryFields']);
    1723    }
    1824
     
    2935
    3036        register_post_meta('post', self::META_RECIPE_CARD_GATED, [
     37            'show_in_rest' => true,
     38            'single' => true,
     39            'type' => 'string',
     40            'auth_callback' => function () {
     41                return current_user_can('edit_posts');
     42            }
     43        ]);
     44
     45        register_post_meta('post', self::META_NO_GATING, [
    3146            'show_in_rest' => true,
    3247            'single' => true,
     
    6277        $post_gated = get_post_meta($post->ID, self::META_POST_GATED, true);
    6378        $recipe_card_gated = get_post_meta($post->ID, self::META_RECIPE_CARD_GATED, true);
     79        $no_gating = get_post_meta($post->ID, self::META_NO_GATING, true);
    6480       
    6581        // Determine current gating option
    66         $current_option = 'none';
    67         if ($post_gated === '1') {
     82        $current_option = 'by_category';
     83        if ($no_gating === '1') {
     84            $current_option = 'no_gating';
     85        } elseif ($post_gated === '1') {
    6886            $current_option = 'post';
    6987        } elseif ($recipe_card_gated === '1') {
    7088            $current_option = 'recipe';
    7189        }
     90
     91        // Check if category-level gating is set
     92        $category_gating_info = '';
     93        $inherited_gating_text = '';
     94        $has_category_gating = false;
     95        $categories = get_the_category($post->ID);
     96       
     97        if (!empty($categories)) {
     98            foreach ($categories as $category) {
     99                $category_gating = get_term_meta($category->term_id, self::META_CATEGORY_GATING_TYPE, true);
     100                if (!empty($category_gating) && $category_gating !== 'none') {
     101                    $has_category_gating = true;
     102                    $gating_label = $category_gating === 'post' ? 'entire post' : 'recipe cards only';
     103                    $category_gating_info .= sprintf(
     104                        '<li>Category "<strong>%s</strong>" gates %s</li>',
     105                        esc_html($category->name),
     106                        esc_html($gating_label)
     107                    );
     108                   
     109                    // Set the inherited text (use first category with gating)
     110                    if (empty($inherited_gating_text)) {
     111                        $inherited_gating_text = sprintf(
     112                            ' <span style="color: #2271b1; font-size: 12px;">(will gate %s from "%s")</span>',
     113                            esc_html($gating_label),
     114                            esc_html($category->name)
     115                        );
     116                    }
     117                }
     118            }
     119        }
     120       
     121        // If no category gating, show that too
     122        if (!$has_category_gating) {
     123            $inherited_gating_text = ' <span style="color: #666; font-size: 12px;">(no gating from categories)</span>';
     124        }
    72125        ?>
    73126        <div class="grocers-list-gating-options">
    74             <p><strong>Gating Options:</strong></p>
     127            <p><strong>Post-Level Gating Options:</strong></p>
    75128            <p>
    76129                <label>
    77                     <input type="radio" name="grocers_list_gating_option" value="none" <?php checked($current_option, 'none'); ?> />
    78                     No gating
     130                    <input type="radio" name="grocers_list_gating_option" value="by_category" <?php checked($current_option, 'by_category'); ?> />
     131                    Inherit from category (default)<?php echo $inherited_gating_text; ?>
     132                </label>
     133            </p>
     134            <p>
     135                <label>
     136                    <input type="radio" name="grocers_list_gating_option" value="no_gating" <?php checked($current_option, 'no_gating'); ?> />
     137                    Do not gate
    79138                </label>
    80139            </p>
     
    91150                </label>
    92151            </p>
    93            
     152
     153            <?php if (!empty($category_gating_info)) : ?>
     154                <hr style="margin: 15px 0;">
     155                <p><strong>Category-Level Gating:</strong></p>
     156                <ul style="margin-left: 20px; font-size: 12px; color: #666;">
     157                    <?php echo $category_gating_info; ?>
     158                </ul>
     159                <p style="font-size: 12px; color: #666;">
     160                    <em>Note: Post-level settings override category settings.</em>
     161                </p>
     162            <?php endif; ?>
    94163        </div>
    95164        <?php
     
    114183        }
    115184
    116         $gating_option = isset($_POST['grocers_list_gating_option']) ? sanitize_text_field($_POST['grocers_list_gating_option']) : 'none';
     185        $gating_option = isset($_POST['grocers_list_gating_option']) ? sanitize_text_field($_POST['grocers_list_gating_option']) : 'by_category';
    117186       
    118187        // Set meta values based on radio selection
    119188        switch ($gating_option) {
     189            case 'no_gating':
     190                $post_gated = '0';
     191                $recipe_card_gated = '0';
     192                $no_gating = '1';
     193                break;
    120194            case 'post':
    121195                $post_gated = '1';
    122196                $recipe_card_gated = '0';
     197                $no_gating = '0';
    123198                break;
    124199            case 'recipe':
    125200                $post_gated = '0';
    126201                $recipe_card_gated = '1';
     202                $no_gating = '0';
    127203                break;
    128             case 'none':
     204            case 'by_category':
    129205            default:
    130206                $post_gated = '0';
    131207                $recipe_card_gated = '0';
     208                $no_gating = '0';
    132209                break;
    133210        }
     
    135212        update_post_meta($post_id, self::META_POST_GATED, $post_gated);
    136213        update_post_meta($post_id, self::META_RECIPE_CARD_GATED, $recipe_card_gated);
     214        update_post_meta($post_id, self::META_NO_GATING, $no_gating);
    137215    }
    138216
     
    146224        return get_post_meta($post_id, self::META_RECIPE_CARD_GATED, true) === '1';
    147225    }
     226
     227    /**
     228     * Add gating fields to category creation form
     229     *
     230     * @return void
     231     */
     232    public function addCategoryFields(): void
     233    {
     234        // If external JS URL is not set, don't show the fields
     235        $externalJsUrl = Config::getExternalJsUrl();
     236        if (empty($externalJsUrl)) {
     237            return;
     238        }
     239        ?>
     240        <div class="form-field">
     241            <label for="grocers_list_category_gating_type">
     242                <?php esc_html_e('Grocers List Membership Gating', 'grocers-list'); ?>
     243            </label>
     244            <select name="grocers_list_category_gating_type" id="grocers_list_category_gating_type">
     245                <option value="none"><?php esc_html_e('Do not gate (default)', 'grocers-list'); ?></option>
     246                <option value="post"><?php esc_html_e('Gate entire post', 'grocers-list'); ?></option>
     247                <option value="recipe"><?php esc_html_e('Gate recipe cards only', 'grocers-list'); ?></option>
     248            </select>
     249            <p class="description">
     250                <?php esc_html_e('Apply gating to all posts in this category. Individual post settings will override this.', 'grocers-list'); ?>
     251            </p>
     252        </div>
     253        <?php
     254    }
     255
     256    /**
     257     * Add gating fields to category edit form
     258     *
     259     * @param \WP_Term $term Current taxonomy term object
     260     * @return void
     261     */
     262    public function editCategoryFields($term): void
     263    {
     264        // If external JS URL is not set, don't show the fields
     265        $externalJsUrl = Config::getExternalJsUrl();
     266        if (empty($externalJsUrl)) {
     267            return;
     268        }
     269
     270        $gating_type = get_term_meta($term->term_id, self::META_CATEGORY_GATING_TYPE, true);
     271        if (empty($gating_type)) {
     272            $gating_type = 'none';
     273        }
     274        ?>
     275        <tr class="form-field">
     276            <th scope="row">
     277                <label for="grocers_list_category_gating_type">
     278                    <?php esc_html_e('Grocers List Membership Gating', 'grocers-list'); ?>
     279                </label>
     280            </th>
     281            <td>
     282                <select name="grocers_list_category_gating_type" id="grocers_list_category_gating_type">
     283                    <option value="none" <?php selected($gating_type, 'none'); ?>>
     284                        <?php esc_html_e('No gating (default)', 'grocers-list'); ?>
     285                    </option>
     286                    <option value="post" <?php selected($gating_type, 'post'); ?>>
     287                        <?php esc_html_e('Gate entire post', 'grocers-list'); ?>
     288                    </option>
     289                    <option value="recipe" <?php selected($gating_type, 'recipe'); ?>>
     290                        <?php esc_html_e('Gate recipe cards only', 'grocers-list'); ?>
     291                    </option>
     292                </select>
     293                <p class="description">
     294                    <?php esc_html_e('Apply gating to all posts in this category. Individual post settings will override this.', 'grocers-list'); ?>
     295                </p>
     296            </td>
     297        </tr>
     298        <?php
     299    }
     300
     301    /**
     302     * Save category gating fields
     303     *
     304     * @param int $term_id Term ID
     305     * @return void
     306     */
     307    public function saveCategoryFields(int $term_id): void
     308    {
     309        if (!isset($_POST['grocers_list_category_gating_type'])) {
     310            return;
     311        }
     312
     313        $gating_type = sanitize_text_field($_POST['grocers_list_category_gating_type']);
     314       
     315        // Validate the gating type
     316        if (!in_array($gating_type, ['none', 'post', 'recipe'], true)) {
     317            $gating_type = 'none';
     318        }
     319
     320        update_term_meta($term_id, self::META_CATEGORY_GATING_TYPE, $gating_type);
     321    }
     322
     323    /**
     324     * Get the effective gating type for a post
     325     * Checks post-level settings first, then falls back to category-level settings
     326     *
     327     * @param int $post_id Post ID
     328     * @return array{post: bool, recipe: bool} Array with 'post' and 'recipe' gating status
     329     */
     330    public static function getEffectiveGating(int $post_id): array
     331    {
     332        // Check if gating is explicitly disabled at post level
     333        $no_gating = get_post_meta($post_id, self::META_NO_GATING, true);
     334        if ($no_gating === '1') {
     335            return ['post' => false, 'recipe' => false];
     336        }
     337
     338        // Check post-level settings
     339        $post_gated = get_post_meta($post_id, self::META_POST_GATED, true);
     340        $recipe_card_gated = get_post_meta($post_id, self::META_RECIPE_CARD_GATED, true);
     341
     342        // If post has explicit gating set, use that
     343        if ($post_gated === '1') {
     344            return ['post' => true, 'recipe' => false];
     345        }
     346        if ($recipe_card_gated === '1') {
     347            return ['post' => false, 'recipe' => true];
     348        }
     349
     350        // Otherwise, check category-level settings
     351        $categories = get_the_category($post_id);
     352        if (empty($categories)) {
     353            return ['post' => false, 'recipe' => false];
     354        }
     355        // Initialize the default post gating to false
     356        $resolved_post_gating = ['post' => false, 'recipe' => false];
     357        // Check each category for gating settings
     358        // If any category has gating enabled, apply it
     359        foreach ($categories as $category) {
     360            $category_gating = get_term_meta($category->term_id, self::META_CATEGORY_GATING_TYPE, true);
     361            // If the category has post gating enabled, return the post gating
     362            if ($category_gating === 'post') {
     363                return ['post' => true, 'recipe' => false];
     364            }
     365
     366            if ($category_gating === 'recipe') {
     367                $resolved_post_gating = ['post' => false, 'recipe' => true];
     368            }
     369        }
     370        // Return the resolved post gating, honoring recipe card category settings
     371        return $resolved_post_gating;
     372    }
    148373}
  • grocerslist/trunk/includes/Frontend/ClientScripts.php

    r3379735 r3386046  
    33namespace GrocersList\Frontend;
    44
     5use GrocersList\Admin\PostGating;
    56use GrocersList\Service\CreatorSettingsFetcher;
    67use GrocersList\Support\Config;
     
    8081            $postId = get_the_ID();
    8182
     83            // Use effective gating which checks both post-level and category-level settings
     84            $effectiveGating = PostGating::getEffectiveGating($postId);
     85
    8286            $window_grocersList['postId'] = get_the_ID();
    8387            $window_grocersList['postGatingConfig'] = [
    84                 'postGated' => get_post_meta($postId, 'grocers_list_post_gated', true) === '1',
    85                 'recipeCardGated' => get_post_meta($postId, 'grocers_list_recipe_card_gated', true) === '1',
     88                'postGated' => $effectiveGating['post'],
     89                'recipeCardGated' => $effectiveGating['recipe'],
    8690            ];
    8791        }
  • grocerslist/trunk/vendor/composer/installed.php

    r3382109 r3386046  
    22    'root' => array(
    33        'name' => '__root__',
    4         'pretty_version' => '1.8.0.x-dev',
    5         'version' => '1.8.0.9999999-dev',
    6         'reference' => 'a1f5a4d1be9225dd7fd194aebded1e77c697c3ec',
     4        'pretty_version' => 'dev-master',
     5        'version' => 'dev-master',
     6        'reference' => 'bc6322799543d31464f03219df5a207404cc7251',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
     
    1212    'versions' => array(
    1313        '__root__' => array(
    14             'pretty_version' => '1.8.0.x-dev',
    15             'version' => '1.8.0.9999999-dev',
    16             'reference' => 'a1f5a4d1be9225dd7fd194aebded1e77c697c3ec',
     14            'pretty_version' => 'dev-master',
     15            'version' => 'dev-master',
     16            'reference' => 'bc6322799543d31464f03219df5a207404cc7251',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.