Software Freedom Law Center

root/trunk/antimatter/tim/Modules/SFLC/TimeTracker/DB.pm

Revision 119, 36.8 kB (checked in by bkuhn, 2 months ago)

r158@hughes: bkuhn | 2008-06-02 13:25:16 -0400

  • I realized that there was not a proper create database script. I had
    started a create database function in DB.pm, but it was incomplete and
    generally only used for testing when you wanted to clear out the users.


Now that function properly creates a new empty database, and there is
a create-db.plx script as well that simply calls it.

Line 
1 # DB.pm                                                          -*- Perl -*-
2 #  Database module for SFLC time tracker
3 # Copyright (C) 2005, 2006, 2007   Software Freedom Law Center, Inc.
4 #  Author: Bradley M. Kuhn <bkuhn@softwarefreedom.org>
5 #
6 #  This software gives you freedom; it is licensed to you under version
7 #  3 of the GNU Affero General Public License, along with the
8 #  additional permission in the following paragraph.
9 #
10 #  This software is distributed WITHOUT ANY WARRANTY, without even the
11 #  implied warranties of MERCHANTABILITY and FITNESS FOR A PARTICULAR
12 #  PURPOSE.  See the GNU Affero General Public License for further
13 #  details.
14 #
15 # You should have received a copy of the GNU Affero General Public
16 # License, version 3 along with this software.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19
20 sub UnLock {}
21 sub Lock {}
22 sub ReadLock {}
23
24 package SFLC::TimeTracker::DB;
25
26 use strict;
27 use warnings;
28
29
30 require Exporter;
31 #use AutoLoader qw(AUTOLOAD);
32
33 our @ISA = qw(Exporter);
34
35 our @EXPORT_OK = ();
36
37 our @EXPORT = qw( );
38
39 our $VERSION = '0.01';
40
41 # FIXME: STOP HARD CODING THE USER LIST EVERYWHERE
42 my @VALID_USERS = qw/user1 user2 user3/;
43
44 use Carp;
45
46 use Date::Manip;
47 use Storable;
48 use MLDBM::Sync;
49 use MLDBM qw(DB_File Storable);
50 use Fcntl qw(:DEFAULT);
51 ###############################################################################
52
53 =head1 NAME
54
55 SFLC::TimeTrack::DB - Database instance for SFLC TimeTracker application
56
57 =head1 SYNOPSIS
58
59   use SFLC::TimeTrack::DB;
60
61   my $database = new SFLC::TimeTrack::DB($TYPE, $OPTION1, $OPTION2)
62
63 =head1 DESCRIPTION
64
65 Creates a database object for storing time tracker data at SFLC.
66
67 Currently, only the MLDBM type is supported.  Effectively, I've written
68 SFLC::TimeTrack::DB::MLDBM.  This should really be a wrapper class that
69 has an MLDBM underlying implementation and an SQL one.
70
71 =head2 EXPORT
72
73 None.
74
75 =head1 SEE ALSO
76
77 SFLC::TimeTrack::Input
78
79 =head1 AUTHOR
80
81 Bradley M. Kuhn, E<lt>bkuhn@softwarefreedom.org<gt>
82
83 =head1 COPYRIGHT AND LICENSE
84
85 Copyright (C) 2005, 2006, 2007   Software Freedom Law Center, Inc.
86
87 This software gives you freedom; it is licensed to you under version
88 3 of the GNU Affero General Public License.
89
90 This software is distributed WITHOUT ANY WARRANTY, without even the
91 implied warranties of MERCHANTABILITY and FITNESS FOR A PARTICULAR
92 PURPOSE.  See the GNU Affero General Public License for further
93 details.
94
95 You should have received a copy of the GNU Affero General Public
96 License, version 3 along with this software.  If not, see
97 <http://www.gnu.org/licenses/>.
98
99 =head1 EXTERNAL FUNCTIONS
100
101 =over
102
103 =cut
104
105 ###############################################################################
106
107 =item new
108
109 usage: new($METHOD, $OPTION1, $OPTION1)
110
111 Instantiates a new database storage method for an SFLC::TimeTrack
112 database.
113
114 =head2 ARGUMENTS
115
116 =over
117
118 =item C<$method>
119
120 Storage method used.  Currently, the only valid C<$METHOD> is "MLDBM".  In
121 the future, I hope to implement a true database storage method.
122
123 =item C<$OPTION1>
124
125 For MLDBM, C<$option1> is the path to the file that will store the
126 completed time data.
127
128 =item C<$option2>
129
130 For MLDBM, C<$option2> is the path to the pending data (tasks started but
131 never closed).
132
133 =back
134
135 =cut
136
137 sub new {
138   my($class, $method, $option1, $option2) = @_;
139
140   croak "Invalid storage method $method" if ($method ne "MLDBM");
141
142   my $self = {}; bless $self, $class;
143
144   $self->{db}{main}{filename} = $option1;
145   $self->{db}{pending}{filename} = $option2;
146   foreach my $db (qw/main pending/) {
147     $self->{dbm}{$db} = tie(%{$self->{$db}}, 'MLDBM::Sync',
148                             $self->{db}{$db}{filename}, O_CREAT|O_RDWR, 0600)
149       or croak "Cannot open database '$self->{db}{$db}{filename}: $!\n";
150
151     $self->{db}{$db}{amReading} = $self->{db}{$db}{amWriting} = 0;
152
153   }
154   return $self;
155 }
156 ###############################################################################
157
158 =item beginReadWork
159
160 usage: beginReadWork()
161
162 Begin read work on the datatbase.
163
164 =head2 ARGUMENTS
165
166 =over
167
168 =item C<$db>
169
170 String representing what database to begin work on.
171
172 =back
173
174 =cut
175
176 sub beginReadWork  {
177   my($self, $db) = @_;
178
179   croak "unknown database $db" unless defined $self->{db}{$db};
180
181   if ($self->{db}{$db}{amReading} <= 0) {
182     $self->{dbm}{$db}->ReadLock;
183     $self->{db}{$db}{amReading} = 1;
184   } else {
185     $self->{db}{$db}{amReading}++;
186   }
187 }
188
189
190 ###############################################################################
191
192 =item beginWriteWork
193
194 usage: beginWriteWork($db)
195
196 Begin read work on the datatbase.
197
198 =head2 ARGUMENTS
199
200 =over
201
202 =item C<$db>
203
204 String representing what database to begin work on.
205
206 =back
207
208 =cut
209
210 sub beginWriteWork  {
211   my($self, $db) = @_;
212
213   croak "unknown database $db" unless defined $self->{db}{$db};
214   if ($self->{db}{$db}{amWriting} <= 0) {
215     $self->{dbm}{$db}->Lock;
216     $self->{db}{$db}{amWriting} = 1;
217   } else {
218     $self->{db}{$db}{amWriting}++;
219   }
220 }
221
222 ###############################################################################
223
224 =item endWriteWork
225
226 usage: endWriteWork($db)
227
228 end read work on the datatbase.
229
230 =head2 ARGUMENTS
231
232 =over
233
234 =item C<$db>
235
236 String representing what database to end work on.
237
238 =back
239
240 =cut
241
242 sub endWriteWork  {
243   my($self, $db) = @_;
244
245   croak "unknown database $db" unless defined $self->{db}{$db};
246   $self->{db}{$db}{amWriting}--;
247   if ($self->{db}{$db}{amWriting} <= 0) {
248     $self->{dbm}{$db}->UnLock;
249     $self->{db}{$db}{amWriting} = 0;
250   }
251 }
252
253
254 ###############################################################################
255
256 =item endReadWork
257
258 usage: endReadWork($db)
259
260 end read work on the datatbase.
261
262 =head2 ARGUMENTS
263
264 =over
265
266 =item C<$db>
267
268 String representing what database to end work on.
269
270 =back
271
272 =cut
273
274 sub endReadWork  {
275   my($self, $db) = @_;
276
277   croak "unknown database $db" unless defined $self->{db}{$db};
278   $self->{db}{$db}{amReading}--;
279   if ($self->{db}{$db}{amReading} <= 0) {
280     $self->{dbm}{$db}->UnLock;
281     $self->{db}{$db}{amReading} = 0;
282   }
283 }
284
285 ###############################################################################
286
287 =item getCategoryHandle
288
289 usage: getCategoryHandle($categoryString)
290
291 Confirms that C<$categoryString> represents a real category name (croak'ing on an
292 error if it isn't) and returns the internal handle value for the category.
293
294 In the MLDBM world, this *is* the same as the cannonicalized
295 C<$categoryString>), but when futrue storage mechanisms exist, it won't be.
296
297 =head2 ARGUMENTS
298
299 =over
300
301 =item C<$db>
302
303 String representing what database to end work on.
304
305 =back
306
307 =cut
308
309 sub getCategoryHandle  {
310   my($self, $categoryString) = @_;
311
312   my $retVal;
313   $self->beginReadWork("main");
314   if (defined $self->{main}{categoryAliases}{GLOBAL}{$categoryString}) {
315     $retVal =  $self->{main}{categoryAliases}{GLOBAL}{$categoryString};
316   } elsif (defined $self->{main}{categoryList}{$categoryString}) {
317     $retVal =  $categoryString;
318   }
319   $self->endReadWork("main");
320   croak "lookup of undefined category, $categoryString" unless defined $retVal;
321   return $retVal;
322 }
323
324 sub getCategoryName {
325   my($self, $categoryHandle) = @_;
326
327   $self->beginReadWork("main");
328
329   if (not defined $self->{main}{categoryList}{$categoryHandle}) {
330     $self->endReadWork("main");
331     return undef;
332   } else {
333     my $retVal =  $self->{main}{categoryList}{$categoryHandle}->{name};
334     $self->endReadWork("main");
335     return $retVal;
336   }
337 }
338
339 sub getCategoryHandleForUserHandle  {
340   my($self, $categoryString, $userHandle) = @_;
341
342   $self->beginReadWork("main");
343
344   croak "invalid user handle $userHandle"
345       unless defined $self->{main}{categoryAliases}{users}{$userHandle};
346
347   my $retVal;
348   if (defined $self->{main}{categoryList}{$categoryString}) {
349     $retVal =  $categoryString;
350   } elsif (defined $self->{main}{categoryAliases}{GLOBAL}{$categoryString}) {
351     $retVal =  $self->{main}{categoryAliases}{GLOBAL}{$categoryString};
352   } elsif (defined $self->{main}{categoryAliases}{users}
353                               {$userHandle}{$categoryString}) {
354     $retVal = $self->{main}{categoryAliases}{users}
355                               {$userHandle}{$categoryString};
356   }
357
358   $self->endReadWork("main");
359   carp "lookup of undefined category, $categoryString" unless defined $retVal;
360   return $retVal;
361 }
362
363 sub getCategory {
364   my($self, $catString) = @_;
365   my $handle = $self->getCategoryHandle($catString);
366   my $retVal;
367   if (defined $handle) {
368     $self->beginReadWork("main");
369     $retVal = $self->{main}{categoryList}{$handle};
370     $self->endReadWork("main");
371   }
372   return $retVal;
373 }
374
375
376 sub getCategoryAliasList {
377   my($self, $userHandle) = @_;
378
379   if (defined $userHandle) {
380     $self->beginReadWork("main");
381     croak "invalid user handle $userHandle"
382       unless defined $self->{main}{categoryAliases}{users}{$userHandle};
383     my @val;
384     push(@val, keys %{$self->{main}{categoryAliases}{users}{$userHandle}});
385     push(@val, keys %{$self->{main}{categoryAliases}{GLOBAL}});
386
387     $self->endReadWork("main");
388     return @val;
389   } else {
390     my @val;
391     $self->beginReadWork("main");
392     push(@val, keys %{$self->{main}{categoryAliases}{GLOBAL}});
393     $self->endReadWork("main");
394     return @val;
395   }
396 }
397
398 =item getUserHandle
399
400 usage: getUserHandle($userString)
401
402 Confirms that C<$userString> represents a real user name (croak'ing on an
403 error if it isn't) and returns the internal handle value for the user.
404
405 In the MLDBM world, this *is* the same as the cannonicalized
406 C<$userString>), but when futrue storage mechanisms exist, it won't be.
407
408 =head2 ARGUMENTS
409
410 =over
411
412 =item C<$db>
413
414 String representing what database to end work on.
415
416 =back
417
418 =cut
419
420 sub getUserHandle  {
421   my($self, $userString) = @_;
422
423   $self->beginReadWork("main");
424   my $isDefined = (defined $self->{main}{entries}{byUser}{$userString});
425   $self->endReadWork("main");
426
427   $isDefined ? return $userString : undef;
428 }
429
430 sub checkIgnoreLines {
431   my($self, $userH, $line) = @_;
432
433   $self->beginReadWork("main");
434   unless (defined $self->{main}{ignoreLines}{users}{$userH}) {
435     carp "invalid user handle $userH during ignore line check of $line";
436     $self->endReadWork("main");
437     return 0;
438   }
439   chomp $line;
440   my $retVal = defined $self->{main}{ignoreLines}{users}{$userH}{"\L$line\E"};
441   $self->endReadWork("main");
442   return $retVal;
443
444 }
445 sub categoryAliasLookup {
446   my($self, $userH, $alias) = @_;
447   my %x;
448
449   my $retVal;
450   $alias = "\L$alias\E";
451   $self->beginReadWork("main");
452
453   if (defined $userH and
454       defined $self->{main}{categoryAliases}{users}{$userH} and
455       defined $self->{main}{categoryAliases}{users}{$userH}{$alias}) {
456     $retVal = $self->{main}{categoryAliases}{users}{$userH}{$alias};
457   } elsif (defined $self->{main}{categoryAliases}{GLOBAL}{$alias}) {
458     $retVal = $self->{main}{categoryAliases}{GLOBAL}{$alias};
459   }
460   $self->endReadWork("main");
461
462   (defined $retVal) ? print "looking up $alias, found $retVal\n" :
463     print "category alias lookup failed on $alias\n";
464   return $retVal;
465 }
466
467 sub setCategoryAlias {
468   my($self, $userH, $alias, $category) = @_;
469   my %x;
470
471   $category = $self->getCategory($category) if (not ref $category);
472
473   croak "cannot alias an undefined category" if (not defined $category);
474
475   my $retVal;
476   $alias = "\L$alias\E";
477   $alias =~ s/\.//g;
478   $alias =~ s%:%/%g;
479   $alias = "/$alias" unless $alias =~ m%^/%;
480
481   $self->beginWriteWork("main");
482   my(%aliases) = %{$self->{main}{categoryAliases}};
483   unless (defined $aliases{users}{$userH}) {
484     carp "invalid user handle $userH during lookup of $alias";
485     $self->endWriteWork("main");
486     return undef;
487   }
488   $aliases{users}{$userH}{$alias} = $category->get('id');
489   $self->{main}{categoryAliases} = \%aliases;
490   $self->endWriteWork("main");
491   print "just alised $alias, to $category->{id}\n";
492   return $category->{id};
493 }
494
495 sub unsetCategoryAlias {
496   my($self, $userH, $alias) = @_;
497   my %x;
498
499   my $retVal;
500   $alias = "\L$alias\E";
501   $self->beginWriteWork("main");
502   my(%aliases) = %{$self->{main}{categoryAliases}};
503   unless (defined $aliases{users}{$userH}) {
504     $self->endWriteWork("main");
505     return undef;
506   }
507   my $catId = $aliases{users}{$userH}{$alias};
508   delete $aliases{users}{$userH}{$alias};
509   $self->{main}{categoryAliases} = \%aliases;
510   $self->endWriteWork("main");
511   print "just unalised $alias for $userH\n";
512   return $catId;
513 }
514
515 sub setGlobalCategoryAlias {
516   my($self, $alias, $category) = @_;
517   my %x;
518
519   my $retVal;
520   $alias = "\L$alias\E";
521   $self->beginWriteWork("main");
522   my(%aliases) = %{$self->{main}{categoryAliases}};
523
524   $aliases{GLOBAL}{$alias} = $category->{id};
525   $self->{main}{categoryAliases} = \%aliases;
526   $self->endWriteWork("main");
527   print "just set $alias, to $category\n";
528   return $category->{id};
529 }
530
531 sub getWordAlias {
532   my($self, $alias) = @_;
533   my %x;
534
535   my $retVal;
536   $alias = "\L$alias\E";
537   $self->beginReadWork("main");
538   my(%aliases) = %{$self->{main}{wordAliases}};
539   $retVal =  $aliases{$alias};
540   $self->endReadWork("main");
541   return $retVal;
542 }
543
544 sub setWordAlias {
545   my($self, $alias, $word) = @_;
546   my %x;
547
548   my $retVal;
549   $alias = "\L$alias\E";
550   $word = "\L$word\E";
551   $self->beginWriteWork("main");
552   my(%aliases) = %{$self->{main}{wordAliases}};
553
554   $aliases{$alias} = $word;
555   $self->{main}{wordAliases} = \%aliases;
556   $self->endWriteWork("main");
557   print "just set word alias, \"$alias\" to \"$word\"\n";
558   return $word;
559 }
560
561
562 sub getPendingEntriesByUser {
563   my $self = shift;
564   my $user = shift;
565
566   $self->beginReadWork("pending");
567
568   my $userH = $self->getUserHandle($user);
569   $self->endReadWork("pending");
570   if (not defined $userH) {
571     return undef;
572   }
573   return $self->getPendingEntriesByUserHandle($userH);
574
575 }
576
577 sub getPendingEntriesByUserHandle {
578   my $self = shift;
579   my $userHandle = shift;
580
581   $self->beginReadWork("pending");
582
583   my(%entries) = %{$self->{'pending'}{entries}};
584   $self->endReadWork("pending");
585
586   my(%x) = %{$entries{byUser}{$userHandle}};
587   # return database fields
588   foreach my $key (keys %x) {
589     # FIXME: boy, this is really cheating...
590     bless $x{$key}, "SFLC::TimeTracker::Entry";
591   }
592   return \%x;
593
594 }
595
596 ###############################################################################
597 sub getUserConfigValue {
598   my($self, $userHandle, $config) = @_;
599
600   $self->beginReadWork("main");
601
602   my(%x) = %{$self->{main}{userConfig}{$userHandle}};
603   $self->endReadWork("main");
604   carp "$config not set fo $userHandle" if (not defined $x{$config});
605   return $x{$config};
606 }
607 ###############################################################################
608 sub setUserConfigValue {
609   my($self, $userHandle, $config, $value) = @_;
610
611   $self->beginWriteWork("main");
612
613   my(%x) = %{$self->{main}{userConfig}};
614   $x{$userHandle}{$config} = $value;
615   $self->{main}{userConfig} = \%x;
616   $self->endWriteWork("main");
617   return $value;
618 }
619 ###############################################################################
620
621 sub _uniquifyId {
622   my($self, $id) = @_;
623
624   $self->beginWriteWork("main");
625   my(%ids) = %{$self->{main}{ids}};
626   $ids{$id} = -1 unless (defined $ids{$id});
627
628   $id =  "${id}-" . ++$ids{$id};
629   $self->{main}{ids} = \%ids;
630   $self->endWriteWork("main");
631
632   return $id;
633 }
634
635 sub insertEntry {
636   my($self, $db, $entry) = @_;
637
638   croak "invalid $db" unless defined $self->{$db};
639   croak "cannot insert entry without userHandle"
640     unless defined $entry->{userHandle};
641   if (not defined $entry->{id}) {
642     if (defined $entry->{startTime}) {
643       $entry->{id} = $entry->{startTime};
644     } elsif (defined $entry->{endTime}) {
645       $entry->{id} = $entry->{endTime};
646     } else {
647       $entry->{id} = ParseDateString("now");
648     }
649     $entry->{id} =  $self->_uniquifyId($entry->{id});
650   }
651
652 #  my(%entCopy) = %{$entry};  # be sure we get a copy
653 #  bless \%entCopy, "SFLC::TimeTracker::Entry";
654 #  delete $entCopy{database};
655   delete $entry->{database};
656   $self->beginWriteWork($db);
657   my(%entries) = %{$self->{$db}{entries}};
658 #  $entries{byUser}{$entry->{userHandle}}{$entry->{id}} = \%entCopy;
659 #  $entries{byCategory}{$entry->{category}}{$entry->{id}} = \%entCopy
660 #    if ($db eq "main");
661   $entries{byUser}{$entry->{userHandle}}{$entry->{id}} = $entry;
662   $self->{$db}{entries} = \%entries;
663   $self->endWriteWork($db);
664   return $entry->{id};
665 }
666
667 sub getEntryById {
668   my($self, $userHandle, $id) = @_;
669
670   croak "cannot lookup entry without userHandle" unless defined $userHandle;
671
672   my $entry;
673   foreach my $db (qw/pending main/) {
674     $self->beginReadWork($db);
675     my(%entries) = %{$self->{$db}{entries}};
676     $self->endReadWork($db);
677     $entry = $entries{byUser}{$userHandle}{$id};
678     if (defined $entry) {
679       # FIXME: boy, this is really cheating...
680       bless $entry, "SFLC::TimeTracker::Entry";
681       return $entry;
682     }
683   }
684   return $entry;
685 }
686
687 sub getLastEntry {
688   my($self, $userHandle) = @_;
689
690   croak "cannot lookup entry without userHandle" unless defined $userHandle;
691
692   my $entry;
693   my $db =  "main";
694   $self->beginReadWork($db);
695
696   my(%entries) = %{$self->{$db}{entries}};
697
698   my $foundEntry;
699   foreach my $key (sort { $b cmp $a } keys %{$entries{byUser}{$userHandle}}) {
700     my $entry = $entries{byUser}{$userHandle}{$key};
701     bless $entry, "SFLC::TimeTracker::Entry";
702     $foundEntry = $entry;
703     last;
704   }
705   $self->endReadWork($db);
706   return $foundEntry;
707 }
708
709 sub getLastEntryWithCategory {
710   my($self, $userHandle, $categoryHandle) = @_;
711
712   croak "cannot lookup entry without userHandle" unless defined $userHandle;
713
714   my $entry;
715   my $db =  "main";
716   $self->beginReadWork($db);
717
718   my(%entries) = %{$self->{$db}{entries}};
719
720   my $foundEntry;
721   foreach my $key (sort { $b cmp $a } keys %{$entries{byUser}{$userHandle}}) {
722     my $entry = $entries{byUser}{$userHandle}{$key};
723     bless $entry, "SFLC::TimeTracker::Entry";
724     my $curCatID = $entry->get('category')->get('id');
725     if ($categoryHandle eq $curCatID) {
726       $foundEntry = $entry;
727       last;
728     }
729   }
730   $self->endReadWork($db);
731   return $foundEntry;
732 }
733 ###############################################################################
734
735 sub getUserList {
736   my($self) = @_;
737
738   $self->beginReadWork("main");
739   my(%entries) = %{$self->{'main'}{entries}};
740   $self->endReadWork('main');
741   return keys %{$entries{byUser}};
742 }
743 ###############################################################################
744 sub getEntriesOnDateWithCategory {
745   my($self, $userHandle, $date, $categoryHandle) = @_;
746
747   my $soughtDate = UnixDate($date, "%Y-%m-%d");
748
749   croak "cannot lookup entry without userHandle" unless defined $userHandle;
750
751   my $entry;
752   my $db =  "main";
753   $self->beginReadWork($db);
754
755   my(%entries) = %{$self->{$db}{entries}};
756
757   my @foundEntries;
758
759   foreach my $entry (values %{$entries{byUser}{$userHandle}}) {
760     bless $entry, "SFLC::TimeTracker::Entry";
761     my $dateOccurred = $entry->get('dateOccurred');
762     my $category = $entry->get('category');
763     warn("no  date occured for entry ID " . $entry->{id})
764       unless defined $dateOccurred;
765     if ($dateOccurred eq $soughtDate) {
766       my $curCatID = $entry->get('category')->get('id');
767       push(@foundEntries, $entry) if ($categoryHandle eq $curCatID);
768     }
769   }
770   $self->endReadWork($db);
771   return @foundEntries;
772 }
773 ###############################################################################
774 sub getEntriesOnDateInDB {
775   my($self, $db, $userHandle, $date) = @_;
776
777   my $soughtDate = UnixDate($date, "%Y-%m-%d");
778
779   croak "cannot lookup entry without userHandle" unless defined $userHandle;
780
781   my $entry;
782
783   $self->beginReadWork($db);
784
785   my(%entries) = %{$self->{$db}{entries}};
786
787   my @foundEntries;
788
789   foreach my $entry (values %{$entries{byUser}{$userHandle}}) {
790     bless $entry, "SFLC::TimeTracker::Entry";
791     my $dateOccurred = $entry->get('dateOccurred');
792     warn("no  date occured for entry ID " . $entry->{id})
793       unless defined $dateOccurred;
794     push(@foundEntries, $entry) if ($dateOccurred eq $soughtDate);
795   }
796   $self->endReadWork($db);
797   return @foundEntries;
798 }
799 ###############################################################################
800 sub getEntriesWithNote {
801   my($self, $db, $userHandle, $soughtNote) = @_;
802
803   croak "cannot lookup entry without userHandle" unless defined $userHandle;
804
805   my $entry;
806
807   $self->beginReadWork($db);
808
809   my(%entries) = %{$self->{$db}{entries}};
810
811   my @foundEntries;
812
813   foreach my $entry (values %{$entries{byUser}{$userHandle}}) {
814     bless $entry, "SFLC::TimeTracker::Entry";
815     my $note = $entry->get('note');
816     push(@foundEntries, $entry) if ($note !~ /^\s*$/ and
817                                     $note eq $soughtNote);
818   }
819   $self->endReadWork($db);
820   return @foundEntries;
821 }
822
823 ###############################################################################
824 sub getEntriesInDateRange {
825   my($self, $db, $userHandle, $startDate, $endDate) = @_;
826
827   $startDate = UnixDate($startDate, "%Y-%m-%d");
828   $endDate = UnixDate($endDate, "%Y-%m-%d");
829
830   croak "cannot lookup entry without userHandle" unless defined $userHandle;
831
832   my $entry;
833
834   $self->beginReadWork($db);
835
836   my(%entries) = %{$self->{$db}{entries}};
837
838   my @foundEntries;
839
840   foreach my $entry (values %{$entries{byUser}{$userHandle}}) {
841     bless $entry, "SFLC::TimeTracker::Entry";
842     my $dateOccurred = $entry->get('dateOccurred');
843     warn("no  date occured for entry ID " . $entry->{id})
844       unless defined $dateOccurred;
845     push(@foundEntries, $entry) if ($dateOccurred ge $startDate and
846                                     $dateOccurred le $endDate);
847   }
848   $self->endReadWork($db);
849   return @foundEntries;
850 }
851
852
853 sub removeQuestion {
854   my($self, $question) = @_;
855
856   croak "cannot insert entry without userHandle"
857     unless defined $question->{userHandle};
858   if (not defined $question->{id}) {
859       $question->{id} = "QUES-" . ParseDateString("now");
860       $question->{id} =  $self->_uniquifyId($question->{id});
861   }
862
863   delete $question->{database};
864   $self->beginWriteWork("pending");
865   my(%entries) = %{$self->{"pending"}{questions}};
866   delete $entries{users}{$question->{userHandle}}{list}{$question->{id}};
867   $self->{'pending'}{questions} =  \%entries;
868   $self->endWriteWork('pending');
869   return undef;
870 }
871
872 sub insertQuestion {
873   my($self, $question) = @_;
874
875   croak "cannot insert entry without userHandle"
876     unless defined $question->{userHandle};
877   if (not defined $question->{id}) {
878       $question->{id} = "QUES-" . ParseDateString("now");
879       $question->{id} =  $self->_uniquifyId($question->{id});
880   }
881
882   $self->beginWriteWork("pending");
883   my(%entries) = %{$self->{"pending"}{questions}};
884   $entries{users}{$question->{userHandle}}{list}{$question->{id}} = $question;
885   $self->{'pending'}{questions} =  \%entries;
886   $self->endWriteWork('pending');
887   return $question->{id};
888 }
889
890 sub updateQuestion {
891   my($self, $question) = @_;
892
893   croak "cannot insert entry without userHandle"
894     unless defined $question->{userHandle};
895   croak "cannot update question without ID" if (not defined $question->{id});
896
897   $self->beginWriteWork("pending");
898   my(%entries) = %{$self->{"pending"}{questions}};
899   $entries{users}{$question->{userHandle}}{list}{$question->{id}} = $question;
900   $self->{'pending'}{questions} =  \%entries;
901   $self->endWriteWork('pending');
902   return $question->{id};
903 }
904
905 sub getOldestQuestion {
906   my($self, $userHandle) = @_;
907
908   croak "cannot insert entry without userHandle"
909     unless defined $userHandle;
910   $self->beginReadWork("pending");
911   my(%entries) = %{$self->{"pending"}{questions}{users}{$userHandle}{list}};
912   $self->endReadWork('pending');
913   my(@list) = values %entries;
914   return undef if @list <= 0;
915   my(@sorted) = sort {$a->{id} cmp $b->{id} } @list;
916   return $sorted[0];
917 }
918
919 sub shiftInteractionTree {
920   my($self, $userHandle) = @_;
921
922   $self->beginWriteWork("pending");
923
924   my(%intTrees) = %{$self->{pending}{interactionTrees}};
925
926   if (not defined $intTrees{$userHandle}) {
927     $self->endWriteWork("pending");
928     croak "unable to find interaction tree list for $userHandle";
929   }
930   my $obj = shift @{$intTrees{$userHandle}};
931
932   $self->{pending}{interactionTrees} = \%intTrees;
933   $self->endWriteWork("pending");
934
935   return $obj;
936 }
937 sub unshiftInteractionTree {
938   my($self, $userHandle, $obj) = @_;
939
940   $self->beginWriteWork("pending");
941
942   my(%intTrees) = %{$self->{pending}{interactionTrees}};
943
944   if (not defined $intTrees{$userHandle}) {
945     $self->endWriteWork("pending");
946     croak "unable to find interaction tree list for $userHandle";
947   }
948   unshift(@{$intTrees{$userHandle}}, $obj);
949   $self->{pending}{interactionTrees} = \%intTrees;
950   $self->endWriteWork("pending");
951 }
952
953 sub setCurrentQuestion {
954   my($self, $question) = @_;
955
956   croak "cannot insert entry without userHandle"
957     unless defined $question->{userHandle};
958   if (not defined $question->{id}) {
959       $question->{id} = "QUES-" . ParseDateString("now");
960       $question->{id} =  $self->_uniquifyId($question->{id});
961   }
962
963   $self->beginWriteWork("pending");
964   my(%entries) = %{$self->{"pending"}{questions}};
965   $entries{users}{$question->{userHandle}}{current} = $question->{id};
966   $self->{'pending'}{questions} =  \%entries;
967   $self->endWriteWork('pending');
968   return $question->{id};
969 }
970
971 sub unsetCurrentQuestion {
972   my($self, $userHandle) = @_;
973
974   $self->beginWriteWork("pending");
975   my(%entries) = %{$self->{"pending"}{questions}};
976   $entries{users}{$userHandle}{current} = undef;
977   $self->{'pending'}{questions} =  \%entries;
978   $self->endWriteWork('pending');
979   return undef;
980 }
981
982 sub getCurrentQuestion {
983   my($self, $userHandle) = @_;
984
985   $self->beginReadWork("pending");
986   my $id = $self->{pending}{questions}{users}{$userHandle}{current};
987
988   my $copy;
989   $copy = (defined $id)
990              ? $self->{pending}{questions}{users}{$userHandle}{list}{$id}
991              : undef;
992   $self->endReadWork("pending");
993   return $copy;
994 }
995
996 sub stackPush {
997   my($self, $entry) = @_;
998
999   croak "cannot insert entry without userHandle"
1000     unless defined $entry->{userHandle};
1001   croak "cannot push entry without existing ID"
1002     unless defined $entry->{id};
1003
1004   $self->beginWriteWork("pending");
1005   my(@stack) = @{$self->{pending}{configs}{users}{$entry->{userHandle}}{stack}};
1006   my $total = push(@stack, $entry);
1007   $self->{pending}{configs}{users}{$entry->{userHandle}}{stack} = \@stack;
1008   $self->endWriteWork("pending");
1009   return $total;
1010 }
1011
1012 sub stackPop {
1013   my($self, $userHandle) = @_;
1014
1015   croak "cannot insert entry without userHandle" unless defined $userHandle;
1016
1017   $self->beginWriteWork("pending");
1018   my(@stack) = @{$self->{pending}{configs}{users}{$userHandle}{stack}};
1019   my $item = pop(@stack);
1020   $self->{pending}{configs}{$userHandle}{stack} = \@stack;
1021   $self->endWriteWork("pending");
1022   return $item;
1023 }
1024
1025 sub stackClear {
1026   my($self, $userHandle) = @_;
1027
1028   croak "cannot insert entry without userHandle" unless defined $userHandle;
1029
1030   $self->beginWriteWork("pending");
1031   my(%configs) = %{$self->{pending}{configs}};
1032   $configs{users}{$userHandle}{stack} = [];
1033   $self->{pending}{configs} = \%configs;
1034
1035   $self->endWriteWork("pending");
1036   return ();
1037 }
1038
1039
1040 sub stackList {
1041   my($self, $userHandle) = @_;
1042
1043   croak "cannot insert entry without userHandle" unless defined $userHandle;
1044
1045   $self->beginReadWork("pending");
1046   my(@stack) = @{$self->{pending}{configs}{users}{$userHandle}{stack}};
1047   $self->endReadWork("pending");
1048   return @stack;
1049 }
1050
1051 sub updateEntry {
1052   my($self, $db, $entry) = @_;
1053
1054   croak "invalid $db" unless defined $self->{$db};
1055   croak "cannot insert entry without userHandle"
1056     unless defined $entry->{userHandle};
1057   croak "cannot update entry without existing ID"
1058     unless defined $entry->{id};
1059
1060 #  my(%entCopy) = %{$entry};  # be sure we get a copy
1061 #  delete $entCopy{database};
1062   delete $entry->{database};
1063   # If we have now completed the entry, we should move it the main DB, and remove
1064   # it from pending
1065   if ($entry->isComplete() and $db eq "pending") {
1066     print STDERR "MOVING $entry->{id} to MAIN\n";
1067     $self->removeEntry($db, $entry);
1068     $db = "main";
1069     $entry->{inDB} = $db;
1070   }
1071   $self->beginWriteWork($db);
1072   my(%entries) = %{$self->{$db}{entries}};
1073 #  $entries{byUser}{$entry->{userHandle}}{$entry->{id}} = \%entCopy;
1074 #  $entries{byCategory}{$entry->{category}}{$entry->{id}} = \%entCopy
1075 #    if ($db eq "main");
1076   $entries{byUser}{$entry->{userHandle}}{$entry->{id}} = $entry;
1077   $self->{$db}{entries} = \%entries;
1078   $self->endWriteWork($db);
1079   return $db;
1080 }
1081 sub removeEntry {
1082   my($self, $db, $entry) = @_;
1083
1084   croak "invalid $db" unless defined $self->{$db};
1085   croak "cannot remove entry without existing ID"
1086     unless defined $entry->{id};
1087
1088   $self->beginWriteWork($db);
1089   my(%entries) = %{$self->{$db}{entries}};
1090   delete $entries{byUser}{$entry->{userHandle}}{$entry->{id}};
1091   $self->{$db}{entries} = \%entries;
1092   $self->endWriteWork($db);
1093   return undef;
1094 }
1095
1096 sub mergeCategory {
1097   my($self, $from, $into) = @_;
1098
1099   croak "bad argument to mergeCategory"
1100     unless ref $from eq "SFLC::TimeTracker::Category" and
1101       ref $into eq "SFLC::TimeTracker::Category";
1102
1103   $self->beginWriteWork("main");
1104   $self->beginWriteWork("pending");
1105
1106   # First, remove the "from" merger  outright from the category list
1107   my %catList = %{$self->{main}{categoryList}};
1108   delete $catList{$from->{id}};
1109   $self->{main}{categoryList} = \%catList;
1110
1111   # Next, correct aliases that point to it ... first for users ..
1112   my %aliases = %{$self->{main}{categoryAliases}};
1113   foreach my $user (keys %{$aliases{users}}) {
1114     foreach my $cat (keys %{$aliases{users}{$user}}) {
1115       $aliases{users}{$user}{$cat} = $into->{id}
1116       if ($aliases{users}{$user}{$cat} eq $from->{id});
1117     }
1118   }
1119   # .. then global ..
1120   foreach my $cat (keys %{$aliases{GLOBAL}}) {
1121     $aliases{GLOBAL}{$cat} = $into->{id}
1122       if ($aliases{GLOBAL}{$cat} eq $from->{id});
1123   }
1124   $self->{main}{categoryAliases} = \%aliases;
1125
1126   # Make a global alias that points from the old name to the new one.
1127
1128   $self->setGlobalCategoryAlias($from->{name}, $into);
1129
1130   foreach my $db (qw/main pending/) {
1131     my %entries = %{$self->{$db}{entries}};
1132     foreach my $user (keys %{$entries{byUser}}) {
1133       foreach my $id (keys %{$entries{byUser}{$user}}) {
1134         my $entry = $entries{byUser}{$user}{$id};
1135         my $cat = $entry->{category};
1136         next if not defined $cat and $db eq "pending";
1137         if ($entry->{category} eq $from->{id}) {
1138           $entry->{category} = $into->{id};
1139         }
1140       }
1141     }
1142     $self->{$db}{entries} = \%entries;
1143   }
1144   $self->{main}{categoryChanged} = time();
1145   $self->endWriteWork("main");
1146   $self->endWriteWork("pending");
1147 }
1148
1149 sub insertCategory {
1150   my($self, $category) = @_;
1151
1152   if (not defined $category->{id}) {
1153     $category->{id} = $self->_uniquifyId($category->{name});
1154   }
1155   $self->beginWriteWork("main");
1156   my(%x) = %{$self->{main}{categoryList}};
1157   my(%aliases) = %{$self->{main}{categoryAliases}};
1158   $x{$category->{id}} = $category;
1159   $aliases{GLOBAL}{$category->{name}} = $category->{id};
1160
1161   $self->{main}{categoryList} = \%x;
1162   $self->{main}{categoryAliases} = \%aliases;
1163
1164   $self->{main}{categoryChanged} = time();
1165
1166   $self->endWriteWork("main");
1167
1168   return $category->{id};
1169 }
1170
1171 ###############################################################################
1172 sub getCategoryLevel {
1173   my($self, $userHandle, $level) = @_;
1174
1175   croak "user handle required to lookup category level"
1176     unless defined $userHandle;
1177
1178   $self->beginReadWork("main");
1179
1180   croak "no category alias list for $userHandle"
1181     unless defined $self->{main}{categoryAliases}{users}{$userHandle};
1182
1183   my @cats;
1184   push(@cats, keys %{$self->{main}{categoryAliases}{users}{$userHandle}});
1185   push(@cats, keys %{$self->{main}{categoryAliases}{GLOBAL}});
1186
1187   my(@found) = grep(/^$level\//, @cats);
1188   @found = grep(/^$level/, @cats) if (@found <= 0);
1189
1190   # The second of the two lines is basically for "backwards
1191   # compatibility".  That grep was written the second way since this
1192   # function was first written, and I don't know if other parts of the
1193   # code rely on it to work for full path names that don't end in a /.  I
1194   # think the logic is now correct, since it searches for a "level" first,
1195   # and then extends it if nothing is found.  -- bkuhn, 2007-10-24
1196
1197   my %noDups;
1198   @noDups{@found} = @found;
1199   @found = keys %noDups;
1200
1201   $self->endReadWork("main");
1202   return @found;
1203 }
1204 ###############################################################################
1205 sub getCategoryLevelNoAliases {
1206   my($self, $level) = @_;
1207
1208   $self->beginReadWork("main");
1209
1210   my @cats;
1211   foreach my $catKey (keys %{$self->{main}{categoryList}}) {
1212     my $category = $self->{main}{categoryList}{$catKey};
1213     push(@cats, $category->{name});
1214   }
1215   my(@found) = grep(/^$level\//, @cats);
1216   @found = grep(/^$level/, @cats) if (@found <= 0);
1217
1218
1219   # The second of the two lines is basically for "backwards
1220   # compatibility".  That grep was written the second way since this
1221   # function was first written, and I don't know if other parts of the
1222   # code rely on it to work for full path names that don't end in a /.  I
1223   # think the logic is now correct, since it searches for a "level" first,
1224   # and then extends it if nothing is found.  -- bkuhn, 2007-10-24
1225
1226   my %noDups;
1227   @noDups{@found} = @found;
1228   @found = keys %noDups;
1229
1230   $self->endReadWork("main");
1231   return @found;
1232 }
1233 ###############################################################################
1234 sub updateCategory {
1235   my($self, $category) = @_;
1236
1237   if (not defined $category->{id}) {
1238     croak "cannot update category that has not yet been assigned an ID";
1239   }
1240   $self->beginWriteWork("main");
1241   my(%x) = %{$self->{main}{categoryList}};
1242   my(%aliases) = %{$self->{main}{categoryAliases}};
1243
1244   $x{$category->{id}} = $category;
1245   $aliases{GLOBAL}{$category->{name}} = $category->{id};
1246
1247   $self->{main}{categoryList} = \%x;
1248   $self->{main}{categoryAliases} = \%aliases;
1249
1250   $self->{main}{categoryChanged} = time();
1251   $self->endWriteWork("main");
1252   return $category->{id};
1253 }
1254
1255 sub clearIRCData {
1256   my($self) = @_;
1257
1258   $self->beginWriteWork("pending");
1259   my %stuff;
1260   foreach my $user (@VALID_USERS) {
1261     $stuff{$user} = {};
1262   }
1263   $self->{pending}{ircData} = \%stuff;
1264
1265   $self->endWriteWork("pending");
1266 }
1267
1268
1269 sub clearInteractionTrees {
1270   my($self) = @_;
1271
1272   $self->beginWriteWork("pending");
1273   my %stuff;
1274   foreach my $user (@VALID_USERS) {
1275     $stuff{$user} = [];
1276   }
1277   $self->{pending}{interactionTrees} = \%stuff;
1278
1279   $self->endWriteWork("pending");
1280 }
1281
1282 sub setIRCDataForUserHandle {
1283   my($self, $userHandle, $data) = @_;
1284
1285   use Data::Dumper;
1286   print "IN setIRCDataForUserHandle, setting $userHandle to: ",
1287     Data::Dumper->Dump([$data]);
1288   $self->beginWriteWork("pending");
1289   my(%ircData) =  %{$self->{pending}{ircData}};
1290   $ircData{$userHandle} = $data;
1291   $self->{pending}{ircData} = \%ircData;
1292   $self->endWriteWork("pending");
1293 }
1294 sub getIRCDataForUserHandle {
1295   my($self, $userHandle) = @_;
1296
1297   $self->beginReadWork("pending");
1298   my (%ircData) =  %{$self->{pending}{ircData}};
1299   $self->endReadWork("pending");
1300   return $ircData{$userHandle};
1301 }
1302 ###############################################################################
1303
1304 =item __CREATE_INITIAL
1305
1306 usage: __CREATE_INITIAL
1307
1308 A function to create initial information that we know.  This is hard coded
1309 and should be fixed to an external maintenance program in the future