Forum Replies Created

Viewing 15 replies - 16 through 30 (of 77 total)
  • Thread Starter jgoldbloom

    (@jgoldbloom)

    @asgaros and anyone interested:

    Below is how I implemented in my child theme functions.php by writing my own forum-widget (for now), adding a custom function to render it via shortcode with instance argument support for shortcode type (topics vs. post and total (i.e. 10) like your widget. To render it I simply added shortcode to call my widget via a custom text widget in my theme sidebar setup, easy as that and no embedded PHP in the no cache AJAX widget which would be a huge security hole. See comments in my code for shortcode syntax.

    Pick out what you need, hope you streamline and improve as my method overall is for my own custom widgets and others, not just Asgaros while you were away:

    1. Install https://www.ads-software.com/plugins/no-cache-ajax-widgets/
    2. In functions.php:

    /*
    
    Render widget via shortcode.
    Shortcode examples for post/page/sidebars widgets/No Cache AJAX Widgets (plugin), etc.:
    [cdsnWidget widget_name="cdsn_forum_activity_widget" instance="type=topics&total=10"] 
    [cdsnWidget widget_name="cdsn_forum_activity_widget" instance="type=posts&total=10"] 
    */
    
    function cdsnWidgetShortCode($atts) {
        
        global $wp_widget_factory;
        
        extract(shortcode_atts(array(
            'widget_name' => FALSE,
            'instance' => FALSE,
        ), $atts));
        
        $instance = str_ireplace("&", '&' ,$instance);
        
        $widget_name = wp_specialchars($widget_name);
        
        if (!is_a($wp_widget_factory->widgets[$widget_name], 'WP_Widget')):
            $wp_class = 'WP_Widget_'.ucwords(strtolower($class));
            
            if (!is_a($wp_widget_factory->widgets[$wp_class], 'WP_Widget')):
                return '<p>'.sprintf(__("%s: Widget class not found. Make sure this widget exists and the class name is correct"),'<strong>'.$class.'</strong>').'</p>';
            else:
                $class = $wp_class;
            endif;
        endif;
        
        ob_start();
        the_widget($widget_name, $instance, array('widget_id'=>'arbitrary-instance-'.$id,
            'before_widget' => '',
            'after_widget' => '',
            'before_title' => '',
            'after_title' => '',
        ));
        $output = ob_get_contents();
        ob_end_clean();
        return $output;
        
    }
    
    /*
    	
    	=======================================
    	Widget class cdsn_forum_activity_widget
    	=======================================
    	
    	This class creates a widget showing most recent Asgaros forum activity due to Ajax issues with native Asgaros widget.
    	Options include widget title and total topics used to limit the SQL query.
    	Load thru Ajax Widget via [cdsnWidget widget_name="cdsn_forum_activity_widget" instance="type=x&total=y"] 
    	where "x" is type (either topics or posts) and "y" is query limit, i.e 10.
    	
    */
    
    class cdsn_forum_activity_widget extends WP_Widget {
    
    	function __construct() {
    	
    		parent::__construct(
    		
    		// Base ID  - this is the widget name in WP
    		'cdsn_forum_activity_widget', 
    		
    		// Widget name will appear in UI
    		__('CDSN Forum Recent Activity (Asgaros)', 'cdsn_forum_activity_widget_domain'), 
    		
    		// Widget description
    		array( 'description' => __( 'Load thru Ajax Widget via [cdsnWidget widget_name="cdsn_forum_activity_widget" 							instance="type=x&total=y"] where "x" is type (either topics or posts) and "y" is query limit, i.e 10', 			'cdsn_forum_activity_widget_domain' ), ) 
    		);
    	}
    	
    	// Creating widget front-end and establish widget display options
    	// This is where the action happens
    	public function widget( $args, $instance ) {
    	
    		$title = apply_filters( 'widget_title', $instance['title'] );
    		$limit = intval($instance['total']);
    		$type = $instance['type'];
    		// before and after widget arguments are defined by themes
    		echo $args['before_widget'];
    		if ( ! empty( $title ) ) {echo $args['before_title'] . $title . $args['after_title'];}
    		
    		/*
    		---------------------------
    		Code to perform widget task
    		---------------------------
    		*/
    		
    		global $wpdb;
    		
    		$excludeForumIDs="36";
    		
    	    // Query for topic data based on instance type
    	    if ($type=='topics') {
    	 		
    	 		$query="        
    			SELECT p1.text, p1.id, p1.date as postdate, p1.parent_id, p1.author_id, 
    			t.name, u.display_name, um.meta_value as avatar,
    			(SELECT COUNT(id) FROM wp_forum_posts WHERE parent_id=p1.parent_id) AS post_counter FROM wp_forum_posts AS p1 
    			LEFT JOIN wp_forum_posts AS p2 ON (p1.parent_id = p2.parent_id AND p1.id > p2.id) 
    			LEFT JOIN wp_forum_topics AS t ON (t.id = p1.parent_id) 
    			LEFT JOIN wp_forum_forums AS f ON (f.id = t.parent_id)
    			LEFT JOIN wp_users AS u ON (u.id = p1.author_id)
    			LEFT JOIN wp_usermeta AS um ON (um.user_id = p1.author_id and meta_key = 'profile_photo')			 
    			WHERE p2.id IS NULL AND f.parent_id NOT IN ($excludeForumIDs) ORDER BY t.id DESC LIMIT $limit";
    		
    		}
    		// Query for posts data based on instance type
    		else {
    		
    	 		$query="        
    			SELECT p1.text, p1.id, p1.date as postdate, p1.parent_id, p1.author_id, 
    			t.name, u.display_name, um.meta_value as avatar,
    			(SELECT COUNT(id) FROM wp_forum_posts WHERE parent_id=p1.parent_id) AS post_counter FROM wp_forum_posts AS p1 
    			LEFT JOIN wp_forum_posts AS p2 ON (p1.parent_id = p2.parent_id AND p1.id > p2.id) 
    			LEFT JOIN wp_forum_topics AS t ON (t.id = p1.parent_id) 
    			LEFT JOIN wp_forum_forums AS f ON (f.id = t.parent_id)
    			LEFT JOIN wp_users AS u ON (u.id = p1.author_id)
    			LEFT JOIN wp_usermeta AS um ON (um.user_id = p1.author_id and meta_key = 'profile_photo')			 
    			WHERE p2.id IS NULL AND f.parent_id NOT IN ($excludeForumIDs) ORDER BY p1.id DESC LIMIT $limit";
    		
    		}
    		
    		// Execute our query
    		$posts = $wpdb->get_results($query);
    		$content='';
    
    		foreach ($posts as $post) {
    			
    			 // Prepare our topic link as this info is not stored in the database  
    		     $url=get_site_url();
    		     $pageNumber = ceil($post->post_counter / 25); // mtached to # of topics per page in Asgaros
     
    		     $link="{$url}/cdsn-forum/?view=thread&id={$post->parent_id}=&part=$pageNumber#postid-{$post->id}";
    		     
    		     // Prepare our avatar handled by Ultimate Member plugin
    		     $avatar_info=new SplFileInfo($post->avatar);
    		     $avatar_ext=$avatar_info->getExtension();
    		     $avatar="{$url}/wp-content/uploads/ultimatemember/{$post->author_id}/profile_photo-40.{$avatar_ext}";
    		     unset ($avatar_info);
    		     
    		     // Ensure we have a default author name if missing
    		     $post->display_name=(!empty($post->display_name)) ? $post->display_name : 'CDSN Member';
    		     			
    			 // Prepare our content
    			 $name=$this->cdsnTruncate(strip_tags($this->cdsnSanitize($post->name)),33);
    			 $link=$this->cdsnSanitize($link);
    			 $author=$this->cdsnSanitize($post->display_name);
    			 date_default_timezone_set('America/New_York'); 
    			 $date=$this->cdsnTimeAgo($post->postdate);
    			 $postCounter=$post->post_counter-1;
    			 if ($postCounter>1) {
    				 $postCounter=", $postCounter replies";
    			 }
    			 elseif ($postCounter==1) {
    				 $postCounter=", 1 reply";
    			 }
    			 else {
    				 $postCounter='';
    			 }
    		
    			 // Pepare debug info (HTML)
    			 $debug="query=$query<br><br>type=$type<br>name=$name<br>link=$link<br>
    			 date=$date<br>author=$author<br>avatar=$avatar<br>";
    			 
    			 // Pepare content (HTML)
    			 $content.="
    			 
    			 <div class='asgarosforum-widget'>	 
    			 	<div class='widget-element cdsnWidgetElement'>	 	
    			 		<div class='widget-avatar cdsnWidgetAvatar'>
    			 			<img src='$avatar' alt='Author profile pic...' />
    			 		</div>
    			 		<div class='widget-content'>
    			 			<span class='post-link'><a href='$link' title='$lnk...'>$name</a></span>
    			 			<span class='post-author'>by <span class='cdsnWidgetAuthor'>$author</span></span>
    			 			<span class='post-date cdsnWidgetDate'>$date$postCounter</span>
    			 		</div>
    			 	</div>
    			 </div>
    			 
    			 ";
    		
    		}
    		
    		// Display prepared content
    		// echo __( $debug.$content, 'cdsn_forum_activity_widget_domain' );
    		echo __( $content, 'cdsn_forum_activity_widget_domain' );
    		echo $args['after_widget'];
    	
    	}
    	
    			
    	// Widget Backend 
    	public function form( $instance ) {
    	
    		// Title 
    		if ( isset( $instance[ 'title' ] ) && !empty($instance[ 'title' ]) ) {$title = $instance[ 'title' ];}
    		else {$title = __( 'Recent Forum Activity', 'cdsn_forum_activity_widget_domain' );}
    		
    		// Total topics
    		if ( isset( $instance[ 'total' ] ) && !empty($instance[ 'total' ]) ) {$total = $instance[ 'total' ];}
    		else {$total = __( '10', 'cdsn_forum_activity_widget_domain' );}
    		
    		// Type
    		if ( isset( $instance[ 'type' ] ) && !empty($instance[ 'type' ]) ) {
    			
    			$type = $instance[ 'type' ];
    			$checkedTopics = ($instance[ 'type' ]=='topics') ? "checked" : '';
    			$checkedPosts = ($instance[ 'type' ]=='posts') ? "checked" : '';
    	
    			}
    		else {
    			
    			$type = __( 'topics', 'cdsn_forum_activity_widget_domain' );
    			$checkedTopics='checked';
    			$checkedPosts='';
    			
    		}
    				
    		// Widget admin form - widget options
    		?>
    			
    		<p>
    		<label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label> 
    		<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" />
    		</p>
    
    		<p>
    		<label for="<?php echo $this->get_field_id( 'type' ); ?>"><?php _e( 'Type:' ); ?></label> 
    		<input class="radio" id="<?php echo $this->get_field_id( 'type' ); ?>" name="<?php echo $this->get_field_name( 'type' ); ?>" type="radio" value="topics" <?php echo $checkedTopics; ?> />Topics
    		<input class="radio" id="<?php echo $this->get_field_id( 'type' ); ?>" name="<?php echo $this->get_field_name( 'type' ); ?>" type="radio" value="posts" <?php echo $checkedPosts; ?> />Posts
    		</p>
    
    		<p>
    		<label for="<?php echo $this->get_field_id( 'total' ); ?>"><?php _e( 'Total Topics:' ); ?></label> 
    		<input class="tiny-text" id="<?php echo $this->get_field_id( 'total' ); ?>" name="<?php echo $this->get_field_name( 'total' ); ?>" type="number" step="1" min="1" value="<?php echo esc_attr( $total ); ?>" />
    		</p>
    	
    		<?php 
    			
    	}
    		
    	// Updating widget replacing old instances with new
    	public function update( $new_instance, $old_instance ) {
    	
    		$instance = array();
    		$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
    		$instance['total'] = ( ! empty( $new_instance['total'] ) ) ? $new_instance['total'] : '10';
    		$instance['type'] = ( ! empty( $new_instance['type'] ) ) ? $new_instance['type'] : 'topics';
    		return $instance;
    	
    	}
    	
    	/* CUSTOM CDSN FUNCTIONS BEGIN */
    	
    	// Sanitize query data to ensure properly escaped and newlines converted to HTML line breaks 
    	private function cdsnSanitize($str) {
    		return nl2br(stripslashes($str),false);
    	}
    	
    	// Truncate a string to configured size/nearest word
    	private function cdsnTruncate($text, $length) {
    	   $length = abs((int)$length);
    	   if(strlen($text) > $length) {
    	      $text = preg_replace("/^(.{1,$length})(\s.*|$)/s", '\\1...', $text);
    	   }
    	   return($text);
    	}
    
    	// Date to "time ago" in Facebook style human readable format
    	private function cdsnTimeAgo($timestamp)  {  
    	      
    	      $time_ago = strtotime($timestamp);  
    	      $current_time = time();  
    	      $time_difference = $current_time - $time_ago;  
    	      $seconds = $time_difference;  
    	      $minutes      = round($seconds / 60 );           // value 60 is seconds  
    	      $hours           = round($seconds / 3600);       // value 3600 is 60 minutes * 60 sec  
    	      $days          = round($seconds / 86400);        // 86400 = 24 * 60 * 60;  
    	      $weeks          = round($seconds / 604800);      // 7*24*60*60;  
    	      $months          = round($seconds / 2629440);    // ((365+365+365+365+366)/5/12)*24*60*60  
    	      $years          = round($seconds / 31553280);    // (365+365+365+365+366)/5 * 24 * 60 * 60  
    	      
    	      if($seconds <= 60)  
    	      {  
    	     return "Just now";  
    	   }  
    	      else if($minutes <=60)  
    	      {  
    	     if($minutes==1)  
    	           {  
    	       return "1 minute ago";  
    	     }  
    	     else  
    	           {  
    	       return "$minutes minutes ago";  
    	     }  
    	   }  
    	      else if($hours <=24)  
    	      {  
    	     if($hours==1)  
    	           {  
    	       return "1 hour ago";  
    	     }  
    	           else  
    	           {  
    	       return "$hours hrs ago";  
    	     }  
    	   }  
    	      else if($days <= 7)  
    	      {  
    	     if($days==1)  
    	           {  
    	       return "Yesterday";  
    	     }  
    	           else  
    	           {  
    	       return "$days days ago";  
    	     }  
    	   }  
    	      else if($weeks <= 4.3) //4.3 == 52/12  
    	      {  
    	     if($weeks==1)  
    	           {  
    	       return "1 week ago";  
    	     }  
    	           else  
    	           {  
    	       return "$weeks weeks ago";  
    	     }  
    	   }  
    	       else if($months <=12)  
    	      {  
    	     if($months==1)  
    	           {  
    	       return "1 month ago";  
    	     }  
    	           else  
    	           {  
    	       return "$months months ago";  
    	     }  
    	   }  
    	      else  
    	      {  
    	     if($years==1)  
    	           {  
    	       return "1 year ago";  
    	     }  
    	           else  
    	           {  
    	       return "$years years ago";  
    	     }  
    	   }  
    	}  
    
    	
    }  // Class cdsn_forum_activity_widget ends here
    
    // Register and load the above widget
    function cdsn_load_forum_widget() {
    	register_widget( 'cdsn_forum_activity_widget' );
    }
    
    /* Make it happen */
    
    add_shortcode('cdsnWidget','cdsnWidgetShortCode');
    add_action( 'widgets_init', 'cdsn_load_forum_widget' );
    

    To others:

    Remember my forum-widget has some hard coded values like forum ID’s to exclude plus I use Ultimate Member plugin so meta content is included for that and my specific site. I included a couple of small helper functions used by this and other widgets/code – FYI.

    -jim

    • This reply was modified 7 years, 10 months ago by jgoldbloom. Reason: Fixed a typo
    • This reply was modified 7 years, 10 months ago by jgoldbloom. Reason: code formatting fix
    Thread Starter jgoldbloom

    (@jgoldbloom)

    Thank you, @asgaros — if any issues I’ll post here when time permits me to test. Cheers and welcome back.

    @asgaros – way cool, thanks!

    Forgot to ask, does asgarosforum_filter_post_username allow author_id to be passed not just username? Or in my function what code can I use to get that?

    Yes, that’s the “programmatic alternative” I noted in my original comment. I’m asking to make it a new feature in the admin setup which I should have clarified. Using the simple macro option to embed author ID in the link for that feature is easy to parse in your code and would fully support other plugins where a profile link almost has the one common element, the ID, fyi.

    The reason I suggested the feature request is because linking the avatar and/or author name should be available to non-developers and such a feature is very useful, common and sensible, I think.

    Also might want to explore Akismet and WP Bruiser plugins for tough post security.

    Thread Starter jgoldbloom

    (@jgoldbloom)

    Just a quick comment the hooks working perfectly in dev, I use them to purge like/dislike meta data in custom fields to ensure the DB is tidy. I also reset likes/dislikes to zero with the legacy edit hook when a post is edited which seems fair! Love ‘dem hooks,.

    Thread Starter jgoldbloom

    (@jgoldbloom)

    Actually I updated my AJAX widget and added a count of replies – hint hint @asgaros after the time ago info, pluralized as needed and no count if no replies yet (which I prefer over “no replies” etc. which is a bummer, heh):

    Recent Forum Activity AJAX widget

    ??

    Thread Starter jgoldbloom

    (@jgoldbloom)

    Thread Starter jgoldbloom

    (@jgoldbloom)

    Only ASCII is allowed in the subject if UTF-8 encoded, which is the encoding I would use overall, but maybe apply something like the following for the subject line only:

    <?php html_entity_decode($subject, ENT_QUOTES | ENT_XML1, 'UTF-8') ?>

    … in addition to stripping HTML/slashes for the subject. It’s the &xxx; entities in question here. esc_html() is more or less lossless — it just turns HTML markup into encoded visible text, so that it’s not rendered as markup by browser.

    @asgaros – personally, instead of or in addition to adding a custom profile link/button below the avatar, etc. using that hook for the author panel it might be cool to:

    • Add an option in settings for a profile link URL that will activate when the avatar itself (or username)
    • Allow a macro like %id% to be embeded in the URL to expand to the author ID in the front end

    ??

    p.s. There are programmatic alternatives but this seems like a useful feature for everyone, my .02 — but you get the idea of basically what I mean.

    Thread Starter jgoldbloom

    (@jgoldbloom)

    To all, I made two fixes in rss-forum.php since original post, adding proper GUID and post link pagination:

    <?php
    /*
    	
    Template Name: Custom RSS Template - Feedname (rss-forum.php) - Asgaros recent topics feed (RSS 2.0)
    
    */
    
    // Configuration options:
    $excludeForumIDs="36"; // Comma separated parent forum ID's to exclude fron wp_forum_forums table query
    $postCount = 25; // The number of posts to show in the feed
    $truncateSize=350; // How many characters to allow for description (truncated at nearest word)
    $postsPerPage=25; // matched to # of topics per page in Asgaros for page link caslculation in post url
    
    // Function: Sanitize query data to ensure properly escaped and newlines converted to HTML line breaks 
    function cdsnSanitize($str) {
    	return nl2br(stripslashes($str),false);
    }
    
    // Function: Truncate a string to configured size/nearest word
    function cdsnTruncate($text, $length) {
       $length = abs((int)$length);
       if(strlen($text) > $length) {
          $text = preg_replace("/^(.{1,$length})(\s.*|$)/s", '\\1...', $text);
       }
       return($text);
    }
    
    // BEGIN FEED OUPUT
    header('Content-Type: '.feed_content_type('rss-http').'; charset=UTF-8', true);
    print '<?xml version="1.0" encoding="UTF-8"?>';
    
    ?>
    <rss version="2.0"
            xmlns:dc="https://purl.org/dc/elements/1.1/"
            xmlns:atom="https://www.w3.org/2005/Atom"
            xmlns:sy="https://purl.org/rss/1.0/modules/syndication/"
            xmlns:media="https://search.yahoo.com/mrss/"
            <?php do_action('rss2_ns'); ?>>
    <channel>
            <title><?php bloginfo_rss('name'); ?> - Recent Forum Activity</title>
            <atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" />
            <link><?php bloginfo_rss('url') ?></link>
            <description><?php bloginfo_rss('description'); ?>...</description>
            <lastBuildDate><?php print mysql2date('D, d M Y H:i:s +0000', get_lastpostmodified('GMT'), false); ?></lastBuildDate>
            <language>en-us</language>
            <sy:updatePeriod><?php print apply_filters( 'rss_update_period', 'hourly' ); ?></sy:updatePeriod>
            <sy:updateFrequency><?php print apply_filters( 'rss_update_frequency', '1' ); ?></sy:updateFrequency>
            <?php 
    	    
    	    // Query for topic data
     		$query="        
    		SELECT p1.text, p1.id, p1.date, p1.parent_id, p1.author_id, t.name, u.display_name, um.meta_value as avatar,
    		(SELECT COUNT(id) FROM wp_forum_posts WHERE parent_id=p1.parent_id) AS post_counter FROM wp_forum_posts AS p1 
    		LEFT JOIN wp_forum_posts AS p2 ON (p1.parent_id = p2.parent_id AND p1.id > p2.id) 
    		LEFT JOIN wp_forum_topics AS t ON (t.id = p1.parent_id) 
    		LEFT JOIN wp_forum_forums AS f ON (f.id = t.parent_id)
    		LEFT JOIN wp_users AS u ON (u.id = p1.author_id)
    		LEFT JOIN wp_usermeta AS um ON (um.user_id = p1.author_id and meta_key = 'profile_photo')			 
    		WHERE p2.id IS NULL AND f.parent_id NOT IN ($excludeForumIDs) ORDER BY t.id DESC LIMIT $postCount;";
            
    		// Execute our query
    		$posts = $wpdb->get_results($query);
            
           ?> 
           <?php foreach ($posts as $post) { 
    	         
    	         // Prepare our topic link as this is not stored in the database  
    		     $url=get_site_url();
    		     $pageNumber = ceil($post->post_counter / $postsPerPage);
    		     $link="{$url}/cdsn-forum/?view=thread&id={$post->parent_id}=&part=$pageNumber#postid-{$post->id}";
    		     
    		     // Prepare our avatar handled by Ultimate Member plugin
    		     $avatar_info=new SplFileInfo($post->avatar);
    		     $avatar_ext=$avatar_info->getExtension();
    		     $avatar="{$url}/wp-content/uploads/ultimatemember/{$post->author_id}/profile_photo-190.{$avatar_ext}";
    		     unset ($avatar_info);
    		     
    		     // Ensure we have a default author name if missing
    		     $post->display_name=(!empty($post->display_name)) ? $post->display_name : 'CDSN Member';
    	       
    	       ?>     
               	<item>
                        <title><?php print strip_tags(cdsnSanitize($post->name)); ?></title>
                        <link><![CDATA[<?php print cdsnSanitize($link); ?>]]></link>
                        <pubDate><?php print mysql2date('D, d M Y H:i:s +0000', cdsnSanitize($post->date)); ?></pubDate>
                        <dc:creator><?php print cdsnSanitize($post->display_name); ?></dc:creator>
                        <guid isPermaLink="false"><![CDATA[<?php print cdsnSanitize($link); ?>]]></guid>
                        <description><![CDATA[<?php print cdsnTruncate(cdsnSanitize($post->text),$truncateSize); ?>]]></description>
                        <media:thumbnail url='<?php print $avatar; ?>' height='190' width='190' />   
                </item>
            <?php } ?>
    </channel>
    </rss>

    Marking this topic as resolved.

    Thread Starter jgoldbloom

    (@jgoldbloom)

    @fahmet – this is an Asgaros support forum and this topic is a feed for that plugin only not bbPress.

    508, btw, refers to the website is temporarily unable to service your request as it exceeded resource limit. It happens when the number of processes (RAM/CPU/INODES) exceeds the limits set by the hosting provider. You need to enhance the capacity with your hosting provider or check in the code whether any process takes longer time (like background tasks). Yoast SEO also has its own support forum or switch to All in One SEO which I switched to as Yoast had numerous issues for my site that all went away after switching.

    Thread Starter jgoldbloom

    (@jgoldbloom)

    Fantastic, thank you.

    ??

    • This reply was modified 7 years, 11 months ago by jgoldbloom.
    Thread Starter jgoldbloom

    (@jgoldbloom)

    Now it started working, but I’ll be honest I think your server was non-responsive for a bit (not my connection, not browser specific, popups disabled) and the lack of an error message as I stated was frustrating. FYI.

Viewing 15 replies - 16 through 30 (of 77 total)