Restrict individual category combinations

While assigning multiple categories to a post, I need to restrict the category choices on a post depending on what other categories have already been assigned to the particular post.

For example, lets say we have categories A, B and C.
For any post, we want to restrict having both categories B and C. So posts may have the following assignments:

Read More
  • A, B, C
  • A, B
  • A, C

No post should have B and C.

How can I achieve this?

Related posts

2 comments

  1. Okay, I’ve had a couple minutes free time, so I wrote up a small plugin. 😉


    The following goes into a new plugin file tf-restrict-categories/tf-restrict-categories.php:

    The introduction

    <?php
    /**
     * Plugin Name: Restrict Categories
     * Description: Individually restrict category combinations.
     * License: MIT
     * License URI: http://opensource.org/licenses/MIT
     * Text Domain: tf-restrict-categories
     * Domain Path: /languages
     */
    
    
    if (! class_exists('TFRestrictCategories')) :
    
    
    /**
     * Main (and only) class.
     */
    class TFRestrictCategories {
    
        /**
         * Plugin instance.
         *
         * @type    object
         */
        protected static $instance = null;
    
    
        /**
         * basename() of global $pagenow.
         *
         * @type    string
         */
        protected static $page_base;
    
    
        /**
         * Plugin textdomain.
         *
         * @type    string
         */
        protected $textdomain = 'tf-restrict-categories';
    
    
        /**
         * Plugin option name.
         *
         * @type    string
         */
        protected $option_name = 'tf_restrict_categories';
    
    
        /**
         * Plugin settings page name.
         *
         * @type    string
         */
        protected $settings_page_name = 'tf-restrict-categories';
    
    
        /**
         * Plugin settings page.
         *
         * @type    string
         */
        protected $settings_page;
    

    The basics

        /**
         * Constructor. Registers activation routine.
         *
         * @hook    wp_loaded
         * @return  void
         */
        public function __construct() {
            register_activation_hook(__FILE__, array(__CLASS__, 'activation'));
        } // function __construct
    
    
        /**
         * Get plugin instance.
         *
         * @hook    wp_loaded
         * @return  object TFRestrictCategories
         */
        public static function get_instance() {
            if (null === self::$instance)
                self::$instance = new self;
    
            return self::$instance;
        } // function get_instance
    
    
        /**
         * Registers uninstall routine.
         *
         * @hook    activation
         * @return  void
         */
        public static function activation() {
            register_uninstall_hook(__FILE__, array(__CLASS__, 'uninstall'));
        } // function activation
    
    
        /**
         * Checks if the plugin has to be loaded.
         *
         * @return  boolean
         */
        public static function has_to_be_loaded() {
            global $pagenow;
    
            if (empty($pagenow))
                return false;
    
            self::$page_base = basename($pagenow, '.php');
    
            // Load plugin for all admin pages
            return is_admin();
        } // function has_to_be_loaded
    
    
        /**
         * Registers plugin actions and filters.
         *
         * @hook    wp_loaded
         * @return  void
         */
        public function init() {
            add_action('admin_menu', array($this, 'add_settings_page'));
            add_action('wp_insert_post', array($this, 'restrict_categories'));
    
            $pages = array(
                'edit',
                'post',
                'post-new',
            );
            if (in_array(self::$page_base, $pages))
                add_action('admin_print_scripts-posts_page_tf-restrict-categories', array($this, 'enqueue_scripts'));
    
            if ('plugins' === self::$page_base)
                add_filter('plugin_action_links_'.plugin_basename(__FILE__), array($this, 'add_settings_link'));
    
            if ('options' === self::$page_base)
                add_action('admin_init', array($this, 'register_setting'));
        } // function init
    
    
        /**
         * Wrapper for get_option().
         *
         * @param   string $key Option name.
         * @param   mixed $default Return value for missing key.
         * @return  mixed|$default Option value.
         */
        protected function get_option($key = null, $default = false) {
            static $option = null;
            if (null === $option) {
                $option = get_option($this->option_name, false);
                if (false === $option)
                    $option = array(
                    );
            }
    
            if (null === $key)
                return $option;
    
            if (! isset($option[$key]))
                return $default;
    
            return $option[$key];
        } // function get_option
    

    This is where the action is

        /**
         * Adds custom settings page to posts settings.
         *
         * @hook    admin_menu
         * @return  void
         */
        public function add_settings_page() {
            $this->settings_page = add_posts_page('Restrict Categories', 'Restrict Categories', 'manage_categories', $this->settings_page_name, array($this, 'print_settings_page'));
        } // function add_settings_page
    
    
        /**
         * Prints settings page.
         *
         * @see     add_settings_page()
         * @return  void
         */
        public function print_settings_page() {
            $this->load_textdomain();
            ?>
            <div class="wrap">
                <h2>Restrict Categories</h2>
                <div class="tool-box">
                    <form method="post" action="<?php echo admin_url('options.php'); ?>">
                        <?php
                        settings_fields($this->option_name);
                        $args = array(
                            'hide_empty' => 0,
                        );
                        if (count($categories = get_categories($args))) {
                            $option = $this->get_option($this->option_name, array());
                            ?>
                            <table id="tf-restrict-categories" class="widefat">
                                <thead>
                                    <tr>
                                        <th></th>
                                        <?php
                                        foreach ($categories as $category) {
                                            ?>
                                            <th><?php echo $category->name; ?></th>
                                            <?php
                                        }
                                        ?>
                                    </tr>
                                </thead>
                                <tbody>
                                    <?php
                                    $alternate = true;
                                    foreach ($categories as $category) {
                                        $class = ($alternate) ? ' class="alternate"' : '';
                                        $alternate = ! $alternate;
                                        ?>
                                        <tr<?php echo $class; ?>>
                                            <td id=""><?php echo $category->name; ?></td>
                                            <?php
                                            foreach ($categories as $second_category) {
                                                ?>
                                                <td id="restrict-category-<?php echo $category->term_id; ?>-<?php echo $second_category->term_id; ?>">
                                                    <?php
                                                    if ($category->term_id !== $second_category->term_id) {
                                                        $checked = (
                                                            isset($option[$category->term_id]) && isset($option[$category->term_id][$second_category->term_id])
                                                            || isset($option[$second_category->term_id]) && isset($option[$second_category->term_id][$category->term_id])
                                                        );
                                                        $checked = ($checked) ? ' checked="checked"' : '';
                                                        $disabled = (isset($option[$second_category->term_id]) && isset($option[$second_category->term_id][$category->term_id]));
                                                        $disabled = ($disabled) ? ' disabled="disabled"' : '';
                                                        ?>
                                                        <input type="checkbox" id="<?php echo $category->term_id; ?>-<?php echo $second_category->term_id; ?>" name="<?php echo $this->option_name; ?>[<?php echo $category->term_id; ?>][<?php echo $second_category->term_id; ?>]" value="1"<?php echo $checked.$disabled; ?> />
                                                        <?php
                                                    }
                                                    ?>
                                                </td>
                                                <?php
                                            }
                                            ?>
                                        </tr>
                                        <?php
                                    }
                                    ?>
                                </tbody>
                            </table>
                            <div class="submit">
                                <input type="submit" class="button-primary" value="<?php _e('Save Changes'); ?>" />
                            </div>
                            <?php
                        } else
                            _e("No categories found.", 'tf-restrict-categories');
                        ?>
                    </form>
                </div>
            </div>
            <?php
            $this->unload_textdomain();
        } // function print_settings_page
    
    
        /**
         * Restricts categories according to stored plugin settings.
         *
         * @hook    wp_insert_post
         * @param   int $id Post ID.
         * @return  void
         */
        public function restrict_categories($id) {
            $args = array(
                'fields' => 'ids',
                'orderby' => 'term_id'
            );
            if (
                count($option = $this->get_option($this->option_name, array()))
                && count($categories = wp_get_object_terms($id, 'category', $args))
            ) {
                foreach ($option as $master_key => $restrict)
                    foreach($restrict as $restrict_key => $v)
                        if (in_array($master_key, $categories))
                            foreach($categories as $key => $category)
                                if ($category === $restrict_key)
                                    unset($categories[$key]);
    
                wp_set_object_terms($id, $categories, 'category');
            }
        } // function restrict_categories
    
    
        /**
         * Adds a link to the settings to the plugin list.
         *
         * @hook    plugin_action_links_{$file}
         * @param   array $links Already existing links.
         * @return  array
         */
        public function add_settings_link($links) {
            $settings_link = array(
                '<a href="'.admin_url('edit.php?page='.$this->settings_page_name).'">'.__("Settings").'</a>'
            );
    
            return array_merge($settings_link, $links);
        } // function add_settings_link
    
    
        /**
         * Enqueues necessary script files.
         *
         * @hook    admin_print_scripts-posts_page_tf-restrict-categories
         * @return  void
         */
        public function enqueue_scripts() {
            $this->load_textdomain();
            wp_enqueue_script('tf-restrict-categories-js', plugin_dir_url(__FILE__).'js/tf-restrict-categories.js', array('jquery'), filemtime(plugin_dir_path(__FILE__).'js/tf-restrict-categories.js'), true);
            $this->unload_textdomain();
        } // function enqueue_scripts
    
    
        /**
         * Registers setting for custom options page.
         *
         * @hook    admin_init
         * @return  void
         */
        public function register_setting() {
            register_setting($this->option_name, $this->option_name, array($this, 'save_setting'));
        } // function register_setting
    
    
        /**
         * Prepares option values before they are saved.
         *
         * @param   array $data Original option values.
         * @return  array Sanitized option values.
         */
        public function save_setting($data) {
            $sanitized_data = $this->get_option();
            if (isset($data) && ! empty($data))
                $sanitized_data[$this->option_name] = $data;
            else
                unset($sanitized_data[$this->option_name]);
    
            return $sanitized_data;
        } // function save_setting
    

    The end

        /**
         * Loads plugin textdomain.
         *
         * @return  boolean
         */
        protected function load_textdomain() {
            return load_plugin_textdomain($this->textdomain, false, plugin_basename(dirname(__FILE__)).'/languages');
        } // function load_textdomain
    
    
        /**
         * Remove translations from memory.
         *
         * @return  void
         */
        protected function unload_textdomain() {
            unset($GLOBALS['l10n'][$this->textdomain]);
        } // function unload_textdomain
    
    
        /**
         * Deletes plugin data on uninstall.
         *
         * @hook    uninstall
         * @return  void
         */
        public static function uninstall() {
            delete_option(self::get_instance()->option_name);
        } // function uninstall
    
    } // class TFRestrictCategories
    
    
    if (TFRestrictCategories::has_to_be_loaded())
        add_action('wp_loaded', array(TFRestrictCategories::get_instance(), 'init'));
    
    
    endif; // if (! class_exists('TFRestrictCategories'))
    

    The following goes into a new plugin file tf-restrict-categories/js/tf-restrict-categories.js:

    The JavaScript

    jQuery(function($) {
    
        $('#tf-restrict-categories [type="checkbox"]').click(function() {
            var $this = $(this);
            if (! $this.is(':disabled')) {
                var n = this.id.split('-');
                $('#tf-restrict-categories [type="checkbox"][name$="\['+n[1]+'\]\['+n[0]+'\]"]')
                    .attr('disabled', $this.is(':checked'))
                    .attr('checked', $this.is(':checked'));
            }
        });
    
    });
    

    Copy&Paste the code into the two files, upload to your plugins folder, activate the plugin and find the new settings page.

    Happy restricting. 🙂


    Okay, okay, some words of explaining…

    The plugin works as follows. On the settings page, we define some category combination. Let’s say, we want to have the rule If ‘Cat A’ is present, do NOT allow ‘Cat C’. This can be achieved by checking the checkbox in the row Cat A and column Cat C. This means: the row category is the master category, and the column category will be restricted.
    Of course, you can have multiple combinations with Cat A (and multiple other categories).

    When saving/updating a post (or, to be more precise: when having saved/updated a post), the categories are checked against the stored plugin settings – and adapted.

    Anything else?

  2. There doesn’t appear to be any hook that you can leverage to alter the categories going in when wp_create_post or wp_update_post are called.

    However, you can always hook on the wp_insert_post action to update categories after a post is saved or updated:

    function update_post_categories( $postID, $post, $is_update ) {
       $categories = wp_get_object_terms( $postID, 'category' );
    
       // Figure out which categories are invalid,
       // Put only the valid categories in $new_categories...
    
       wp_set_object_terms( $postID, $new_categories, 'category', false );
    }
    add_action( 'wp_insert_post', 'update_post_categories', 10, 3 );
    

    What this snippet above does is fetch the categories that would be assigned to a post, leaves only the ones that should be assigned in $new_categories, and then over-writes the categories for that post.

    As you can see, I left out the logic to figure out which categories are valid, since you didn’t describe what that logic is.

Comments are closed.