Created an alternative cost filter, looking for second opinions.

Home Forums Calendar Products Filter Bar Created an alternative cost filter, looking for second opinions.

Viewing 6 posts - 1 through 6 (of 6 total)
  • Author
    Posts
  • #1164093
    Jay
    Participant

    Hey everyone,

    I’ve created a Boston tech events calendar, and the event prices ended up being a pretty large range. From meetups with $5 pizza costs to conferences with $5000+ registration fees. I wasn’t liking that my only options for the cost filter were a range slider that wasn’t really convenient for the distribution of the costs or a checklist of price-ranges that ended up being pretty arbitrary (and didn’t cover the ranges I actually wanted).

    So I added a new filter, following the process I’d seen on these forums (where the category filter was used as an example to add a custom taxonomy filter). Basically I copied the code for the Cost filter class, modified it, and added it to my functions.php with a line to initialize it:

    class BSG__Cost__Tribe__Events__Filterbar__Filters__Cost extends Tribe__Events__Filterbar__Filter {
    	const EXPLICITLY_FREE = 'set_to_0';
    	const IMPLICITLY_FREE = 'unset_or_0';
    
    	public $type = 'range';
    	public $free = self::IMPLICITLY_FREE;
    	private $min_cost = null;
    	private $max_cost = null;
    
    	protected function settings() {
    		parent::settings();
    		$this->free_logic();
    	}
    
    	protected function free_logic() {
    		$settings = Tribe__Events__Filterbar__View::instance()->get_filter_settings();
    		$this->free = isset( $settings[ $this->slug ]['free'] ) && self::EXPLICITLY_FREE === $settings[ $this->slug ]['free']
    			? self::EXPLICITLY_FREE : self::IMPLICITLY_FREE;
    	}
    
    	protected function get_submitted_value() {
    		if ( ! empty( $_REQUEST[ 'tribe_' . $this->slug ] ) ) {
    			$value = (array) $_REQUEST[ 'tribe_' . $this->slug ];
    
    			if ( isset( $value['min'] ) && isset( $value['max'] ) ) {
    				return array( $value );
    			} else {
    				foreach ( $value as &$v ) {
    					$range = explode( '-', $v );
    					if ( ! preg_match( '/[0-9]+\-[0-9]+/', $v ) ) {
    						continue;
    					}
    					$v = array( 'min' => $range[0], 'max' => $range[1] );
    				}
    				return $value;
    			}
    		}
    		return array();
    	}
    
    	public function get_admin_form() {
    		$title = $this->get_title_field();
    		$type = $this->get_type_field();
    		return $title.$type;
    	}
    
    	protected function get_type_field() {
    		$name = $this->get_admin_field_name( 'type' );
    		$type_field = sprintf( __( 'Type: %s %s', 'tribe-events-filter-view' ),
    			sprintf( '<label><input type="radio" name="%s" value="range" %s /> %s</label>',
    				$name,
    				checked( $this->type, 'range', false ),
    				__( 'Range Slider', 'tribe-events-filter-view' )
    			),
    			sprintf( '<label><input type="radio" name="%s" value="checkbox" %s /> %s</label>',
    				$name,
    				checked( $this->type, 'checkbox', false ),
    				__( 'Checkboxes', 'tribe-events-filter-view' )
    			)
    		);
    
    		$name = $this->get_admin_field_name( 'free' );
    		$cost_field = sprintf( __( 'Events are considered free when cost field is: %s %s', 'tribe-events-filter-view' ),
    			sprintf( '<label><input type="radio" name="%s" value="unset_or_0" %s /> %s</label>',
    				$name,
    				checked( $this->free, 'unset_or_0', false ),
    				sprintf(
    					__( '"%s" or empty or set to zero', 'tribe-events-filter-view' ),
    					'<strong><abbr title="' . __( 'Used as the identifier for free Events', 'tribe-events-filter-view' ) . '">' . $this->get_free_string() . '</abbr></strong>'
    				)
    			),
    			sprintf( '<label><input type="radio" name="%s" value="set_to_0" %s /> %s</label>',
    				$name,
    				checked( $this->free, 'set_to_0', false ),
    				__( 'Only when set to zero', 'tribe-events-filter-view' )
    			)
    		);
    
    		return '<div class="tribe_events_active_filter_type_options">' . $type_field . $cost_field . '</div>';
    	}
    
    	protected function get_free_string() {
    		return _x( 'Free', 'Used as the identifier for free Events', 'tribe-events-filter-view' );
    	}
    
    	protected function get_values() {
    		$this->set_min_and_max();
    
    		if ( $this->type == 'range' ) {
    			return array( 'min' => $this->min_cost, 'max' => $this->max_cost );
    		}
    
    		$cost_range = array();
    		if ( $this->has_non_numeric_costs() ) {
    			$cost_range['other'] = __( 'Other', 'tribe-events-filter-view' );
    		}
    
    		if ( $this->min_cost == 0 ) {
    			$cost_range['0-0'] = __( 'Free', 'tribe-events-filter-view' );
    		}
    		if ( $this->max_cost == $this->min_cost ) {
    			if ( $this->max_cost != 0 ) {
    				$cost_range[ $this->min_cost . '-' . $this->max_cost ] = $this->min_cost . '-' . $this->max_cost;
    			}
    		} else { //hard-coding my desired price ranges
    
    			$cost_range['1-49'] = 'Inexpensive (\$1 - \$49)';
    			$cost_range['50-249'] = 'Moderate (\$50 - \$249)';
    			$cost_range['250-' . $this->max_cost] = 'Professional (\$250+)';
    		}
    		$values = array();
    		foreach ( $cost_range as $key => $cost ) {
    			$values[] = array(
    				'name' => $cost,
    				'value' => $key,
    			);
    		}
    		return $values;
    	}
    
    	private function partition_range( $min, $max, $count ) {
    		$range_size = $max - $min + 1;
    		$partition_size = floor( $range_size / $count );
    		$partition_remainder = $range_size % $count;
    		$partitioned = array();
    		$mark = $min;
    		for ( $i = 0; $i < $count; $i++ ) {
    			$incr = ( $i < $partition_remainder ) ? $partition_size : $partition_size - 1;
    			$partitioned[ $i ] = array(
    				'min' => $mark,
    				'max' => $mark + $incr,
    			);
    			$mark += $incr + 1;
    		}
    		return $partitioned;
    	}
    
    	protected function is_selected( $option ) {
    		if ( preg_match( '/[0-9]*\-[0-9]*/', $option ) ) {
    			$option = explode( '-', $option );
    			$option = array(
    				'min' => $option[0],
    				'max' => $option[1],
    			);
    		} elseif ( in_array( 'other', $this->currentValue ) ) {
    			return true;
    		}
    
    		return in_array( (array) $option, $this->currentValue );
    	}
    
    	protected function setup_query_filters() {
    		if ( $this->currentValue ) {
    			$this->set_min_and_max();
    		}
    		parent::setup_query_filters();
    	}
    
    	protected function setup_join_clause() {
    		global $wpdb;
    		$this->joinClause = " LEFT JOIN {$wpdb->postmeta} AS cost_filter ON ({$wpdb->posts}.ID = cost_filter.post_id)";
    	}
    
    	protected function setup_where_clause() {
    		global $wpdb;
    		$clauses = array();
    
    		foreach ( $this->currentValue as $value ) {
    			$free_clause = '';
    			if ( isset( $value['min'] ) ) {
    				// Should we exclude events where a cost has not been provided?
    				$free_clause = $this->free_clause( $value['min'] );
    			}
    
    			if ( 'other' === $value ) {
    				$length_clause = null;
    				if ( self::IMPLICITLY_FREE === $this->free ) {
    					$length_clause = 'AND LENGTH( TRIM( cost_filter.meta_value ) ) > 0';
    				}
    
    				$clause = "
    					( cost_filter.meta_key = '_EventCost'
    					$length_clause
    					AND CAST( cost_filter.meta_value AS SIGNED ) = 0
    					AND cost_filter.meta_value != '0'
    				";
    
    				if ( self::EXPLICITLY_FREE !== $this->free ) {
    					$clause .= $wpdb->prepare( ' AND LOWER(cost_filter.meta_value) != %s', strtolower( $this->get_free_string() ) );
    				}
    
    				// Close the Conditional
    				$clause .= ')';
    
    				$clauses[] = $clause;
    			} elseif ( isset( $value['min'], $value['max'] ) && $value['min'] == 0 && $value['max'] == 0 ) {
    
    				$blank_clause = null;
    				if ( self::IMPLICITLY_FREE === $this->free ) {
    					$blank_clause = "
    						OR cost_filter.meta_value = ''
    						OR cost_filter.meta_value IS NULL
    						OR $free_clause
    					";
    				}
    
    				$clauses[] = "
    					(
    						cost_filter.meta_key = '_EventCost'
    						AND (
    							cost_filter.meta_value = '0'
    							$blank_clause
    						)
    					)
    				";
    			} else {
    				$clauses[] = $wpdb->prepare(
    					"(
    						cost_filter.meta_key = '_EventCost'
    						AND cost_filter.meta_value >= %d
    						AND cost_filter.meta_value IS NOT NULL
    						AND CAST(cost_filter.meta_value AS SIGNED) BETWEEN %d AND %d
    					) ",
    					$value['min'],
    					$value['min'],
    					$value['max']
    				);
    			}
    		}
    
    		$this->whereClause = ' AND (' . implode( ' OR ', $clauses ) . ') ';
    	}
    
    	protected function free_clause( $min ) {
    		global $wpdb;
    
    		if ( 0 !== (int) $min ) {
    			return 'LENGTH( TRIM( cost_filter.meta_value ) ) > 0 AND CAST( cost_filter.meta_value AS SIGNED ) > 0';
    		}
    
    		return $wpdb->prepare(
    			'(
    				LENGTH( TRIM( cost_filter.meta_value ) ) > 0
    				AND CAST( cost_filter.meta_value AS SIGNED ) = 0
    				AND LOWER( cost_filter.meta_value ) = %s
    			)',
    			strtolower( $this->get_free_string() )
    		);
    	}
    
    	private function set_min_and_max() {
    		if ( ! isset( $this->max_cost ) || ! isset( $this->min_cost ) ) {
    			$this->max_cost = tribe_get_maximum_cost();
    			$this->min_cost = tribe_has_uncosted_events() ? 0 : tribe_get_minimum_cost();
    		}
    	}
    
    	private function has_non_numeric_costs() {
    		$costs = Tribe__Events__Cost_Utils::instance()->get_all_costs();
    		foreach ( $costs as $index => $cost ) {
    			if ( is_numeric( $index ) ) {
    				unset( $costs[ $index ] );
    			}
    		}
    		return ! empty( $costs );
    	}
    }
    $cost_filter_alternative = new BSG__Cost__Tribe__Events__Filterbar__Filters__Cost('Price', 'price');
    

    The key bit is here, where I’ve removed Tribe’s chunking code and hard-coded my own ranges:

    $cost_range = array();
    		if ( $this->has_non_numeric_costs() ) {
    			$cost_range['other'] = __( 'Other', 'tribe-events-filter-view' );
    		}
    
    		if ( $this->min_cost == 0 ) {
    			$cost_range['0-0'] = __( 'Free', 'tribe-events-filter-view' );
    		}
    		if ( $this->max_cost == $this->min_cost ) {
    			if ( $this->max_cost != 0 ) {
    				$cost_range[ $this->min_cost . '-' . $this->max_cost ] = $this->min_cost . '-' . $this->max_cost;
    			}
    		} else { //hard-coding my desired price ranges
    
    			$cost_range['1-49'] = 'Inexpensive (\$1 - \$49)';
    			$cost_range['50-249'] = 'Moderate (\$50 - \$249)';
    			$cost_range['250-' . $this->max_cost] = 'Professional (\$250+)';
    		}
    		$values = array();
    		foreach ( $cost_range as $key => $cost ) {
    			$values[] = array(
    				'name' => $cost,
    				'value' => $key,
    			);
    		}
    		return $values;
    

    Now, this works. I have the filter available within my settings, I’ve added it, and it filters correctly. The only thing that’s weird about it is the parameter that gets added to the URL has some extra percent-encoding in it: ?tribe_paged=1&tribe_event_display=list&tribe_price%5B%5D=1-49 – not a big deal, but would be great if I could fix it.

    What I’m really asking for is anyone with more development experience than me (I’m a non-CS-background, copy-and-paste backend developer) to tell me if I’m doing anything wrong with this implementation? I feel like I’m repeating a lot of code, most of which I don’t know what it does, and even though it seems to be working, stuff like the URL having unexplained extra characters in it makes me wonder if there are other unintended consequences I’m missing. Have I missed any steps anywhere? Is there a better way to do what I’m doing?

    #1164103
    George
    Participant

    Hey Jonathan,

    Thanks for reaching out.

    This is an awesome customization, but we are unfortunately not able to help with writing custom code or with providing analysis/insight on your own custom code. Please read our support policy to learn more about this → https://theeventscalendar.com/knowledgebase/what-support-is-provided-for-license-holders/

    I’m sorry to disappoint.

    While I cannot help with the code that you have posted, one thing I can recommend is to just skip the “Cost” filter altogether and make the categories of events you’ve made (Free, Inexpensive, Moderate, Professional) actual event categories.

    Then, you can manually put events in these price ranges into those categories, and allow folks to filter by the category that way.

    This isn’t ideal, but may be much simpler than trying to redo the cost filter and be less problematic.

    Sincerely,
    George

    PS

    Despite the policies I mentioned above, I did take a gander at your code. It’s unfortunately true that you have to pretty much copy the whole class just to modify that one method, because the values for that method are not currently filterable at this time.

    So, despite the additional URL characters and your concerns, the back-end code changes you’ve made are actually pretty in line with what would be required to make these sorts of front-end changes you want here. If it works and you don’t run into issues or bugs, then I can’t honestly think of many ways to improve upon the current configuration you’re using.

    If you are really keen on getting a Pro to help you implement this customization as perfectly as possible, I would recommend checking out the options on our Customizations page here → http://theeventscalendar.com/customizations

    #1164121
    Jay
    Participant

    Thanks George! Appreciate the pointer to your recommended freelancers and may consider reaching out to one of them for a round-up review of customizations I’ve made.

    And really appreciate you taking a look and affirming my approach. =)

    Is it okay if I leave this thread open in case any community members want to chime in?

    #1164311
    George
    Participant

    Hey Jay,

    Sounds good. When it comes to leaving the thread open, I would recommend instead leaving a website contact page any interested folks can contact you through, or your twitter profile, or something.

    We try to keep threads closed to make the forums more manageable—it makes a big difference.

    Cheers,
    George

    #1164564
    Jay
    Participant

    Sounds good. I’m fine with leaving my email here in this format: jay [@at@] jayneely [.dot.] com — if anyone wants to reach out about customizing the cost filter, please feel free to drop me a line.

    #1164584
    George
    Participant

    Thanks Jay. Best of luck with your project!

    Cheers,
    George

Viewing 6 posts - 1 through 6 (of 6 total)
  • The topic ‘Created an alternative cost filter, looking for second opinions.’ is closed to new replies.