[Rt-commit] [svn] r1212 - in rtfm/trunk: etc html/Callbacks/RTFM/autohandler html/RTFM html/RTFM/Admin/Classes html/RTFM/Admin/Elements html/RTFM/Article html/RTFM/Article/Elements html/RTFM/Elements lib/RT/FM

alexmv at pallas.eruditorum.org alexmv at pallas.eruditorum.org
Mon Jul 12 18:31:21 EDT 2004


Author: alexmv
Date: Mon Jul 12 18:31:21 2004
New Revision: 1212

Added:
   rtfm/trunk/html/RTFM/Admin/Classes/Topics.html
   rtfm/trunk/html/RTFM/Article/Elements/EditTopics
   rtfm/trunk/html/RTFM/Article/Elements/ShowTopics
   rtfm/trunk/html/RTFM/Topics.html
   rtfm/trunk/lib/RT/FM/ObjectTopic.pm
   rtfm/trunk/lib/RT/FM/ObjectTopicCollection.pm
   rtfm/trunk/lib/RT/FM/ObjectTopicCollection_Overlay.pm
   rtfm/trunk/lib/RT/FM/Topic.pm
   rtfm/trunk/lib/RT/FM/TopicCollection.pm
   rtfm/trunk/lib/RT/FM/TopicCollection_Overlay.pm
   rtfm/trunk/lib/RT/FM/Topic_Overlay.pm
Modified:
   rtfm/trunk/etc/acl.Oracle
   rtfm/trunk/etc/acl.Pg
   rtfm/trunk/etc/drop_schema.Oracle
   rtfm/trunk/etc/drop_schema.Pg
   rtfm/trunk/etc/drop_schema.mysql
   rtfm/trunk/etc/schema.Oracle
   rtfm/trunk/etc/schema.Pg
   rtfm/trunk/etc/schema.mysql
   rtfm/trunk/html/Callbacks/RTFM/autohandler/Default
   rtfm/trunk/html/RTFM/Admin/Elements/ClassTabs
   rtfm/trunk/html/RTFM/Article/Display.html
   rtfm/trunk/html/RTFM/Article/Edit.html
   rtfm/trunk/html/RTFM/Article/PreCreate.html
   rtfm/trunk/html/RTFM/Elements/Tabs
   rtfm/trunk/lib/RT/FM/ArticleCollection_Overlay.pm
   rtfm/trunk/lib/RT/FM/Article_Overlay.pm
   rtfm/trunk/lib/RT/FM/Class_Overlay.pm
Log:
 * First pass at topic hierarchies


Modified: rtfm/trunk/etc/acl.Oracle
==============================================================================
--- rtfm/trunk/etc/acl.Oracle	(original)
+++ rtfm/trunk/etc/acl.Oracle	Mon Jul 12 18:31:21 2004
@@ -1,4 +1,4 @@
-# mysql doesn't need new acls
+# Oracle doesn't need new acls
 sub acl {
 	return ('select id from Users where id = 1');
 }

Modified: rtfm/trunk/etc/acl.Pg
==============================================================================
--- rtfm/trunk/etc/acl.Pg	(original)
+++ rtfm/trunk/etc/acl.Pg	Mon Jul 12 18:31:21 2004
@@ -5,7 +5,7 @@
     my @acls;
 
     my @tables = qw (
-fm_classes_id_seq
+FM_Classes_id_seq
 FM_Classes 
 FM_ClassCustomFields_id_seq 
 FM_ClassCustomFields 
@@ -19,6 +19,10 @@
 FM_ArticleCFValues 
 FM_Transactions_id_seq 
 FM_Transactions 
+FM_Topics_id_seq
+FM_Topics
+FM_ObjectTopics_id_seq
+FM_ObjectTopics
 
     );
 

Modified: rtfm/trunk/etc/drop_schema.Oracle
==============================================================================
--- rtfm/trunk/etc/drop_schema.Oracle	(original)
+++ rtfm/trunk/etc/drop_schema.Oracle	Mon Jul 12 18:31:21 2004
@@ -5,7 +5,8 @@
 DROP TABLE FM_CustomFieldValues ;
 DROP TABLE FM_ArticleCFValues ;
 DROP TABLE FM_Transactions ;
-DROP TABLE FM_Deltas ;
+DROP TABLE FM_ObjectTopics ;
+DROP TABLE FM_Topics ;
 DROP sequence FM_Classes_seq;
 DROP sequence FM_ClassCustomFields_seq;
 DROP sequence FM_CustomFields_seq;
@@ -13,4 +14,5 @@
 DROP sequence FM_CustomFieldValues_seq;
 DROP sequence FM_ArticleCFValues_seq;
 DROP sequence FM_Transactions_seq;
-DROP sequence FM_Deltas_seq;
+DROP sequence FM_ObjectTopics_seq;
+DROP sequence FM_Topics_seq;

Modified: rtfm/trunk/etc/drop_schema.Pg
==============================================================================
--- rtfm/trunk/etc/drop_schema.Pg	(original)
+++ rtfm/trunk/etc/drop_schema.Pg	Mon Jul 12 18:31:21 2004
@@ -5,7 +5,8 @@
 DROP TABLE FM_CustomFieldValues ;
 DROP TABLE FM_ArticleCFValues ;
 DROP TABLE FM_Transactions ;
-DROP TABLE FM_Deltas ;
+DROP TABLE FM_ObjectTopics ;
+DROP TABLE FM_Topics ;
 DROP sequence FM_Classes_id_seq;
 DROP sequence FM_ClassCustomFields_id_seq;
 DROP sequence FM_CustomFields_id_seq;
@@ -13,4 +14,5 @@
 DROP sequence FM_CustomFieldValues_id_seq;
 DROP sequence FM_ArticleCFValues_id_seq;
 DROP sequence FM_Transactions_id_seq;
-DROP sequence FM_Deltas_id_seq;
+DROP sequence FM_ObjectTopics_id_seq;
+DROP sequence FM_Topics_id_seq;

Modified: rtfm/trunk/etc/drop_schema.mysql
==============================================================================
--- rtfm/trunk/etc/drop_schema.mysql	(original)
+++ rtfm/trunk/etc/drop_schema.mysql	Mon Jul 12 18:31:21 2004
@@ -5,4 +5,6 @@
 DROP TABLE FM_CustomFieldValues ;
 DROP TABLE FM_ArticleCFValues ;
 DROP TABLE FM_Transactions ;
+DROP TABLE FM_ObjectTopics ;
+DROP TABLE FM_Topics ;
 DELETE FROM Links;

Modified: rtfm/trunk/etc/schema.Oracle
==============================================================================
--- rtfm/trunk/etc/schema.Oracle	(original)
+++ rtfm/trunk/etc/schema.Oracle	Mon Jul 12 18:31:21 2004
@@ -1,3 +1,4 @@
+
 CREATE SEQUENCE FM_Classes_seq;
 CREATE TABLE FM_Classes (
 id NUMBER(11,0)
@@ -12,6 +13,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_ClassCustomFields_seq;
 CREATE TABLE FM_ClassCustomFields (
 id NUMBER(11,0)
@@ -25,6 +27,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_CustomFields_seq;
 CREATE TABLE FM_CustomFields (
 id NUMBER(11,0)
@@ -39,6 +42,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_Articles_seq;
 CREATE TABLE FM_Articles (
 id NUMBER(11,0)
@@ -55,6 +59,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_CustomFieldValues_seq;
 CREATE TABLE FM_CustomFieldValues (
 id NUMBER(11,0)
@@ -69,6 +74,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_ArticleCFValues_seq;
 CREATE TABLE FM_ArticleCFValues (
 id NUMBER(11,0)
@@ -82,6 +88,7 @@
 LastUpdated DATE
 );
 
+
 CREATE SEQUENCE FM_Transactions_seq;
 CREATE TABLE FM_Transactions (
 id NUMBER(11,0)
@@ -95,3 +102,25 @@
 Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
 Created DATE
 );
+
+
+CREATE SEQUENCE FM_Topics_seq;
+CREATE TABLE FM_Topics (
+id NUMBER(11,0)
+  CONSTRAINT FM_Topics_key PRIMARY KEY,
+Parent NUMBER(11,0) DEFAULT 0 NOT NULL,
+Name varchar2(255) DEFAULT '' NOT NULL,
+Description varchar2(255) DEFAULT '' NOT NULL,
+ObjectType varchar2(64) DEFAULT '' NOT NULL,
+ObjectId NUMBER(11,0) NOT NULL
+);
+
+
+CREATE SEQUENCE FM_ObjectTopics_seq;
+CREATE TABLE FM_ObjectTopics (
+id NUMBER(11,0)
+  CONSTRAINT FM_ObjectTopics_key PRIMARY KEY,
+Topic NUMBER(11,0) NOT NULL,
+ObjectType varchar2(64) DEFAULT '' NOT NULL,
+ObjectId NUMBER(11,0) NOT NULL
+);

Modified: rtfm/trunk/etc/schema.Pg
==============================================================================
--- rtfm/trunk/etc/schema.Pg	(original)
+++ rtfm/trunk/etc/schema.Pg	Mon Jul 12 18:31:21 2004
@@ -38,6 +38,8 @@
 LastUpdated TIMESTAMP NULL,
 PRIMARY KEY (id)
 );
+
+
 CREATE TABLE FM_Articles (
 id SERIAL,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -52,6 +54,8 @@
 LastUpdated TIMESTAMP NULL,
 PRIMARY KEY (id)
 );
+
+
 CREATE TABLE FM_CustomFieldValues (
 id SERIAL,
 CustomField int NOT NULL,
@@ -64,6 +68,8 @@
 LastUpdated TIMESTAMP NULL,
 PRIMARY KEY (id)
 );
+
+
 CREATE TABLE FM_ArticleCFValues (
 id SERIAL,
 Article int NOT NULL,
@@ -74,8 +80,9 @@
 LastUpdatedBy integer NOT NULL DEFAULT 0,
 LastUpdated TIMESTAMP NULL,
 PRIMARY KEY (id)
-
 );
+
+
 CREATE TABLE FM_Transactions (
 id SERIAL,
 Article integer NOT NULL DEFAULT 0,
@@ -88,3 +95,23 @@
 Created TIMESTAMP NULL,
 PRIMARY KEY (id)
 );
+
+
+CREATE TABLE FM_Topics (
+id SERIAL,
+Parent integer NOT NULL DEFAULT 0,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+);
+
+
+CREATE TABLE FM_ObjectTopics (
+id SERIAL,
+Topic integer NOT NULL,
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+);

Modified: rtfm/trunk/etc/schema.mysql
==============================================================================
--- rtfm/trunk/etc/schema.mysql	(original)
+++ rtfm/trunk/etc/schema.mysql	Mon Jul 12 18:31:21 2004
@@ -38,6 +38,8 @@
 LastUpdated DATETIME NULL,
 PRIMARY KEY (id)
 ) TYPE=InnoDB;
+
+
 CREATE TABLE FM_Articles (
 id INTEGER NOT NULL AUTO_INCREMENT,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -52,6 +54,8 @@
 LastUpdated DATETIME NULL,
 PRIMARY KEY (id)
 ) TYPE=InnoDB;
+
+
 CREATE TABLE FM_CustomFieldValues (
 id INTEGER NOT NULL AUTO_INCREMENT,
 CustomField int NOT NULL,
@@ -64,6 +68,8 @@
 LastUpdated DATETIME NULL,
 PRIMARY KEY (id)
 ) TYPE=InnoDB;
+
+
 CREATE TABLE FM_ArticleCFValues (
 id INTEGER NOT NULL AUTO_INCREMENT,
 Article int NOT NULL,
@@ -74,8 +80,9 @@
 LastUpdatedBy integer NOT NULL DEFAULT 0,
 LastUpdated DATETIME NULL,
 PRIMARY KEY (id)
-
 ) TYPE=InnoDB;
+
+
 CREATE TABLE FM_Transactions (
 id INTEGER NOT NULL AUTO_INCREMENT,
 Article integer NOT NULL DEFAULT 0,
@@ -88,3 +95,23 @@
 Created DATETIME NULL,
 PRIMARY KEY (id)
 ) TYPE=InnoDB;
+
+
+CREATE TABLE FM_Topics (
+id INTEGER NOT NULL AUTO_INCREMENT,
+Parent integer NOT NULL DEFAULT 0,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+) TYPE=InnoDB;
+
+
+CREATE TABLE FM_ObjectTopics (
+id INTEGER NOT NULL AUTO_INCREMENT,
+Topic integer NOT NULL,
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+) TYPE=InnoDB;

Modified: rtfm/trunk/html/Callbacks/RTFM/autohandler/Default
==============================================================================
--- rtfm/trunk/html/Callbacks/RTFM/autohandler/Default	(original)
+++ rtfm/trunk/html/Callbacks/RTFM/autohandler/Default	Mon Jul 12 18:31:21 2004
@@ -21,6 +21,8 @@
 use RT::FM::ArticleCollection;
 use RT::FM::ClassCollection;
 use RT::FM::CustomFieldCollection;
+use RT::FM::TopicCollection;
+use RT::FM::ObjectTopicCollection;
 use Time::ParseDate;
 return(1);
 </%init>

Added: rtfm/trunk/html/RTFM/Admin/Classes/Topics.html
==============================================================================
--- (empty file)
+++ rtfm/trunk/html/RTFM/Admin/Classes/Topics.html	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,188 @@
+%# BEGIN LICENSE BLOCK
+%# 
+%#  Copyright (c) 2002-2003 Jesse Vincent <jesse at bestpractical.com>
+%#  
+%#  This program is free software; you can redistribute it and/or modify
+%#  it under the terms of version 2 of the GNU General Public License 
+%#  as published by the Free Software Foundation.
+%# 
+%#  A copy of that license should have arrived with this
+%#  software, but in any event can be snarfed from www.gnu.org.
+%# 
+%#  This program is distributed in the hope that it will be useful,
+%#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+%#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+%#  GNU General Public License for more details.
+%# 
+%# END LICENSE BLOCK
+
+<& /RTFM/Admin/Elements/ClassTabs, id => $ClassObj->id, Title => $title, current_tab => "RTFM/Admin/Classes/Topics.html?id=$id" &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="<%$RT::WebPath%>/RTFM/Admin/Classes/Topics.html" method="POST">
+<input type="hidden" name="id" value="<%$ClassObj->Id%>">
+
+% if (!$Modify) {
+<table>
+<tr>
+<td><&|/l&>Topic Name</&></td>
+<td><input type="text" name="Name" size="30" /></td>
+</tr>
+<tr>
+<td><&|/l&>Description</&></td>
+<td><input type="text" name="Description" size="50" /></td>
+</tr>
+</table>
+% } else {
+<a href="Topics.html">New topic</a>
+% }
+
+<& .tree, Element => $tree, Action => $Modify ? "Move" : "Add", Prefix => $Modify ? "Topic-$Modify-Parent" : "Insert", ClassObj => $ClassObj, Modify => $Modify &>
+
+</form>
+
+<%def .edit>
+&nbsp;
+<table style="margin-top: -0.75em">
+<tr><td>
+<input type="text" name="Topic-<%$topic->Id%>-Name" size="20" value="<%$topic->Name%>" /><br />
+<input type="text" name="Topic-<%$topic->Id%>-Description" size="20" value="<%$topic->Description%>" />
+</td><td>
+<input type="submit" name="Update" value="Update"><br />
+<input type="submit" name="Delete-Topic-<%$topic->Id%>" value="Delete" />
+</td></tr>
+</table>
+<%args>
+$topic
+</%args>
+</%def>
+
+<%def .tree>
+% my $topic = $Element->getNodeValue;
+% unless ($Element->isRoot) {
+%   if ($Modify and $topic->Id == $Modify) {
+%     $Action = "";
+<& .edit, topic => $topic &>
+%   } else {
+<a href="<%$RT::WebPath%>/RTFM/Admin/Classes/Topics.html?id=<%$ClassObj->Id%>&Modify=<%$topic->Id%>"
+   title="<%$topic->Description%>"><%$topic->Name || loc("(no name)") %></a>
+%   }
+% }
+<ul>
+% for my $e (sort {$a->getNodeValue->Name cmp $b->getNodeValue->Name} $Element->getAllChildren) {
+<li><& .tree, Element => $e, Action => $Action, Prefix => $Prefix, ClassObj => $ClassObj, Modify => $Modify &></li>
+% }
+% if ($Action) {
+<li><input type="submit" name="<%$Prefix%>-<%$topic eq "root" ? 0 : $topic->Id%>" value="<&|/l&><%$Action%> here</&>" /></li>
+% }
+</ul>
+<%args>
+$Element
+$Action
+$Prefix
+$ClassObj
+$Modify
+</%args>
+</%def>
+</%def>
+
+<%INIT>
+
+my $ClassObj = new RT::FM::Class($session{'CurrentUser'});
+$ClassObj->Load($ARGS{'id'}) || $m->comp("/RTFM/Elements/Error", Why => "Couldn't load class '$id'");
+
+my $title = $Modify 
+  ? loc("Modify topic for [_1]", $ClassObj->Name) 
+  : loc("Edit topic hierarchy for [_1]", $ClassObj->Name);
+
+my @results;
+
+for my $k (keys %ARGS) {
+    if ($k =~ /^Topic-(\d+)-(Name|Description)/) {
+        my $topic = RT::FM::Topic->new($session{'CurrentUser'});
+        $topic->Load($1);
+        if ($topic->Id) {
+            next if $ARGS{$k} eq $topic->$2;
+            my $proc = "Set$2";
+            my ($val, $msg) = $topic->$proc($ARGS{$k});
+            push @results, $msg;
+        } else {
+            $m->comp("/Elements/Error", Why => loc("Topic not found"));
+        }
+    } elsif ($k =~ /^Topic-(\d+)-Parent-(\d+)/) {
+        my $topic = RT::FM::Topic->new($session{'CurrentUser'});
+        $topic->Load($1);
+        if ($topic->Id) {
+            next if $2 eq $topic->Parent;
+            my $old = $topic->Parent;
+            my $new = "$2";
+            my ($val, $msg) = $topic->setParent($new);
+            push @results, $msg;
+        } else {
+            $m->comp("/Elements/Error", Why => loc("Topic not found"));
+        }
+    } elsif ($k =~ /^Insert-(\d+)/) {
+        my $topic = RT::FM::Topic->new($session{'CurrentUser'});
+        my ($id, $msg) = $topic->Create(
+                                        Parent => $1,
+                                        Name => $ARGS{'Name'},
+                                        Description => $ARGS{'Description'},
+                                        ObjectType => ref($ClassObj),
+                                        ObjectId => $ClassObj->Id,
+                                       );
+        push @results, $msg;
+    }
+}
+for my $k (keys %ARGS) {
+    next unless $k =~ /^Delete-Topic-(\d+)/;
+    my $topic = RT::FM::Topic->new($session{'CurrentUser'});
+    $topic->Load($1);
+    if ($topic->Id) {
+        my ($val, $msg) = $topic->DeleteAll();
+        push @results, $msg;
+    } else {
+        $m->comp("/Elements/Error", Why => loc("Topic not found"));
+    }
+}
+
+my $topics = new RT::FM::TopicCollection($session{'CurrentUser'});
+$topics->LimitToObject($ClassObj);
+$topics->OrderByCols({FIELD => 'Parent'}, {FIELD => 'id'});
+
+use Tree::Simple;
+my $tree = Tree::Simple->new(Tree::Simple->ROOT);
+my %lookup = (0 => $tree);
+
+my @todo;
+while (my $topic = $topics->Next) {
+    push @todo, $topic;
+}
+
+{
+    my $changed = 0;
+    my @work = @todo;
+    @todo = ();
+    for my $topic (@work) {
+        if (defined $lookup{$topic->Parent}) {
+            $lookup{$topic->Id} = Tree::Simple->new($topic, $lookup{$topic->Parent});
+            $changed = 1;
+        } else {
+            push @todo, $topic;
+        }
+    }
+    redo unless $changed == 0;
+}
+
+for my $topic (@todo) {
+    $topic->setParent(0);
+    $lookup{$topic->Id} = Tree::Simple->new($topic, $tree);
+    push @results, "Reparented orphan ".$topic->Id." to root";
+}
+
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+$Modify => ""
+</%ARGS>

Modified: rtfm/trunk/html/RTFM/Admin/Elements/ClassTabs
==============================================================================
--- rtfm/trunk/html/RTFM/Admin/Elements/ClassTabs	(original)
+++ rtfm/trunk/html/RTFM/Admin/Elements/ClassTabs	Mon Jul 12 18:31:21 2004
@@ -28,22 +28,24 @@
    $tabs = {
                  D => { title => loc('Basics'),
                         path => "RTFM/Admin/Classes/Modify.html?id=".$id,
-                           },
-                 
-                 E => { title => loc('Custom Fields'),
-                        path => 'RTFM/Admin/Classes/CustomFields.html?id='.$id,
-                        },
+                      },
+
+                 E => { title => loc('Topics'),
+                        path => "RTFM/Admin/Classes/Topics.html?id=".$id,
+                      },
 
-                 F => { title => loc('Group Rights'),
-                          path => "RTFM/Admin/Classes/GroupRights.html?id=".$id,
-                        },      
-                 G => { title => loc('User Rights'),
-                          path => "RTFM/Admin/Classes/UserRights.html?id=".$id,
-                        },
+                 F => { title => loc('Custom Fields'),
+                        path => 'RTFM/Admin/Classes/CustomFields.html?id='.$id,
+                      },
 
+                 G => { title => loc('Group Rights'),
+                        path => "RTFM/Admin/Classes/GroupRights.html?id=".$id,
+                      },      
 
-                 
-};
+                 H => { title => loc('User Rights'),
+                        path => "RTFM/Admin/Classes/UserRights.html?id=".$id,
+                      },
+           };
 }
 if ($session{'CurrentUser'}->HasRight( Object => $RT::FM::System, Right => 'AdminClass')) {
   $tabs->{"A"} = { title => loc('Select class'),

Modified: rtfm/trunk/html/RTFM/Article/Display.html
==============================================================================
--- rtfm/trunk/html/RTFM/Article/Display.html	(original)
+++ rtfm/trunk/html/RTFM/Article/Display.html	Mon Jul 12 18:31:21 2004
@@ -24,6 +24,8 @@
 <br><br><br>
   <& Elements/ShowCustomFields, article => $article &>
 <br>
+<& Elements/ShowTopics, article => $article &>
+<br>
 <& Elements/ShowLinks, article => $article &>
 <%init>
 

Modified: rtfm/trunk/html/RTFM/Article/Edit.html
==============================================================================
--- rtfm/trunk/html/RTFM/Article/Edit.html	(original)
+++ rtfm/trunk/html/RTFM/Article/Edit.html	Mon Jul 12 18:31:21 2004
@@ -33,14 +33,18 @@
 
 <& Elements/EditBasics, ArticleObj => $ArticleObj,
                         EditClass =>$EditClass,
-                        ClassObj =>$ClassObj,  
+                        ClassObj => $ClassObj,  
                         id => $id,
-                       %ARGS &>
+                        %ARGS &>
 <& Elements/EditCustomFields, ArticleObj => $ArticleObj, 
                               CFContent => \%CFContent, 
                               ClassObj => $ClassObj, 
                               id =>$id,
                               %ARGS &>
+<& Elements/EditTopics, ArticleObj => $ArticleObj,
+                        ClassObj => $ArticleObj->Id ? $ArticleObj->ClassObj : $ClassObj,
+                        id => $id,
+                        %ARGS &>
 <& Elements/EditLinks, ArticleObj => $ArticleObj, 
                         id => $id,
                        %ARGS &>
@@ -51,6 +55,10 @@
 </form>
 <%INIT>
 
+if (exists $ARGS{'Topics'}) {
+  $ARGS{'Topics'} = ref($ARGS{'Topics'}) ? $ARGS{'Topics'} : [$ARGS{'Topics'}]
+}
+
 my @results;
 my $title;
 
@@ -99,6 +107,7 @@
     ( $id, $msg ) = $ArticleObj->Create( Summary => $ARGS{'Summary'},
                                          Name    => $ARGS{'Name'},
                                          Class   => $ARGS{'Class'},
+                                         Topics  => $ARGS{'Topics'},
                                          %create_args );
         push ( @results, $msg );
     if ($id ) {
@@ -261,6 +270,26 @@
 
     }
 
+    my %topics;
+    if ($ARGS{'EditTopics'}) {
+        $topics{$_}++ for @{$ARGS{'Topics'}};
+        my $objTopics = new RT::FM::ObjectTopicCollection($session{'CurrentUser'});
+        $objTopics->LimitToObject($ArticleObj);
+        while (my $t = $objTopics->Next) {
+            $topics{$t->Topic}--;
+        }
+        for my $id (keys %topics) {
+            if ($topics{$id} > 0) {
+                my ($val, $msg) = $ArticleObj->AddTopic(Topic => $id);
+                push @results, $msg;
+            } elsif ($topics{$id} < 0) {
+                my ($val, $msg) = $ArticleObj->DeleteTopic(Topic => $id);
+                push @results, $msg;
+            }
+        }
+    }
+    
+
     $title = loc( 'Modify article #[_1]', $ArticleObj->Id );
   }
 

Added: rtfm/trunk/html/RTFM/Article/Elements/EditTopics
==============================================================================
--- (empty file)
+++ rtfm/trunk/html/RTFM/Article/Elements/EditTopics	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,109 @@
+%# BEGIN LICENSE BLOCK
+%# 
+%#  Copyright (c) 2002-2003 Jesse Vincent <jesse at bestpractical.com>
+%#  
+%#  This program is free software; you can redistribute it and/or modify
+%#  it under the terms of version 2 of the GNU General Public License 
+%#  as published by the Free Software Foundation.
+%# 
+%#  A copy of that license should have arrived with this
+%#  software, but in any event can be snarfed from www.gnu.org.
+%# 
+%#  This program is distributed in the hope that it will be useful,
+%#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+%#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+%#  GNU General Public License for more details.
+%# 
+%# END LICENSE BLOCK
+
+<table>
+<tr>
+<td>Fields in this class:</td>
+<td>
+
+<input type="hidden" name="EditTopics" value="1" />
+<select multiple size="10" name="Topics">
+  <optgroup label="Current class (<% $ClassObj->Name%>)"> 
+% $inTree->traverse(sub {
+%   my $tree = shift;
+%   my $topic = $tree->getNodeValue;
+%   $m->print("<option value=\"".$topic->Id."\""
+%     .(exists $topics{$topic->Id} ? " selected" : "").">"
+%     .("&nbsp;" x ($tree->getDepth*5)).$topic->Name."</option>\n");
+% });
+% my $class = $ClassObj->Id;
+% $otherTree->traverse(sub {
+%   my $tree = shift;
+%   my $topic = $tree->getNodeValue;
+%   if ($topic->ObjectId != $class) {
+%     $class = $topic->ObjectId;
+%     $m->print("</optgroup>\n");
+%     my $c = new RT::FM::Class($session{'CurrentUser'});
+%     $c->Load($topic->ObjectId);
+%     $m->print("<optgroup label=\"".$c->Name."\">\n");
+%   }
+%   $m->print("<option value=\"".$topic->Id."\""
+%     .(exists $topics{$topic->Id} ? " selected" : "").">"
+%     .("&nbsp;" x ($tree->getDepth*5)).$topic->Name."</option>\n");
+% });
+</optgroup>
+</select>
+</td>
+</tr>
+
+<%INIT>
+use Tree::Simple;
+
+my $inClass = new RT::FM::TopicCollection($session{'CurrentUser'});
+$inClass->LimitToObject($ClassObj);
+$inClass->OrderByCols({FIELD => 'Name'});
+my $inTree = buildTree($inClass);
+
+my $otherClass = new RT::FM::TopicCollection($session{'CurrentUser'});
+$otherClass->Limit(FIELD => 'ObjectType', VALUE => 'RT::FM::Class');
+$otherClass->Limit(FIELD => 'ObjectId', OPERATOR => '!=', VALUE => $ClassObj->Id);
+my $otherTree = buildTree($otherClass);
+
+my $articleTopics = new RT::FM::ObjectTopicCollection($session{'CurrentUser'});
+$articleTopics->LimitToObject($ArticleObj);
+my %topics;
+while (my $topicObj = $articleTopics->Next) {
+    $topics{$topicObj->Topic} = $topicObj;
+}
+
+
+sub buildTree {
+    my $query = shift;
+    
+    use Tree::Simple;
+    my $tree = Tree::Simple->new(Tree::Simple->ROOT);
+    my %lookup = (0 => $tree);
+
+    my @todo;
+    while (my $topic = $query->Next) {
+        push @todo, $topic;
+    }
+
+    {
+        my $changed = 0;
+        my @work = @todo;
+        @todo = ();
+        for my $topic (@work) {
+            if (defined $lookup{$topic->Parent}) {
+                $lookup{$topic->Id} = Tree::Simple->new($topic, $lookup{$topic->Parent});
+                $changed = 1;
+            } else {
+                push @todo, $topic;
+            }
+        }
+        redo unless $changed == 0;
+    }
+    return $tree;
+}
+
+</%INIT>
+<%ARGS>
+$id => undef
+$ArticleObj => undef
+$ClassObj => undef
+</%ARGS>

Added: rtfm/trunk/html/RTFM/Article/Elements/ShowTopics
==============================================================================
--- (empty file)
+++ rtfm/trunk/html/RTFM/Article/Elements/ShowTopics	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,44 @@
+%# BEGIN LICENSE BLOCK
+%# 
+%#  Copyright (c) 2002-2003 Jesse Vincent <jesse at bestpractical.com>
+%#  
+%#  This program is free software; you can redistribute it and/or modify
+%#  it under the terms of version 2 of the GNU General Public License 
+%#  as published by the Free Software Foundation.
+%# 
+%#  A copy of that license should have arrived with this
+%#  software, but in any event can be snarfed from www.gnu.org.
+%# 
+%#  This program is distributed in the hope that it will be useful,
+%#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+%#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+%#  GNU General Public License for more details.
+%# 
+%# END LICENSE BLOCK
+
+Topics:
+% my $topics = new RT::FM::ObjectTopicCollection($session{'CurrentUser'});
+% $topics->LimitToObject($article);
+% if ($topics->Count) {
+<ul>
+% my @topics;
+% while (my $t = $topics->Next) {
+%   my @path;
+%   my $t = $t->TopicObj;
+%   while ($t->Id) {
+%     unshift @path, $t->Name;
+%     $t = $t->ParentObj;
+%   }
+%   push @topics, join(" : ", @path);
+% }
+% for (sort @topics) {
+<li><% $_ %></li>
+% }
+</ul>
+% } else {
+<i>no topics</i>
+% }
+
+<%args>
+$article => undef
+</%args>

Modified: rtfm/trunk/html/RTFM/Article/PreCreate.html
==============================================================================
--- rtfm/trunk/html/RTFM/Article/PreCreate.html	(original)
+++ rtfm/trunk/html/RTFM/Article/PreCreate.html	Mon Jul 12 18:31:21 2004
@@ -16,14 +16,13 @@
 %# 
 %# END LICENSE BLOCK
 
-<& /RTFM/Article/Elements/Tabs, current_tab => "RTFM/Article/PreCreate.html",
+<& /RTFM/Article/Elements/Tabs, current_tab => "RTFM/Topics.html",
 Title => loc('Create an article in class...') &>
 <ul>                                                                            
 % my $Classes = RT::FM::ClassCollection->new($session{'CurrentUser'});          
 % $Classes->UnLimit;                                                            
 % while (my $Class = $Classes->Next) {                                          
-<li><a href="Edit.html?Class=<%$Class->Id%>"><&|/l, $Class->Name&>in class [_1]</&>
-</a></li>                                                                       
+<li><a href="Edit.html?Class=<%$Class->Id%>"><&|/l, $Class->Name&>in class [_1]</&></a></li>
 % }                                                                             
                                                                                 
 </ul>                                                                           

Modified: rtfm/trunk/html/RTFM/Elements/Tabs
==============================================================================
--- rtfm/trunk/html/RTFM/Elements/Tabs	(original)
+++ rtfm/trunk/html/RTFM/Elements/Tabs	Mon Jul 12 18:31:21 2004
@@ -39,12 +39,14 @@
                 };
 
 my $second_tabs = { 'aab' => { title => loc('Overview'),
-                                               path  => 'RTFM/index.html' },
-                                      'articles' => { title => loc('Articles'),
-                                              path => 'RTFM/Article/Search.html' },
-                                      'd' => { title => loc('Configuration'),
-                                               path  => 'RTFM/Admin/index.html' } 
-                                 };
+                               path  => 'RTFM/index.html' },
+                    'articles' => { title => loc('Articles'),
+                                    path => 'RTFM/Article/Search.html' },
+                    'atopics' => { title => loc('Topics'),
+                                   path => 'RTFM/Topics.html'},
+                    'd' => { title => loc('Configuration'),
+                             path  => 'RTFM/Admin/index.html' } 
+                  };
 
 my $topactions = {
         B => { html => $m->scomp('/RTFM/Elements/GotoArticle') }

Added: rtfm/trunk/html/RTFM/Topics.html
==============================================================================
--- (empty file)
+++ rtfm/trunk/html/RTFM/Topics.html	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,89 @@
+%# BEGIN LICENSE BLOCK
+%# 
+%#  Copyright (c) 2002-2003 Jesse Vincent <jesse at bestpractical.com>
+%#  
+%#  This program is free software; you can redistribute it and/or modify
+%#  it under the terms of version 2 of the GNU General Public License 
+%#  as published by the Free Software Foundation.
+%# 
+%#  A copy of that license should have arrived with this
+%#  software, but in any event can be snarfed from www.gnu.org.
+%# 
+%#  This program is distributed in the hope that it will be useful,
+%#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+%#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+%#  GNU General Public License for more details.
+%# 
+%# END LICENSE BLOCK
+
+<& /RTFM/Elements/Tabs, current_toptab => "RTFM/Topics.html", Title => loc('Topic hierarchy') &>
+
+% if (defined $class) {
+
+% if ($id != 0) {
+% my $t = RT::FM::Topic->new($session{'CurrentUser'});
+% $t->Load($id);
+<a href="Topics.html?class=<% $class %>&id=<% $t->Parent %>">Parent</a><br />
+% }
+
+<& /Elements/TitleBoxStart, title => loc('Topics') &>
+% my $Topics = RT::FM::TopicCollection->new($session{'CurrentUser'});
+% $Topics->Limit(FIELD => 'ObjectType', VALUE => 'RT::FM::Class');
+% $Topics->Limit(FIELD => 'ObjectId',   VALUE => $class);
+% $Topics->Limit(FIELD => 'Parent',     VALUE => $id);
+% if ($Topics->Count) {
+<ul>
+% while (my $t = $Topics->Next) {
+<li><a href="Topics.html?class=<% $class %>&id=<% $t->Id %>"><% $t->Name %></a></li>
+% }
+</ul>
+% } else {
+<i>No sub-topics</i>
+% }
+<& /Elements/TitleBoxEnd &>
+
+<br />
+
+% if ($id != 0) {
+<& /Elements/TitleBoxStart, title => loc('Articles') &>
+% my $Articles = RT::FM::ObjectTopicCollection->new($session{'CurrentUser'});
+% $Articles->Limit(FIELD => 'Topic',      VALUE => $id);
+% $Articles->Limit(FIELD => 'ObjectType', VALUE => 'RT::FM::Article');
+% if ($Articles->Count) {
+<ul>
+% while (my $obj = $Articles->Next) {
+%   my $a = RT::FM::Article->new($session{'CurrentUser'});
+%   $a->Load($obj->ObjectId);
+<li><a href="Article/Display.html?id=<% $a->Id %>"><% $a->Name %></a></li>
+% }
+</ul>
+% } else {
+<i>No articles in this topic</i>
+% }
+<& /Elements/TitleBoxEnd &>
+% }
+
+% } else {
+Choose a class to examine:
+<ul>
+% my $Classes = RT::FM::ClassCollection->new($session{'CurrentUser'});
+% $Classes->UnLimit;
+% while (my $c = $Classes->Next) {
+<li><a href="Topics.html?class=<%$c->Id%>"><% $c->Name %></li>
+% }
+</ul>
+
+% }
+
+<%init>
+my $Classes;
+if (defined $class) {
+    
+} else {
+}
+</%init>
+
+<%args>
+$id => 0
+$class => undef
+</%args>

Modified: rtfm/trunk/lib/RT/FM/ArticleCollection_Overlay.pm
==============================================================================
--- rtfm/trunk/lib/RT/FM/ArticleCollection_Overlay.pm	(original)
+++ rtfm/trunk/lib/RT/FM/ArticleCollection_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -200,6 +200,7 @@
 
 
 # {{{ LimitToParent ID
+
 =item LimitToParent ID
 
 Limit the returned set of articles to articles which are children

Modified: rtfm/trunk/lib/RT/FM/Article_Overlay.pm
==============================================================================
--- rtfm/trunk/lib/RT/FM/Article_Overlay.pm	(original)
+++ rtfm/trunk/lib/RT/FM/Article_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -91,9 +91,6 @@
                  Links        => {},
                  @_ );
 
-    use Data::Dumper;
-    $RT::Logger->crit(Dumper \%args);
-
     my $class = RT::FM::Class->new($RT::SystemUser);
     $class->Load( $args{'Class'} );
     unless ( $class->Id ) {
@@ -139,6 +136,18 @@
     }
 
     # }}}
+    # {{{ Add topics
+
+    foreach my $topic (@{$args{Topics}}) {
+        my ( $cfid, $cfmsg ) = $self->AddTopic(Topic => $topic);
+        
+        unless ($cfid) {
+            $RT::Handle->Rollback();
+            return ( undef, $cfmsg );
+        }
+    }
+
+    # }}}
     # {{{ Add relationships
 
     foreach my $type ( keys %args ) {
@@ -291,6 +300,7 @@
     my $linksfrom = $self->_Links( Field => 'Base' );
     my $cfvalues = $self->CustomFieldValues;
     my $txns     = $self->Transactions;
+    my $topics   = $self->Topics;
 
     while ( my $item = $linksto->Next ) {
         my ( $val, $msg ) = $item->Delete();
@@ -309,6 +319,7 @@
             return ( 0, $self->loc('Internal Error') );
         }
     }
+
     while ( my $item = $txns->Next ) {
         my ( $val, $msg ) = $item->Delete();
         unless ($val) {
@@ -327,6 +338,15 @@
         }
     }
 
+    while (my $item = $topics->Next) {
+        my ($val, $msg ) = $item->Delete();
+        unless ($val) {
+            $RT::Logger->crit( ref($item) . ": $msg" );
+            $RT::Handle->Rollback();
+            return ( 0, $self->loc('Internal Error') );
+        }
+    }
+
     $self->SUPER::Delete();
     $RT::Handle->Commit();
     return ( 1, $self->loc('Article Deleted') );
@@ -1062,9 +1082,9 @@
 # {{{ DeleteCustomFieldValue
 
 =item DeleteCustomFieldValue { CustomField => undef, Content => undef } 
-  
-  Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this
-  ticket's object keywords.
+
+Takes a paramhash. Deletes the Keyword denoted by the I<Keyword>
+parameter from this ticket's object keywords.
 
 =cut
 
@@ -1130,6 +1150,70 @@
 
 # }}}
 
+# {{{ Topics
+
+# {{{ Topics
+sub Topics {
+    my $self = shift;
+    
+    my $topics = new RT::FM::ObjectTopicCollection($self->CurrentUser);
+    if ($self->CurrentUserHasRight('ShowArticle')) {
+        $topics->LimitToObject($self);
+    }
+    return $topics;
+}
+# }}}
+
+# {{{ AddTopic
+sub AddTopic {
+    my $self = shift;
+    my %args = ( @_ );
+    
+    unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my $t = new RT::FM::ObjectTopic($self->CurrentUser);
+    my ($tid) = $t->Create( Topic      => $args{'Topic'},
+                            ObjectType => ref($self),
+                            ObjectId   => $self->Id );
+    if ($tid) {
+        return ($tid, $self->loc("Topic membership added"));
+    } else {
+        return (0, $self->loc("Unable to add topic membership"));
+    } 
+}
+# }}}
+
+# {{{ DeleteTopic
+sub DeleteTopic {
+    my $self = shift;
+    my %args = ( @_ );
+
+    unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my $t = new RT::FM::ObjectTopic($self->CurrentUser);
+    $t->LoadByCols(Topic => $args{'Topic'}, ObjectId => $self->Id, ObjectType => ref($self));
+    if ($t->Id) {
+        my $del = $t->Delete;
+        unless ($del) {
+            return ( undef, 
+                     $self->loc("Unable to delete topic membership in [_1]",
+                                $t->TopicObj->Name));
+        } else {
+            return ( 1,
+                     $self->loc("Topic membership removed"));
+        }
+    } else {
+        return ( undef,
+                 $self->loc("Couldn't load topic membership while trying to delete it"));
+    }
+}
+# }}}
+# }}}
+
 # {{{ CurrentUserHasRight
 
 =head2 CurrentUserHasRight

Modified: rtfm/trunk/lib/RT/FM/Class_Overlay.pm
==============================================================================
--- rtfm/trunk/lib/RT/FM/Class_Overlay.pm	(original)
+++ rtfm/trunk/lib/RT/FM/Class_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -52,12 +52,14 @@
 
     SeeClass            => 'See that this class exists', #loc_pair
     CreateArticle       => 'Create articles in this class', #loc_pair
-    ShowArticle       => 'See articles in this class', #loc_pair
-    ShowArticleHistory       => 'See articles in this class', #loc_pair
+    ShowArticle         => 'See articles in this class', #loc_pair
+    ShowArticleHistory  => 'See articles in this class', #loc_pair
     ModifyArticle       => 'Modify or delete articles in this class', #loc_pair
+    ModifyArticleTopics => 'Modify topics for articles in this class', #loc_pair
     AdminClass          => 'Modify metadata and custom fields for this class', #loc_pair
-    ShowACL             => 'Display Access Control List',             # loc_pair
-    ModifyACL           => 'Modify Access Control List',              # loc_pair
+    AdminTopics         => 'Modify topic hierarchy associated with this class', #loc_pair
+    ShowACL             => 'Display Access Control List', # loc_pair
+    ModifyACL           => 'Modify Access Control List', # loc_pair
 };
 
 # TODO: This should be refactored out into an RT::ACLedObject or something

Added: rtfm/trunk/lib/RT/FM/ObjectTopic.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/ObjectTopic.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,199 @@
+
+
+
+# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
+# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
+# 
+# !! DO NOT EDIT THIS FILE !!
+#
+
+
+=head1 NAME
+
+RT::FM::ObjectTopic
+
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=cut
+
+package RT::FM::ObjectTopic;
+use RT::FM::Record; 
+use RT::FM::Topic;
+
+
+use base qw( RT::FM::Record );
+
+sub _Init {
+  my $self = shift; 
+
+  $self->Table('FM_ObjectTopics');
+  $self->SUPER::_Init(@_);
+}
+
+
+
+
+
+=item Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+  int(11) 'Topic'.
+  varchar(64) 'ObjectType'.
+  int(11) 'ObjectId'.
+
+=cut
+
+
+
+
+sub Create {
+    my $self = shift;
+    my %args = ( 
+                Topic => '0',
+                ObjectType => '',
+                ObjectId => '0',
+
+		  @_);
+    $self->SUPER::Create(
+                         Topic => $args{'Topic'},
+                         ObjectType => $args{'ObjectType'},
+                         ObjectId => $args{'ObjectId'},
+);
+
+}
+
+
+
+=item id
+
+Returns the current value of id. 
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=item Topic
+
+Returns the current value of Topic. 
+(In the database, Topic is stored as int(11).)
+
+
+
+=item SetTopic VALUE
+
+
+Set Topic to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Topic will be stored as a int(11).)
+
+
+=cut
+
+
+=item TopicObj
+
+Returns the Topic Object which has the id returned by Topic
+
+
+=cut
+
+sub TopicObj {
+	my $self = shift;
+	my $Topic =  RT::FM::Topic->new($self->CurrentUser);
+	$Topic->Load($self->Topic());
+	return($Topic);
+}
+
+=item ObjectType
+
+Returns the current value of ObjectType. 
+(In the database, ObjectType is stored as varchar(64).)
+
+
+
+=item SetObjectType VALUE
+
+
+Set ObjectType to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ObjectType will be stored as a varchar(64).)
+
+
+=cut
+
+
+=item ObjectId
+
+Returns the current value of ObjectId. 
+(In the database, ObjectId is stored as int(11).)
+
+
+
+=item SetObjectId VALUE
+
+
+Set ObjectId to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ObjectId will be stored as a int(11).)
+
+
+=cut
+
+
+
+sub _CoreAccessible {
+    {
+     
+        id =>
+		{read => 1, type => 'int(11)', default => ''},
+        Topic => 
+		{read => 1, write => 1, type => 'int(11)', default => '0'},
+        ObjectType => 
+		{read => 1, write => 1, type => 'varchar(64)', default => ''},
+        ObjectId => 
+		{read => 1, write => 1, type => 'int(11)', default => '0'},
+
+ }
+};
+
+
+        eval "require RT::FM::ObjectTopic_Overlay";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+        eval "require RT::FM::ObjectTopic_Local";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+
+
+
+=head1 SEE ALSO
+
+This class allows "overlay" methods to be placed
+into the following files _Overlay is for a System overlay by the original author,
+while _Local is for site-local customizations.  
+
+These overlay files can contain new subs or subs to replace existing subs in this module.
+
+If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line 
+
+   no warnings qw(redefine);
+
+so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
+
+RT::FM::ObjectTopic_Overlay, RT::FM::ObjectTopic_Local
+
+=cut
+
+
+1;

Added: rtfm/trunk/lib/RT/FM/ObjectTopicCollection.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/ObjectTopicCollection.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,85 @@
+
+# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
+# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
+# 
+# !! DO NOT EDIT THIS FILE !!
+#
+
+
+=head1 NAME
+
+  RT::FM::ObjectTopicCollection -- Class Description
+ 
+=head1 SYNOPSIS
+
+  use RT::FM::ObjectTopicCollection
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=cut
+
+package RT::FM::ObjectTopicCollection;
+
+use RT::FM::SearchBuilder;
+use RT::FM::ObjectTopic;
+
+use base qw(RT::FM::SearchBuilder);
+
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = 'FM_ObjectTopics';
+    $self->{'primary_key'} = 'id';
+
+
+    return ( $self->SUPER::_Init(@_) );
+}
+
+
+=item NewItem
+
+Returns an empty new RT::FM::ObjectTopic item
+
+=cut
+
+sub NewItem {
+    my $self = shift;
+    return(RT::FM::ObjectTopic->new($self->CurrentUser));
+}
+
+        eval "require RT::FM::ObjectTopicCollection_Overlay";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+        eval "require RT::FM::ObjectTopicCollection_Local";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+
+
+
+=head1 SEE ALSO
+
+This class allows "overlay" methods to be placed
+into the following files _Overlay is for a System overlay by the original author,
+while _Local is for site-local customizations.  
+
+These overlay files can contain new subs or subs to replace existing subs in this module.
+
+If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line 
+
+   no warnings qw(redefine);
+
+so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
+
+RT::FM::ObjectTopicCollection_Overlay, RT::FM::ObjectTopicCollection_Local
+
+=cut
+
+
+1;

Added: rtfm/trunk/lib/RT/FM/ObjectTopicCollection_Overlay.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/ObjectTopicCollection_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,68 @@
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse at bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
+use strict;
+no warnings qw(redefine);
+
+# {{{ LimitToTopic
+
+=head2 LimitToTopic FIELD
+
+Returns values for the topic with Id FIELD
+
+=cut
+  
+sub LimitToTopic {
+    my $self = shift;
+    my $cf = shift;
+    return ($self->Limit( FIELD => 'Topic',
+			  VALUE => $cf,
+			  OPERATOR => '='));
+
+}
+
+# }}}
+
+
+# {{{ LimitToObject
+
+=head2 LimitToObject OBJ
+
+Returns associations for the given OBJ only
+
+=cut
+
+sub LimitToObject {
+    my $self = shift;
+    my $object = shift;
+
+    $self->Limit( FIELD => 'ObjectType',
+		  VALUE => ref($object));
+    $self->Limit( FIELD => 'ObjectId',
+                  VALUE => $object->Id);
+
+}
+
+# }}}
+
+1;

Added: rtfm/trunk/lib/RT/FM/Topic.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/Topic.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,230 @@
+
+
+
+# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
+# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
+# 
+# !! DO NOT EDIT THIS FILE !!
+#
+
+
+=head1 NAME
+
+RT::FM::Topic
+
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=cut
+
+package RT::FM::Topic;
+use RT::FM::Record; 
+
+
+use base qw( RT::FM::Record );
+
+sub _Init {
+  my $self = shift; 
+
+  $self->Table('FM_Topics');
+  $self->SUPER::_Init(@_);
+}
+
+
+
+
+
+=item Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+  int(11) 'Parent'.
+  varchar(255) 'Name'.
+  varchar(255) 'Description'.
+  varchar(64) 'ObjectType'.
+  int(11) 'ObjectId'.
+
+=cut
+
+
+
+
+sub Create {
+    my $self = shift;
+    my %args = ( 
+                Parent => '',
+                Name => '',
+                Description => '',
+                ObjectType => '',
+                ObjectId => '0',
+
+		  @_);
+    $self->SUPER::Create(
+                         Parent => $args{'Parent'},
+                         Name => $args{'Name'},
+                         Description => $args{'Description'},
+                         ObjectType => $args{'ObjectType'},
+                         ObjectId => $args{'ObjectId'},
+);
+
+}
+
+
+
+=item id
+
+Returns the current value of id. 
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=item Parent
+
+Returns the current value of Parent. 
+(In the database, Parent is stored as int(11).)
+
+
+
+=item SetParent VALUE
+
+
+Set Parent to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Parent will be stored as a int(11).)
+
+
+=cut
+
+
+=item Name
+
+Returns the current value of Name. 
+(In the database, Name is stored as varchar(255).)
+
+
+
+=item SetName VALUE
+
+
+Set Name to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Name will be stored as a varchar(255).)
+
+
+=cut
+
+
+=item Description
+
+Returns the current value of Description. 
+(In the database, Description is stored as varchar(255).)
+
+
+
+=item SetDescription VALUE
+
+
+Set Description to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Description will be stored as a varchar(255).)
+
+
+=cut
+
+
+=item ObjectType
+
+Returns the current value of ObjectType. 
+(In the database, ObjectType is stored as varchar(64).)
+
+
+
+=item SetObjectType VALUE
+
+
+Set ObjectType to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ObjectType will be stored as a varchar(64).)
+
+
+=cut
+
+
+=item ObjectId
+
+Returns the current value of ObjectId. 
+(In the database, ObjectId is stored as int(11).)
+
+
+
+=item SetObjectId VALUE
+
+
+Set ObjectId to VALUE. 
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, ObjectId will be stored as a int(11).)
+
+
+=cut
+
+
+
+sub _CoreAccessible {
+    {
+     
+        id =>
+		{read => 1, type => 'int(11)', default => ''},
+        Parent => 
+		{read => 1, write => 1, type => 'int(11)', default => ''},
+        Name => 
+		{read => 1, write => 1, type => 'varchar(255)', default => ''},
+        Description => 
+		{read => 1, write => 1, type => 'varchar(255)', default => ''},
+        ObjectType => 
+		{read => 1, write => 1, type => 'varchar(64)', default => ''},
+        ObjectId => 
+		{read => 1, write => 1, type => 'int(11)', default => '0'},
+
+ }
+};
+
+
+        eval "require RT::FM::Topic_Overlay";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+        eval "require RT::FM::Topic_Local";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+
+
+
+=head1 SEE ALSO
+
+This class allows "overlay" methods to be placed
+into the following files _Overlay is for a System overlay by the original author,
+while _Local is for site-local customizations.  
+
+These overlay files can contain new subs or subs to replace existing subs in this module.
+
+If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line 
+
+   no warnings qw(redefine);
+
+so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
+
+RT::FM::Topic_Overlay, RT::FM::Topic_Local
+
+=cut
+
+
+1;

Added: rtfm/trunk/lib/RT/FM/TopicCollection.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/TopicCollection.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,85 @@
+
+# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
+# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
+# 
+# !! DO NOT EDIT THIS FILE !!
+#
+
+
+=head1 NAME
+
+  RT::FM::TopicCollection -- Class Description
+ 
+=head1 SYNOPSIS
+
+  use RT::FM::TopicCollection
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=cut
+
+package RT::FM::TopicCollection;
+
+use RT::FM::SearchBuilder;
+use RT::FM::Topic;
+
+use base qw(RT::FM::SearchBuilder);
+
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = 'FM_Topics';
+    $self->{'primary_key'} = 'id';
+
+
+    return ( $self->SUPER::_Init(@_) );
+}
+
+
+=item NewItem
+
+Returns an empty new RT::FM::Topic item
+
+=cut
+
+sub NewItem {
+    my $self = shift;
+    return(RT::FM::Topic->new($self->CurrentUser));
+}
+
+        eval "require RT::FM::TopicCollection_Overlay";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+        eval "require RT::FM::TopicCollection_Local";
+        if ($@ && $@ !~ /^Can't locate/) {
+            die $@;
+        };
+
+
+
+
+=head1 SEE ALSO
+
+This class allows "overlay" methods to be placed
+into the following files _Overlay is for a System overlay by the original author,
+while _Local is for site-local customizations.  
+
+These overlay files can contain new subs or subs to replace existing subs in this module.
+
+If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line 
+
+   no warnings qw(redefine);
+
+so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
+
+RT::FM::TopicCollection_Overlay, RT::FM::TopicCollection_Local
+
+=cut
+
+
+1;

Added: rtfm/trunk/lib/RT/FM/TopicCollection_Overlay.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/TopicCollection_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,67 @@
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse at bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
+use strict;
+no warnings qw(redefine);
+
+# {{{ LimitToObject
+
+=head2 LimitToObject OBJ
+
+Find all Topics hung off of the given Object
+
+=cut
+
+sub LimitToObject {
+    my $self = shift;
+    my $object = shift;
+    
+    $self->Limit(FIELD => 'ObjectId',
+                 VALUE => $object->Id);
+    $self->Limit(FIELD => 'ObjectType',
+                 VALUE => ref($object));
+}
+
+# }}}
+
+
+# {{{ LimitToKids
+
+=head2 LimitToKids TOPIC
+
+Find all Topics which are immediate children of Id TOPIC.  Note this
+does not do the recursive query of their kids, etc.
+
+=cut
+
+sub LimitToKids {
+    my $self = shift;
+    my $topic = shift;
+    
+    $self->Limit(FIELD => 'Parent',
+                 VALUE => $topic);
+}
+
+# }}}
+
+1;

Added: rtfm/trunk/lib/RT/FM/Topic_Overlay.pm
==============================================================================
--- (empty file)
+++ rtfm/trunk/lib/RT/FM/Topic_Overlay.pm	Mon Jul 12 18:31:21 2004
@@ -0,0 +1,189 @@
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse at bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
+use strict;
+no warnings qw(redefine);
+
+# {{{ Create
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+  int(11) 'Parent'.
+  varchar(255) 'Name'.
+  varchar(255) 'Description'.
+  varchar(64) 'ObjectType'.
+  int(11) 'ObjectId'.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    
+    unless ( $self->CurrentUserHasRight('AdminTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    $self->SUPER::Create(@_);
+}
+
+# }}}
+
+
+# {{{ Delete
+
+=head2 Delete
+
+Deletes this topic, reparenting all sub-topics to this one's parent.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    
+    unless ( $self->CurrentUserHasRight('AdminTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    my $kids = new RT::FM::TopicCollection($self->CurrentUser);
+    $kids->LimitToKids($self->Id);
+    while (my $topic = $kids->Next) {
+        $topic->setParent($self->Parent);
+    }
+
+    $self->SUPER::Delete(@_);
+    return (0, "Topic deleted");
+}
+
+# }}}
+
+
+# {{{ DeleteAll
+
+=head2 DeleteAll
+
+Deletes this topic, and all of its descendants.
+
+=cut
+
+sub DeleteAll {
+    my $self = shift;
+    
+    unless ( $self->CurrentUserHasRight('AdminTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+
+    $self->SUPER::Delete(@_);
+    my $kids = new RT::FM::TopicCollection($self->CurrentUser);
+    $kids->LimitToKids($self->Id);
+    while (my $topic = $kids->Next) {
+        $topic->DeleteAll;
+    }
+
+    return (0, "Topic tree deleted");
+}
+
+# }}}
+
+
+# {{{ ParentObj
+
+=head2 ParentObj
+
+Returns the parent Topic of this one.
+
+=cut
+
+sub ParentObj {
+  my $self = shift;
+  my $id = $self->Parent;
+  my $obj = new RT::FM::Topic($self->CurrentUser);
+  $obj->Load($id);
+  return $obj;
+}
+
+# }}}
+
+
+# {{{ _Set
+
+=head2 _Set
+
+Intercept attempts to modify the Topic so we can apply ACLs
+
+=cut
+
+sub _Set {
+    my $self = shift;
+    
+    unless ( $self->CurrentUserHasRight('AdminTopics') ) {
+        return ( 0, $self->loc("Permission Denied") );
+    }
+    $self->SUPER::_Set(@_);
+}
+
+# }}}
+
+
+# {{{ CurrentUserHasRight
+
+=head2 CurrentUserHasRight
+
+Returns true if the current user has the right for this topic, for the
+whole system or for whatever object this topic is associated with
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self  = shift;
+    my $right = shift;
+
+    my $equiv = [ $RT::FM::System ];
+    if ($self->ObjectId) {
+        my $obj = $self->ObjectType->new($self->CurrentUser);
+        $obj->Load($self->ObjectId);
+        push @{$equiv}, $obj;
+    }
+    if ($self->Id) {
+        return ( $self->CurrentUser->HasRight(
+                                              Right        => $right,
+                                              Object       => $self,
+                                              EquivObjects => $equiv,
+                                             ) );
+    } else {
+        # If we don't have an ID, we don't even know what object we're
+        # attached to -- so the only thing we can fall back on is the
+        # system object.
+        return ( $self->CurrentUser->HasRight(
+                                              Right        => $right,
+                                              Object       => $RT::FM::System,
+                                             ) );
+    }
+    
+
+}
+
+# }}}
+
+1;


More information about the Rt-commit mailing list