• Do not register here on develop.twiki.org, login with your twiki.org account.
• Use View topic Item7848 for generic doc work for TWiki-6.1.1. Use View topic Item7851 for doc work on extensions that are not part of a release. More... Close
• Anything you create or change in standard webs (Main, TWiki, Sandbox etc) will be automatically reverted on every SVN update.
Does this site look broken?. Use the LitterTray web for test cases.

Item7411: Watch all topics in web and watch new topics in web; fix for mod_perl

Item Form Data

AppliesTo: Component: Priority: CurrentState: WaitingFor: TargetRelease ReleasedIn
Extension WatchlistPlugin Enhancement Closed   patch 6.0.1

Edit Form Data

Summary:
Reported By:
Codebase:
Applies To:
Component:
Priority:
Current State:
Waiting For:
Target Release:
Released In:
 

Detail

Here we go again.
  • WatchlistPlugin attempts to cache the changed topics in each web when creating the digest messages. It rebuilds the cache for each user, but the data is valid for all users.
  • The code that builds the list of changes in each web assumed that TWiki::Func::eachChangeSince returns changes in chronological order. It returns them in reverse chronological order (most recent first.)
  • IE Table alignment issues with the Watched Topics tab.
Found while prototyping watch entire web; I didn't break out the changes.

Since the stated goal of Watchlist plugin is to replace MailerContrib, the absence of watch web and watch new seem like bugs.

With the following prototype, if you watch an entire web, you will be notified of new topics as well.

The code could use some more testing, but it seems like a reasonable start... It adds a web-level checkbox to the 'Watched Topics' tab on the Watchlist page. If checked, all topics in that web will be watched. Any topics listed individually in that web are redundant. All user webs will appear on the tab.

Loose ends:

  • I'm not sure I like the behavior of 'set all' and 'unwatch all'; seems to me that webs and topics might be different. In any case, the label should probably change to 'select all' and 'deselect all'; the mixture of 'set' and 'watch' doesn't make sense. And 'unwatch' doesn't actually remove the watch until 'Update Watchlist' is clicked.
  • The Recent Changes tab should be reformatted.
    • I think the Web should come before the topic, and the rest of each line should be in the same order as WebChanges (Topic, rev, date, user)
    • Is it really useful to have the table be sortable? If so, each field should be a separate column. Or disable sorting.
  • I haven't tested deleting/renaming topics, emptying trash - but if these worked before, they still should.
  • If watching a lot of topics in several webs, it might be convenient if each Web header was a twisty that hid/revealed the topics watched in that web.
Building the Recent Changes list seems to take a while if you watch a web with lots of changes.

Apply this patch after the patches in items 7408, 7409, and 7410.

--- /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin/Core.pm~   2014-01-17 06:56:03.000000000 -0500
+++ /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin/Core.pm    2014-01-17 14:06:34.000000000 -0500
@@ -176,23 +176,23 @@
     my $lastUpdateFile = "$workDir/lastupdate.txt";
     my $lastUpdate = TWiki::Func::readFile( $lastUpdateFile ) || 0;
     my $now = time();
     TWiki::Func::saveFile( $lastUpdateFile, $now );
     print "WatchlistPlugin digest notification START at "
-      . TWiki::Time::formatTime( $now ) . "\n" if( $verbose );
+      . TWiki::Time::formatTime( $now ) . ", since " . TWiki::Time::formatTime( $lastUpdate ) . "\n" if( $verbose );
     my $web = $this->{watchlistWeb};
     my $viewScriptUrl = TWiki::Func::getViewUrl( 'WebName', 'TopicName' );
-    $viewScriptUrl =~ s/.WebName.TopicName//o; # cut weband topic
-    foreach my $topic ( $this->_findWatchlistTopicsByPrefs( undef, 'n2' ) ) {
+    $viewScriptUrl =~ s/.WebName.TopicName//o; # cut web and topic
+    my $webItrs; # Cache changes in the webs
+    my $webData;
+    foreach my $topic ( $this->_findWatchlistTopicsByPrefs( undef, 'n2' ) ) { # Users wanting digest notification
         print "- processing $web.$topic\n" .
               "  - updated since last time: " if( $verbose );
         my $wikiUser = $topic;
         $wikiUser =~ s/Watchlist$//;
-        my $webItrs;
-        my $webData;
         my $dateRef;
-        my $wTopics = $this->_getWatchedTopics( $web, $topic );
+        my $wTopics = $this->_getWatchedTopics( $web, $topic ); # Topics watched by this user
         foreach my $item ( split( /,\s*/, $wTopics ) ) {
             my( $w, $t ) = split( /\./, $item );
             unless( $webItrs->{$w} ) {
                 $it = TWiki::Func::eachChangeSince( $w, $lastUpdate + 1 );
                 $webItrs->{$w} = $it;
@@ -203,13 +203,24 @@
                     next unless TWiki::Func::topicExists( $w, $cTopic );
                     $webData->{$w}{$cTopic} = {
                       user => $change->{user},
                       time => $change->{time},
                       rev  => $change->{revision},
-                    };
+                    } unless( exists $webData->{$w}{$cTopic} ); # Keep only most recent revision.
                 }
             }
+            if( $t eq '*' ) { # Watching entire web, match all changed topics
+                next unless( exists $webData->{$w} );
+                foreach my $t (keys %{$webData->{$w}}) {
+                    if( $webData->{$w}{$t} && $webData->{$w}{$t}{user} ne $wikiUser ) {
+                        # changed topic is watched, so add to date reference
+                        $dateRef->{"$w~$t"} = $webData->{$w}{$t}{time};
+                        print "$w.$t, " if( $verbose );
+                    }
+                }
+                next;
+            }
             if( $webData->{$w}{$t} && $webData->{$w}{$t}{user} ne $wikiUser ) {
                 # changed topic is watched, so add to date reference
                 $dateRef->{"$w~$t"} = $webData->{$w}{$t}{time};
                 print "$w.$t, " if( $verbose );
             }
@@ -337,10 +348,20 @@
         my $dateRef;
         my $userRef;
         my $revRef;
         foreach my $item ( split( /, */, $watchedTopics ) ) {
             my ( $w, $t ) = TWiki::Func::normalizeWebTopicName( $web, $item );
+            if( $t eq '*' ) {
+                next unless( TWiki::Func::webExists( $w ) );
+                foreach my $t (TWiki::Func::getTopicList( $w )) {
+                    my ( $date, $user, $rev ) = TWiki::Func::getRevisionInfo( $w, $t );
+                    $dateRef->{"$w~$t"} = $date;
+                    $userRef->{"$w~$t"} = $user;
+                    $revRef->{"$w~$t"} = $rev;
+                }
+                next;
+            }
             next unless( TWiki::Func::topicExists( $w, $t ) );
             my ( $date, $user, $rev ) = TWiki::Func::getRevisionInfo( $w, $t );
             $dateRef->{"$w~$t"} = $date;
             $userRef->{"$w~$t"} = $user;
             $revRef->{"$w~$t"} = $rev;
@@ -382,36 +403,65 @@
     my $watchedTopics = $params->{topics};
     $watchedTopics =~ s/^\s+//s;
     $watchedTopics =~ s/\s+$//s;
     $watchedTopics = join( ', ',
         sort
-        grep { TWiki::Func::topicExists( $web, $_ ) }
+        grep { /^(.*)\.\*$/? TWiki::Func::webExists( $1 ) : TWiki::Func::topicExists( $web, $_ ) }
         split( /[\s,]+/, $watchedTopics )
       );
+    my %webs = map { $_ => 1 } grep { $_ !~ /^(?:$TWiki::cfg{TrashWebName})$/ } TWiki::Func::getListOfWebs( 'user' );
+    my $first = 1;
+
+    $this->{text} .= ( '<form action="%SCRIPTURLPATH{viewauth}%/' . "$web/$topic\""
+                       . ' id="watchlist_topics" method="post">' . "\n" );
+    my $context = TWiki::Func::getContext();
     if( $watchedTopics ) {
-        $this->{text} .= '<form action="%SCRIPTURLPATH{viewauth}%/' . "$web/$topic\""
-                       . ' id="watchlist_topics" method="post">' . "\n";
-        my $currentWeb = '';
+       my $currentWeb = '';
         foreach my $item ( split( /, */, $watchedTopics ) ) {
             my ( $w, $t ) = TWiki::Func::normalizeWebTopicName( $web, $item );
+            $this->{text} .= "%TABLE{sort=\"off\" dataalign=\"left\" headeralign=\"left\" }%\n" if( $first && $context->{TablePluginEnabled} );
+            $first = 0;
             if( $w ne $currentWeb ) {
-                $this->{text} .= '| *<img src="%ICONURL{web-bg}%" border="0" alt="" '
+                delete $webs{$w};
+                $this->{text} .= '| *<img src="%ICONURL{web-bg}%" border="0" alt="Web color" '
                   . 'width="16" height="16" style="background-color:%VAR{"WEBBGCOLOR" '
-                  . "web=\"$w\"}%\" /> <nop>$w web:* |\n";
+                  . "web=\"$w\"}%\" /> <nop>$w web:*";
                 $currentWeb = $w;
+                $this->{text} .= ' | *<input type="checkbox" name="watchlist_item" !value="' . "$w.*" .
+                                           '" title="Watch all topics in the ' . $w . ' web"';
+               if( $t eq '*' ) {
+                    $this->{text} .= " checked=\"checked\" />* |\n";
+                    next;
+                } else {
+                    $this->{text} .= " />* |\n";
+                }
             }
             $this->{text} .= '| %ICON{empty}% <input type="checkbox" name="watchlist_item" '
                   . "value=\"$w.$t\" checked=\"checked\" /> "
-                  . "[[$w.$t][$t]] |\n";
+                  . "[[$w.$t][$t]]  | |\n";
         }
+    }
+    foreach my $web (sort keys %webs) {
+        $this->{text} .= "%TABLE{sort=\"off\" dataalign=\"left\" headeralign=\"left\" }%\n" if( $first && $context->{TablePluginEnabled} );
+        $first = 0;
+        $this->{text} .= '|  *<img src="%ICONURL{web-bg}%" border="0" alt="Web color" '
+                  . 'width="16" height="16" style="background-color:%VAR{"WEBBGCOLOR" '
+                  . "web=\"$web\"}%\" /> <nop>$web web:*";
+        $this->{text} .= '   | *<input type="checkbox" name="watchlist_item" !value="' . "$web.*" .
+                                   '" title="Watch all topics in the ' . $web . ' web" />*' ." |\n";
+    }
+    if( $first ) {
+        $this->{note} .= $this->{EmptyMessage};
+        $this->{text} .= "</form>";
+    } else {
         $this->{text} .= '| <div style="font-size: 90%;">'
           . '<input type="button" value="Set All"'
           . ' onClick="chkAll(this.form, true);" /> '
           . '<input type="button" value="Unwatch All"'
-          . ' onClick="chkAll(this.form, false);" /></div> |' . "\n"
+          . ' onClick="chkAll(this.form, false);" /></div> ||' . "\n"
           . '| <input type="submit" value="Update Watchlist" '
-          . 'class="twikiSubmit" /> |' . "\n"
+          . 'class="twikiSubmit" /> ||' . "\n"
           . '<input type="hidden" name="watchlist_topic" value="' . "$web.$topic\" />\n"
           . '<input type="hidden" name="watchlist_action" value="updatelist" />' . "\n"
           . '<script language="JavaScript">' . "\n"
           . '<!-- HIDE and <pre> escape TWiki rendering' . "\n"
           . '  function chkAll( theForm, theCheck )' . "\n"
@@ -421,13 +471,12 @@
           . '    }' . "\n"
           . '  }' . "\n"
           . '//STOP HIDING and </pre> escaping -->' . "\n"
           . '</script>' . "\n"
           . '</form>';
-    } else {
-        $this->{note} .= $this->{EmptyMessage};
     }
+
     return;
 }

 # =========================
 sub _actionUpdateWatchedTopics {
@@ -493,12 +542,14 @@
     my ( $this, $webTopic ) = @_;

     # All watchlist topics:
     my $web = $this->{watchlistWeb};
     my @topics = grep { /Watchlist$/ } TWiki::Func::getTopicList( $web );
-    # Reduce watchlist topics to those that have the "Web.Topic":
-    my $regex = $watchedPreRegex . ".*\\b$webTopic\\b";
+    # Reduce watchlist topics to those that have the "Web.Topic" or "Web.*":
+    my $regex = $watchedPreRegex;
+    my( $w, $t ) =  $webTopic =~ /^(.*)\.([^.]+)$/;
+    $regex .=  ".*\\b(?:$webTopic\\b|$w\\.\\*(?:\\b|\"))";
     my $result = TWiki::Func::searchInWebContent( $regex, $web, \@topics,
                                                   { type => 'regex' } );
     return sort ( keys %$result );
 }

@@ -510,11 +561,11 @@
     my $web = $this->{watchlistWeb};
     unless( $topicRef ) {
         my @topics = grep { /Watchlist$/ } TWiki::Func::getTopicList( $web );
         $topicRef = \@topics;
     }
-    # Reduce watchlist topics to those that have the "notify" preference:
+    # Reduce watchlist topics to those that have the "$notify" type (immed,digest,etc) preference:
     my $regex = $prefsPreRegex . ".*\\b$notify\\b";
     my $result = TWiki::Func::searchInWebContent( $regex, $web, $topicRef,
                                                   { type => 'regex' } );
     return sort ( keys %$result );
 }

Enjoy.

-- TWiki:Main/TimotheLitt - 2014-01-17

Thank you Timothe for the patch! I'll look into it.

Could you please read the log of yesterday's release meeting? We discussed this feature, TWiki:Codev.KampalaReleaseMeeting2014x01x16.

-- TWiki:Main.PeterThoeny - 2014-01-17

A prototype in the hand is worth a lot of discussion. Suggest you apply the patch and see if you like it. Don't make a formal release, yet. If you put it in svn, the others can try it from trunk.

This version provides a 'watch all' checkbox for an entire web. It's a very intuitive UI. It doesn't have the 1+ features discussed in the meeting - which I would oppose. Simple is good.

I am thinking about one more checkbox - 'watch all new topics in this web'. This would automatically add new topics to the user's watch list (and so notify of creation and subsequent changes.) With this, it won't be necessary to watch the whole web to get notice of new topics. If you decide you aren't interested, you unwatch the new topic. Subsequent changes won't notify you.

If that works out, it will be incremental to (based on) this patch.

If there is demand for 'watch children of a topic', I suggest that the creator of a topic add it to the watch list: perhaps a meta variable that a template can supply or a save option. I don't think that complicating the watchlist UI to deal with parent/child relationships is a good thing. If we make it complicated, it won't be used - and/or it will be buggy.

-- TWiki:Main.TimotheLitt - 2014-01-17

Well, this was a bit of a hassle; more bugs in the base code. The anti-recursion logic didn't work for autoinc topics. Interpolated '.' into regexes without escaping it. And splitting web.topic on . won't work with subwebs.

There were some problems with the token that I used for a wildcard; I changed that, so if you played with the first version, you may see a strange 'topic' in the watch list. You can delete it with the form and re-select 'all'. Sorry about that.

Anyhow, here is an incremental patch that implements 'New' as described above. This patch applies AFTER all the previous patches. Note that this one touches both modules.

--- /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin.pm~        2013-09-15 18:09:19.000000000 -0400
+++ /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin.pm 2014-01-17 21:40:35.000000000 -0500
@@ -29,11 +29,10 @@
 # =========================
 my $debug = $TWiki::cfg{Plugins}{WatchlistPlugin}{Debug} || 0;
 my $core;
 my $baseWeb;
 my $baseTopic;
-my $previousAfterSave;

 # =========================
 sub initPlugin {
     ( $baseTopic, $baseWeb ) = @_;

@@ -42,11 +41,10 @@
         TWiki::Func::writeWarning( "Version mismatch between WatchlistPlugin and Plugins.pm" );
         return 0;
     }

     $core = undef;
-    $previousAfterSave = '';
     TWiki::Func::registerTagHandler( 'WATCHLIST', \&_WATCHLIST );

     # Plugin correctly initialized
     TWiki::Func::writeDebug( "- WatchlistPlugin: initPlugin( "
       . "$baseWeb.$baseTopic ) is OK" ) if $debug;
@@ -65,22 +63,39 @@
     }
     return $core->VarWATCHLIST( @_ );
 }

 # =========================
+sub beforeSaveHandler {
+    # do not uncomment, use $_[0], $_[1]... instead
+    ### my ( $text, $topic, $web, $meta ) = @_;
+
+    TWiki::Func::writeDebug( "- WatchlistPlugin: beforeSaveHandler( " .
+                             "$_[2].$_[1] )" ) if $debug;
+
+    # Prevent recursion
+    my $query = TWiki::Func::getCgiQuery();
+    return if( $query && $query->param( 'watchlist_action' ) );
+
+    unless( $core ) {
+        require TWiki::Plugins::WatchlistPlugin::Core;
+        $core = new TWiki::Plugins::WatchlistPlugin::Core( $baseWeb, $baseTopic );
+    }
+    return $core->beforeSaveHandler( @_ );
+}
+
+# =========================
 sub afterSaveHandler {
     # do not uncomment, use $_[0], $_[1]... instead
     ### my ( $text, $topic, $web, $error, $meta ) = @_;

     TWiki::Func::writeDebug( "- WatchlistPlugin: afterSaveHandler( " .
                              "$_[2].$_[1] )" ) if $debug;

     # Prevent recursion
     my $query = TWiki::Func::getCgiQuery();
     return if( $query && $query->param( 'watchlist_action' ) );
-    return if( "$_[2].$_[1]" eq $previousAfterSave );
-    $previousAfterSave = "$_[2].$_[1]";

     unless( $core ) {
         require TWiki::Plugins::WatchlistPlugin::Core;
         $core = new TWiki::Plugins::WatchlistPlugin::Core( $baseWeb, $baseTopic );
     }
--- /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin/Core.pm~   2014-01-17 14:06:34.000000000 -0500
+++ /var/www/servers/twiki/lib/TWiki/Plugins/WatchlistPlugin/Core.pm    2014-01-17 22:36:52.000000000 -0500
@@ -132,24 +132,55 @@
     }
     return $text;
 }

 # =========================
+sub beforeSaveHandler {
+    # do not uncomment, use $_[0], $_[1]... instead
+    ### my ( $this, $text, $topic, $web, $meta ) = @_;
+
+    my $this  = shift;
+    my $web   = $_[2];
+    my $topic = $_[1];
+
+    push @{$this->{exists}}, TWiki::Func::topicExists( $web, $topic ) || 0;
+
+    return;
+}
+
+# =========================
 sub afterSaveHandler {
     # do not uncomment, use $_[0], $_[1]... instead
     ### my ( $text, $topic, $web, $error, $meta ) = @_;
     my $this  = shift;
     my $web   = $_[2];
     my $topic = $_[1];

+    my $newtopic = !pop @{$this->{exists}} || 0;
+    return if( @{$this->{exists}} ); # Recursing
+
     my $savingUser = TWiki::Func::getWikiName || '';
     my @topics = $this->_findWatchlistTopicsByWatchedTopic( "$web.$topic" );
     foreach my $wikiUser ( $this->_findWatchlistTopicsByPrefs( \@topics, 'n1' ) ) {
+        if( $newtopic  ) {
+            my $ttext = TWiki::Func::readTopicText( $this->{watchlistWeb}, $wikiUser, undef, 1 );
+            if( $ttext && $ttext !~ /^http.*?\/oops/  ) {
+                my $regex = $watchedPreRegex;
+                $regex .= ".*\\b$web\\.##(?:\\b|[,\"])";
+                if( $ttext =~ /$regex/ ) {
+                    if( $ttext =~ s/$watchedRegex/$1$web.$topic, $2/ ) { # at least Web.## exists, so safe to insert at front
+                        # We know this topic exists, so this can't recurse.
+                        TWiki::Func::saveTopicText( $this->{watchlistWeb}, $wikiUser, $ttext, 1, 1 );
+                    }
+                }
+            }
+        }
         $wikiUser =~ s/Watchlist$//;
         $this->_sendWatchlistEmail( $web, $topic, $wikiUser )
           unless( $wikiUser eq $savingUser );
     }
+    return;
 }

 # =========================
 sub afterRenameHandler {
     my ( $this, $oldWeb, $oldTopic, $unused, $newWeb, $newTopic ) = @_;
@@ -190,11 +221,11 @@
         my $wikiUser = $topic;
         $wikiUser =~ s/Watchlist$//;
         my $dateRef;
         my $wTopics = $this->_getWatchedTopics( $web, $topic ); # Topics watched by this user
         foreach my $item ( split( /,\s*/, $wTopics ) ) {
-            my( $w, $t ) = split( /\./, $item );
+            my( $w, $t ) = $item =~ /^(.*)\.([^.]+)$/;
             unless( $webItrs->{$w} ) {
                 $it = TWiki::Func::eachChangeSince( $w, $lastUpdate + 1 );
                 $webItrs->{$w} = $it;
                 while( $it->hasNext() ) {
                     my $change = $it->next();
@@ -206,11 +237,12 @@
                       time => $change->{time},
                       rev  => $change->{revision},
                     } unless( exists $webData->{$w}{$cTopic} ); # Keep only most recent revision.
                 }
             }
-            if( $t eq '*' ) { # Watching entire web, match all changed topics
+            next if( $t eq '##' ); # Ignore new topic token
+            if( $t eq '#' ) { # Watching entire web, match all changed topics
                 next unless( exists $webData->{$w} );
                 foreach my $t (keys %{$webData->{$w}}) {
                     if( $webData->{$w}{$t} && $webData->{$w}{$t}{user} ne $wikiUser ) {
                         # changed topic is watched, so add to date reference
                         $dateRef->{"$w~$t"} = $webData->{$w}{$t}{time};
@@ -348,11 +380,12 @@
         my $dateRef;
         my $userRef;
         my $revRef;
         foreach my $item ( split( /, */, $watchedTopics ) ) {
             my ( $w, $t ) = TWiki::Func::normalizeWebTopicName( $web, $item );
-            if( $t eq '*' ) {
+            next if( $t eq '##' );
+            if( $t eq '#' ) {
                 next unless( TWiki::Func::webExists( $w ) );
                 foreach my $t (TWiki::Func::getTopicList( $w )) {
                     my ( $date, $user, $rev ) = TWiki::Func::getRevisionInfo( $w, $t );
                     $dateRef->{"$w~$t"} = $date;
                     $userRef->{"$w~$t"} = $user;
@@ -403,40 +436,55 @@
     my $watchedTopics = $params->{topics};
     $watchedTopics =~ s/^\s+//s;
     $watchedTopics =~ s/\s+$//s;
     $watchedTopics = join( ', ',
         sort
-        grep { /^(.*)\.\*$/? TWiki::Func::webExists( $1 ) : TWiki::Func::topicExists( $web, $_ ) }
+        grep { /^(.*)\.##?$/? TWiki::Func::webExists( $1 ) : TWiki::Func::topicExists( $web, $_ ) }
         split( /[\s,]+/, $watchedTopics )
       );
     my %webs = map { $_ => 1 } grep { $_ !~ /^(?:$TWiki::cfg{TrashWebName})$/ } TWiki::Func::getListOfWebs( 'user' );
     my $first = 1;

-    $this->{text} .= ( '<form action="%SCRIPTURLPATH{viewauth}%/' . "$web/$topic\""
+    $this->{text} .= ( q(%TWISTY{mode="div" showlink="Instructions" hidelink="Hide instructions" showimgleft="%ICONURLPATH{toggleopen-small}%" hideimgleft="%ICONURLPATH{toggleclose-small}%"}%<small>%MAKETEXT{"To watch all topics in a web, check the *All* box.<br />To have new topics added to your watchlist automatically, check the *New* box.<br />Then click *Update Watchlist*.<br />To add a topic to the list, visit it and click *Watch* on the menu bar."}%<br /></small>%ENDTWISTY%) .
+                       '<form action="%SCRIPTURLPATH{viewauth}%/' . "$web/$topic\""
                        . ' id="watchlist_topics" method="post">' . "\n" );
     my $context = TWiki::Func::getContext();
     if( $watchedTopics ) {
        my $currentWeb = '';
-        foreach my $item ( split( /, */, $watchedTopics ) ) {
+       my @items = split( /, */, $watchedTopics );
+       while( @items ) {
+            my $item = shift @items;
             my ( $w, $t ) = TWiki::Func::normalizeWebTopicName( $web, $item );
             $this->{text} .= "%TABLE{sort=\"off\" dataalign=\"left\" headeralign=\"left\" }%\n" if( $first && $context->{TablePluginEnabled} );
             $first = 0;
             if( $w ne $currentWeb ) {
                 delete $webs{$w};
                 $this->{text} .= '| *<img src="%ICONURL{web-bg}%" border="0" alt="Web color" '
                   . 'width="16" height="16" style="background-color:%VAR{"WEBBGCOLOR" '
                   . "web=\"$w\"}%\" /> <nop>$w web:*";
                 $currentWeb = $w;
-                $this->{text} .= ' | *<input type="checkbox" name="watchlist_item" !value="' . "$w.*" .
+                $this->{text} .= ' | *All: <input type="checkbox" name="watchlist_item" !value="' . "$w.#" .
                                            '" title="Watch all topics in the ' . $w . ' web"';
-               if( $t eq '*' ) {
+                if( $t eq '#' ) {
+                    $this->{text} .= " checked=\"checked\" />";
+                } else {
+                    $this->{text} .= " /> ";
+                }
+                $this->{text} .= ' New: <input type="checkbox" name="watchlist_item" !value="' . "$w.##" .
+                                           '" title="Watch new topics in the ' . $w . ' web"';
+                if( @items && $items[0] =~ /^$w\.##$/ ) {
+                    $t = '##';
+                    shift @items;
+                }
+                if( $t eq '##' ) {
                     $this->{text} .= " checked=\"checked\" />* |\n";
-                    next;
                 } else {
                     $this->{text} .= " />* |\n";
                 }
+                next if( $t =~/^##?$/ );
             }
+
             $this->{text} .= '| %ICON{empty}% <input type="checkbox" name="watchlist_item" '
                   . "value=\"$w.$t\" checked=\"checked\" /> "
                   . "[[$w.$t][$t]]  | |\n";
         }
     }
@@ -444,21 +492,23 @@
         $this->{text} .= "%TABLE{sort=\"off\" dataalign=\"left\" headeralign=\"left\" }%\n" if( $first && $context->{TablePluginEnabled} );
         $first = 0;
         $this->{text} .= '|  *<img src="%ICONURL{web-bg}%" border="0" alt="Web color" '
                   . 'width="16" height="16" style="background-color:%VAR{"WEBBGCOLOR" '
                   . "web=\"$web\"}%\" /> <nop>$web web:*";
-        $this->{text} .= '   | *<input type="checkbox" name="watchlist_item" !value="' . "$web.*" .
-                                   '" title="Watch all topics in the ' . $web . ' web" />*' ." |\n";
+        $this->{text} .= '   | *All: <input type="checkbox" name="watchlist_item" !value="' . "$web.#" .
+                                   '" title="Watch all topics in the ' . $web . ' web" />';
+        $this->{text} .= ' New: <input type="checkbox" name="watchlist_item" !value="' . "$web.##" .
+                                   '" title="Watch new topics in the ' . $web . ' web" />*' ." |\n";
     }
     if( $first ) {
         $this->{note} .= $this->{EmptyMessage};
         $this->{text} .= "</form>";
     } else {
         $this->{text} .= '| <div style="font-size: 90%;">'
-          . '<input type="button" value="Set All"'
+          . '<input type="button" value="Check All"'
           . ' onClick="chkAll(this.form, true);" /> '
-          . '<input type="button" value="Unwatch All"'
+          . '<input type="button" value="Uncheck All"'
           . ' onClick="chkAll(this.form, false);" /></div> ||' . "\n"
           . '| <input type="submit" value="Update Watchlist" '
           . 'class="twikiSubmit" /> ||' . "\n"
           . '<input type="hidden" name="watchlist_topic" value="' . "$web.$topic\" />\n"
           . '<input type="hidden" name="watchlist_action" value="updatelist" />' . "\n"
@@ -542,14 +592,16 @@
     my ( $this, $webTopic ) = @_;

     # All watchlist topics:
     my $web = $this->{watchlistWeb};
     my @topics = grep { /Watchlist$/ } TWiki::Func::getTopicList( $web );
-    # Reduce watchlist topics to those that have the "Web.Topic" or "Web.*":
+    # Reduce watchlist topics to those that have the "Web.Topic", "Web.#", or "Web.##":
     my $regex = $watchedPreRegex;
     my( $w, $t ) =  $webTopic =~ /^(.*)\.([^.]+)$/;
-    $regex .=  ".*\\b(?:$webTopic\\b|$w\\.\\*(?:\\b|\"))";
+    $w =~ s/\./\\./g;
+    $webTopic =~ s/\./\\./g;
+    $regex .=  ".*\\b(?:$webTopic\\b|$w\\.##?(?:\\b|[,\"]))";
     my $result = TWiki::Func::searchInWebContent( $regex, $web, \@topics,
                                                   { type => 'regex' } );
     return sort ( keys %$result );
 }

I added some instructions, and changed the labels on the un/check buttons.

I didn't do anything about the other loose ends.

Note that when a new topic is created, the user may update other users' watchlists. This is done because the other users' asked for it, so the 'override permissions' use is intentional.

Which raises a point - shouldn't there be a lock protecting all updates of the watchlist?

I think I'm done. Enjoy.

-- Timothe Litt - 2014-01-18

To simplify things, I've attached the patched modules as I use them. (This includes my changes to the 'OK' link that Peter and I haven't converged on.)

Also, a screenshot,

Sample.png

Completely patched WatchlistPlugin files

-- Timothe Litt - 2014-01-18

I've ported this to trunk, made a few improvements and committed it as svn rev 26996.

Although marked "enhancement", there are quite a few bugfixes included.

Since this is a core plugin, I ran perltidy on the sources.

It also had a lot of problems under warnings/strict, some serious.

I still think we need a lock on watchlist updates.

I also still think that "Recent Changes" should be reformatted as noted above.

Note that while I added some support for 4.x in watchlistnotify, the plugin won't run on unmodified 4.anything as it depends on a number of post 4.x features. What I added allows it to run on my 4.x, which has backported those features. The code is harmless in 5.x, but this way I don't have to keep a private version.

In any case, it's available to be played with.

Enjoy.

FYI, revs 26997 and 26998 fix day 0 issues under use warnings and use strict - which the plugin wasn't invoking, and would have caught a warning that briefly broke develop.twiki.org.

The plugin was referencing variables up-scope that weren't intended, failing to declare variables, and committing other sins.

use warnings and use strict really are required syntax....

-- TWiki:Main.TimotheLitt - 2014-02-06

To reduce future problems, I re-architected the interface between the base plugin and the plugin's loadable Core. (N.B. In this discussion, Core means the Core module of the plugin, not the TWiki core.)

The new interface is much cleaner - no more duplicate subroutines to maintain, no more sketchy division of labor between the base and the Core, no more subtly-different argument lists.

The base plugin is much smaller and easier to maintain. The Core knows nothing about the base.

Other plugins that do lazy loading may want to use this as a template and convert.

In fact, since lazy loading is such an important performance win, perhaps EmptyPlugin should be split into a base + Core to encourage developers to use this method.

Essentials:

  • Any plugin handler (e.g. beforeSaveHandler)'s name is listed in the base; it is automatically defined & will invoke the Core method.
  • Any other function (such as a tag handler, REST handler, or Engine->run hander) needs nothing in the plugin hander. It is automagically detected and invoked.

All handlers are invoked as object methods on the Core, which is lazy loaded. The name and the argument list are identical except for the object handle; in fact there is no intermediate stack frame. The handler thinks it is invoked as an object method by whatever called the plugin.

If an unimplemented handler is invoked, an error (confession) results.

-- TWiki:Main.TimotheLitt - 2014-02-07

Thanks Timothe for the extensive enhancements!

I did some layout fixes in the watchlist topics.

Marking this as "waiting for release".

-- TWiki:Main.PeterThoeny - 2014-05-19

Re-open this to fix a bug introduced with this feature: The list of web must not show webs that logged-in user has no access to.

-- TWiki:Main.PeterThoeny - 2014-05-20

Web list issue is now fixed in SVN trunk and 6.0 branch.

-- TWiki:Main.PeterThoeny - 2014-05-20

ItemTemplate
Summary Watch all topics in web and watch new topics in web; fix for mod_perl
ReportedBy TWiki:Main.TimotheLitt
Codebase ~twiki4, 6.0.0, 4.2.3
SVN Range TWiki-6.0.1-trunk, Thu, 09 Jan 2014, build 26720
AppliesTo Extension
Component WatchlistPlugin
Priority Enhancement
CurrentState Closed
WaitingFor

Checkins TWikirev:26996 TWikirev:26997 TWikirev:26998 TWikirev:26999 TWikirev:27000 TWikirev:27317 TWikirev:27318 TWikirev:27319 TWikirev:27320 TWikirev:27321 TWikirev:27426 TWikirev:27427 TWikirev:27428 TWikirev:27429
TargetRelease patch
ReleasedIn 6.0.1
Edit | Attach | Watch | Print version | History: r29 < r28 < r27 < r26 < r25 | Backlinks | Raw View | Raw edit | More topic actions
Topic revision: r29 - 2014-10-06 - PeterThoeny
 
This site is powered by the TWiki collaboration platform Powered by PerlCopyright © 2008-2019 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback