Merge branch 'master' of https://github.com/tla/stemmatology
[scpubgit/stemmatology.git] / persistence / lib / Text / Tradition / Directory.pm
CommitLineData
8d9a1cd8 1package Text::Tradition::Directory;
2
3use strict;
4use warnings;
5use Moose;
98a6cab2 6use DBI;
0a900793 7use Encode qw/ decode_utf8 /;
ad1291ee 8use KiokuDB::GC::Naive;
8d9a1cd8 9use KiokuDB::TypeMap;
10use KiokuDB::TypeMap::Entry::Naive;
951ddfe8 11use Safe::Isa;
861c3e27 12use Text::Tradition::Error;
8d9a1cd8 13
cf7e4e7b 14## users
15use KiokuX::User::Util qw(crypt_password);
f3f26624 16use Text::Tradition::Store;
cf7e4e7b 17use Text::Tradition::User;
f3f26624 18use Text::Tradition::TypeMap::Entry;
cf7e4e7b 19
8d9a1cd8 20extends 'KiokuX::Model';
21
8943ff68 22use vars qw/ $VERSION /;
e3a52f81 23$VERSION = "1.1";
8943ff68 24
12523041 25=head1 NAME
26
8943ff68 27Text::Tradition::Directory - a KiokuDB interface for storing and retrieving
28traditions and their owners
12523041 29
30=head1 SYNOPSIS
31
32 use Text::Tradition::Directory;
33 my $d = Text::Tradition::Directory->new(
34 'dsn' => 'dbi:SQLite:mytraditions.db',
35 'extra_args' => { 'create' => 1 },
36 );
37
38 my $tradition = Text::Tradition->new( @args );
37bf09f4 39 my $stemma = $tradition->add_stemma( dotfile => $dotfile ); # if Analysis module installed
12523041 40 $d->save_tradition( $tradition );
12523041 41
42 foreach my $id ( $d->traditions ) {
43 print $d->tradition( $id )->name;
12523041 44 }
770f7a2b 45
46 ## Users:
47 my $userstore = Text::Tradition::UserStore->new(dsn => 'dbi:SQLite:foo.db');
48 my $newuser = $userstore->add_user({ username => 'fred',
49 password => 'somepassword' });
50
51 my $fetchuser = $userstore->find_user({ username => 'fred' });
52 if($fetchuser->check_password('somepassword')) {
53 ## login user or .. whatever
54 }
55
56 my $user = $userstore->deactivate_user({ username => 'fred' });
57 if(!$user->active) {
58 ## shouldnt be able to login etc
59 }
12523041 60
61=head1 DESCRIPTION
62
8943ff68 63Text::Tradition::Directory is an interface for storing and retrieving text
64traditions and all their data, including an associated stemma hypothesis
65and a user who has ownership rights to the tradition data. It is an
66instantiation of a KiokuDB::Model, storing traditions and associated
67stemmas by UUID.
68
69The Text::Tradition::Directory package also includes the
70L<Text::Tradition::User> class for user objects, and the
71L<Text::Tradition::Ownership> role which extends the Text::Tradition class
72to handle user ownership.
12523041 73
770f7a2b 74=head1 ATTRIBUTES
75
76=head2 MIN_PASS_LEN
77
78Constant for the minimum password length when validating passwords,
79defaults to "8".
80
81=cut
82
83has MIN_PASS_LEN => ( is => 'ro', isa => 'Num', default => sub { 8 } );
84
12523041 85=head1 METHODS
86
87=head2 new
88
56cf65bd 89Returns a Directory object.
12523041 90
98a6cab2 91=head2 traditionlist
12523041 92
98a6cab2 93Returns a hashref mapping of ID => name for all traditions in the directory.
12523041 94
95=head2 tradition( $id )
96
97Returns the Text::Tradition object of the given ID.
98
56cf65bd 99=head2 save( $tradition )
12523041 100
56cf65bd 101Writes the given tradition to the database, returning its ID.
12523041 102
d7ba60b4 103=head2 delete( $tradition )
104
105Deletes the given tradition object from the database.
106WARNING!! Garbage collection does not yet work. Use this sparingly.
107
12523041 108=begin testing
109
861c3e27 110use TryCatch;
12523041 111use File::Temp;
951ddfe8 112use Safe::Isa;
12523041 113use Text::Tradition;
12523041 114use_ok 'Text::Tradition::Directory';
115
116my $fh = File::Temp->new();
117my $file = $fh->filename;
118$fh->close;
119my $dsn = "dbi:SQLite:dbname=$file";
861c3e27 120my $uuid;
12523041 121my $t = Text::Tradition->new(
56cf65bd 122 'name' => 'inline',
123 'input' => 'Tabular',
124 'file' => 't/data/simple.txt',
125 );
37bf09f4 126my $stemma_enabled = $t->can( 'add_stemma' );
56cf65bd 127
861c3e27 128{
129 my $d = Text::Tradition::Directory->new( 'dsn' => $dsn,
130 'extra_args' => { 'create' => 1 } );
951ddfe8 131 ok( $d->$_isa('Text::Tradition::Directory'), "Got directory object" );
861c3e27 132
133 my $scope = $d->new_scope;
134 $uuid = $d->save( $t );
135 ok( $uuid, "Saved test tradition" );
136
951ddfe8 137 SKIP: {
138 skip "Analysis package not installed", 5 unless $stemma_enabled;
139 my $s = $t->add_stemma( dotfile => 't/data/simple.dot' );
140 ok( $d->save( $t ), "Updated tradition with stemma" );
141 is( $d->tradition( $uuid ), $t, "Correct tradition returned for id" );
142 is( $d->tradition( $uuid )->stemma(0), $s, "...and it has the correct stemma" );
143 try {
144 $d->save( $s );
145 } catch( Text::Tradition::Error $e ) {
146 is( $e->ident, 'database error', "Got exception trying to save stemma directly" );
147 like( $e->message, qr/Cannot directly save non-Tradition object/,
148 "Exception has correct message" );
149 }
861c3e27 150 }
151}
152my $nt = Text::Tradition->new(
153 'name' => 'CX',
154 'input' => 'CollateX',
155 'file' => 't/data/Collatex-16.xml',
156 );
951ddfe8 157ok( $nt->$_isa('Text::Tradition'), "Made new tradition" );
861c3e27 158
159{
160 my $f = Text::Tradition::Directory->new( 'dsn' => $dsn );
161 my $scope = $f->new_scope;
98a6cab2 162 is( scalar $f->traditionlist, 1, "Directory index has our tradition" );
861c3e27 163 my $nuuid = $f->save( $nt );
164 ok( $nuuid, "Stored second tradition" );
98a6cab2 165 my @tlist = $f->traditionlist;
166 is( scalar @tlist, 2, "Directory index has both traditions" );
861c3e27 167 my $tf = $f->tradition( $uuid );
98a6cab2 168 my( $tlobj ) = grep { $_->{'id'} eq $uuid } @tlist;
169 is( $tlobj->{'name'}, $tf->name, "Directory index has correct tradition name" );
861c3e27 170 is( $tf->name, $t->name, "Retrieved the tradition from a new directory" );
951ddfe8 171 my $sid;
172 SKIP: {
173 skip "Analysis package not installed", 4 unless $stemma_enabled;
174 $sid = $f->object_to_id( $tf->stemma(0) );
175 try {
176 $f->tradition( $sid );
177 } catch( Text::Tradition::Error $e ) {
178 is( $e->ident, 'database error', "Got exception trying to fetch stemma directly" );
179 like( $e->message, qr/not a Text::Tradition/, "Exception has correct message" );
180 }
181 try {
182 $f->delete( $sid );
183 } catch( Text::Tradition::Error $e ) {
184 is( $e->ident, 'database error', "Got exception trying to delete stemma directly" );
185 like( $e->message, qr/Cannot directly delete non-Tradition object/,
186 "Exception has correct message" );
187 }
861c3e27 188 }
ad39942e 189
861c3e27 190 $f->delete( $uuid );
191 ok( !$f->exists( $uuid ), "Object is deleted from DB" );
951ddfe8 192 ok( !$f->exists( $sid ), "Object stemma also deleted from DB" ) if $stemma_enabled;
98a6cab2 193 is( scalar $f->traditionlist, 1, "Object is deleted from index" );
861c3e27 194}
195
e3a52f81 196TODO: {
197 todo_skip "Deletion conflicts with Analysis package", 2
198 if $t->does('Text::Tradition::HasStemma');
861c3e27 199 my $g = Text::Tradition::Directory->new( 'dsn' => $dsn );
200 my $scope = $g->new_scope;
98a6cab2 201 is( scalar $g->traditionlist, 1, "Now one object in new directory index" );
ad39942e 202 my $ntobj = $g->tradition( 'CX' );
09909f9d 203 my @w1 = sort { $a->sigil cmp $b->sigil } $ntobj->witnesses;
204 my @w2 = sort{ $a->sigil cmp $b->sigil } $nt->witnesses;
ad39942e 205 is_deeply( \@w1, \@w2, "Looked up remaining tradition by name" );
861c3e27 206}
12523041 207
208=end testing
209
210=cut
fc7b6388 211use Text::Tradition::TypeMap::Entry;
12523041 212
12523041 213has +typemap => (
fc7b6388 214 is => 'rw',
215 isa => 'KiokuDB::TypeMap',
216 default => sub {
217 KiokuDB::TypeMap->new(
218 isa_entries => {
f3f26624 219 # now that we fall back to YAML deflation, all attributes of
220 # Text::Tradition will be serialized to YAML as individual objects
221 # Except if we declare a specific entry type here
fc7b6388 222 "Text::Tradition" =>
f3f26624 223 KiokuDB::TypeMap::Entry::MOP->new(),
224 # We need users to be naive entries so that they hold
225 # references to the original tradition objects, not clones
226 "Text::Tradition::User" =>
227 KiokuDB::TypeMap::Entry::MOP->new(),
228 "Text::Tradition::Collation" =>
229 KiokuDB::TypeMap::Entry::MOP->new(),
230 "Text::Tradition::Witness" =>
231 KiokuDB::TypeMap::Entry::MOP->new(),
fb4caab6 232 "Graph" => Text::Tradition::TypeMap::Entry->new(),
7e17346f 233 "Set::Scalar" => Text::Tradition::TypeMap::Entry->new(),
fc7b6388 234 }
235 );
236 },
8d9a1cd8 237);
238
98a6cab2 239# Push some columns into the extra_args
240around BUILDARGS => sub {
241 my $orig = shift;
242 my $class = shift;
243 my $args;
244 if( @_ == 1 ) {
245 $args = $_[0];
246 } else {
247 $args = { @_ };
248 }
f3f26624 249 my @column_args;
98a6cab2 250 if( $args->{'dsn'} =~ /^dbi/ ) { # We're using Backend::DBI
f3f26624 251 @column_args = ( 'columns',
52dcc672 252 [ 'name' => { 'data_type' => 'varchar', 'is_nullable' => 1 },
253 'public' => { 'data_type' => 'bool', 'is_nullable' => 1 } ] );
98a6cab2 254 }
f3f26624 255 my $ea = $args->{'extra_args'};
256 if( ref( $ea ) eq 'ARRAY' ) {
257 push( @$ea, @column_args );
258 } elsif( ref( $ea ) eq 'HASH' ) {
259 $ea = { %$ea, @column_args };
260 } else {
261 $ea = { @column_args };
262 }
263 $args->{'extra_args'} = $ea;
264
98a6cab2 265 return $class->$orig( $args );
266};
267
f3f26624 268override _build_directory => sub {
269 my($self) = @_;
270 Text::Tradition::Store->connect(@{ $self->_connect_args },
271 resolver_constructor => sub {
272 my($class) = @_;
273 $class->new({ typemap => $self->directory->merged_typemap,
274 fallback_entry => Text::Tradition::TypeMap::Entry->new() });
275 });
276};
277
7cb56251 278## These checks don't cover store($id, $obj)
fc7b6388 279# before [ qw/ store update insert delete / ] => sub {
280before [ qw/ delete / ] => sub {
8d9a1cd8 281 my $self = shift;
861c3e27 282 my @nontrad;
283 foreach my $obj ( @_ ) {
951ddfe8 284 if( ref( $obj ) && !$obj->$_isa( 'Text::Tradition' )
285 && !$obj->$_isa('Text::Tradition::User') ) {
861c3e27 286 # Is it an id => Tradition hash?
287 if( ref( $obj ) eq 'HASH' && keys( %$obj ) == 1 ) {
288 my( $k ) = keys %$obj;
951ddfe8 289 next if $obj->{$k}->$_isa('Text::Tradition');
8d9a1cd8 290 }
861c3e27 291 push( @nontrad, $obj );
8d9a1cd8 292 }
12523041 293 }
861c3e27 294 if( @nontrad ) {
295 throw( "Cannot directly save non-Tradition object of type "
296 . ref( $nontrad[0] ) );
297 }
298};
12523041 299
d7ba60b4 300# TODO Garbage collection doesn't work. Suck it up and live with the
301# inflated DB.
d94224d9 302after delete => sub {
303 my $self = shift;
304 my $gc = KiokuDB::GC::Naive->new( backend => $self->directory->backend );
305 $self->directory->backend->delete( $gc->garbage->members );
306};
56cf65bd 307
308sub save {
861c3e27 309 my $self = shift;
310 return $self->store( @_ );
12523041 311}
312
56cf65bd 313sub tradition {
314 my( $self, $id ) = @_;
315 my $obj = $self->lookup( $id );
ad39942e 316 unless( $obj ) {
317 # Try looking up by name.
318 foreach my $item ( $self->traditionlist ) {
319 if( $item->{'name'} eq $id ) {
320 $obj = $self->lookup( $item->{'id'} );
321 last;
322 }
323 }
324 }
951ddfe8 325 if( $obj && !$obj->$_isa('Text::Tradition') ) {
861c3e27 326 throw( "Retrieved object is a " . ref( $obj ) . ", not a Text::Tradition" );
12523041 327 }
56cf65bd 328 return $obj;
12523041 329}
8d9a1cd8 330
98a6cab2 331sub traditionlist {
861c3e27 332 my $self = shift;
fefeeeda 333 my ($user) = @_;
334
335 return $self->user_traditionlist($user) if($user);
336
337 my @tlist;
98a6cab2 338 # If we are using DBI, we can do it the easy way; if not, the hard way.
339 # Easy way still involves making a separate DBI connection. Ew.
0a900793 340 if( $self->dsn =~ /^dbi:(\w+):/ ) {
341 my $dbtype = $1;
98a6cab2 342 my @connection = @{$self->directory->backend->connect_info};
343 # Get rid of KiokuDB-specific arg
344 pop @connection if scalar @connection > 4;
0a900793 345 $connection[3]->{'sqlite_unicode'} = 1 if $dbtype eq 'SQLite';
346 $connection[3]->{'pg_enable_utf8'} = 1 if $dbtype eq 'Pg';
98a6cab2 347 my $dbh = DBI->connect( @connection );
52dcc672 348 my $q = $dbh->prepare( 'SELECT id, name, public from entries WHERE class = "Text::Tradition"' );
98a6cab2 349 $q->execute();
350 while( my @row = $q->fetchrow_array ) {
0a900793 351 my( $id, $name ) = @row;
352 # Horrible horrible hack
353 $name = decode_utf8( $name ) if $dbtype eq 'mysql';
52dcc672 354 push( @tlist, { 'id' => $row[0], 'name' => $row[1], 'public' => $row[2] } );
98a6cab2 355 }
356 } else {
357 $self->scan( sub { my $o = shift;
358 push( @tlist, { 'id' => $self->object_to_id( $o ),
52dcc672 359 'name' => $o->name,
360 'public' => $o->public } ) } );
98a6cab2 361 }
362 return @tlist;
861c3e27 363}
364
365sub throw {
366 Text::Tradition::Error->throw(
367 'ident' => 'database error',
368 'message' => $_[0],
369 );
370}
371
cf7e4e7b 372
373# has 'directory' => (
374# is => 'rw',
375# isa => 'KiokuX::Model',
376# handles => []
377# );
378
379## TODO: Some of these methods should probably optionally take $user objects
380## instead of hashrefs.
381
382## It also occurs to me that all these methods don't need to be named
383## XX_user, but leaving that way for now incase we merge this code
384## into ::Directory for one-store.
385
a445ce40 386=head1 USER DIRECTORY METHODS
cf7e4e7b 387
a445ce40 388=head2 add_user( $userinfo )
cf7e4e7b 389
390Takes a hashref of C<username>, C<password>.
391
392Create a new user object, store in the KiokuDB backend, and return it.
393
394=cut
395
396sub add_user {
397 my ($self, $userinfo) = @_;
10ef7653 398
399 my $username = $userinfo->{username};
cf7e4e7b 400 my $password = $userinfo->{password};
7cb56251 401 my $role = $userinfo->{role} || 'user';
cf7e4e7b 402
b77f6c1b 403 throw( "No username given" ) unless $username;
404 throw( "Invalid password - must be at least " . $self->MIN_PASS_LEN
405 . " characters long" )
406 unless ( $self->validate_password($password) || $username =~ /^https?:/ );
cf7e4e7b 407
408 my $user = Text::Tradition::User->new(
409 id => $username,
410 password => ($password ? crypt_password($password) : ''),
a528f0f6 411 email => ($userinfo->{email} ? $userinfo->{email} : $username),
7cb56251 412 role => $role,
cf7e4e7b 413 );
414
cf7e4e7b 415 $self->store($user->kiokudb_object_id, $user);
416
417 return $user;
418}
419
a445ce40 420=head2 create_user( $userinfo )
421
422Takes a hashref that can either be suitable for add_user (see above) or be
423a hash of OpenID user information from Credential::OpenID.
424
425=cut
426
cf7e4e7b 427sub create_user {
10ef7653 428 my ($self, $userinfo) = @_;
429
430 ## No username means probably an OpenID based user
431 if(!exists $userinfo->{username}) {
a445ce40 432 _extract_openid_data($userinfo);
10ef7653 433 }
434
435 return $self->add_user($userinfo);
436}
437
438## Not quite sure where this method should be.. Auth /
439## Credential::OpenID just pass us back the chunk of extension data
a445ce40 440sub _extract_openid_data {
10ef7653 441 my ($userinfo) = @_;
442
443 ## Spec says SHOULD use url as identifier
444 $userinfo->{username} = $userinfo->{url};
445
446 ## Use email addy as display if available
447 if(exists $userinfo->{extensions} &&
448 exists $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'} &&
449 defined $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'}{'value.email'}) {
450 ## Somewhat ugly attribute extension reponse, contains
451 ## google-email string which we can use as the id
452
a528f0f6 453 $userinfo->{email} = $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'}{'value.email'};
10ef7653 454 }
455
456 return;
cf7e4e7b 457}
458
a445ce40 459=head2 find_user( $userinfo )
cf7e4e7b 460
a4c19656 461Takes a hashref of C<username> or C<email>, and possibly openIDish results from
10ef7653 462L<Net::OpenID::Consumer>.
cf7e4e7b 463
464Fetches the user object for the given username and returns it.
465
466=cut
467
468sub find_user {
469 my ($self, $userinfo) = @_;
10ef7653 470
a4c19656 471 ## A URL field means probably an OpenID based user
472 if( exists $userinfo->{url} ) {
a445ce40 473 _extract_openid_data($userinfo);
10ef7653 474 }
475
a4c19656 476 my $user;
477 if( exists $userinfo->{username} ) {
478 my $username = $userinfo->{username};
479 ## No logins if user is deactivated (use lookup to fetch to re-activate)
480 $user = $self->lookup(Text::Tradition::User->id_for_user($username));
481 ## If there is an inactive user, skip it
482 return if( $user && !$user->active );
483 } elsif( exists $userinfo->{email} ) {
484 ## Scan the users looking for a matching email
485 my @matches;
486 $self->scan( sub { push( @matches, @_ )
487 if $_[0]->isa('Text::Tradition::User')
488 && $_[0]->email eq $userinfo->{email} } );
489 $user = shift @matches;
490 }
c80a73ea 491# print STDERR "Found user, $username, email is :", $user->email, ":\n";
df8c12f0 492 return $user;
cf7e4e7b 493}
494
a445ce40 495=head2 modify_user( $userinfo )
cf7e4e7b 496
497Takes a hashref of C<username> and C<password> (same as add_user).
498
499Retrieves the user, and updates it with the new information. Username
500changing is not currently supported.
501
502Returns the updated user object, or undef if not found.
503
504=cut
505
506sub modify_user {
507 my ($self, $userinfo) = @_;
508 my $username = $userinfo->{username};
509 my $password = $userinfo->{password};
4d4c5789 510 my $role = $userinfo->{role};
cf7e4e7b 511
52dcc672 512 throw( "Missing username" ) unless $username;
cf7e4e7b 513
cf7e4e7b 514 my $user = $self->find_user({ username => $username });
b77f6c1b 515 throw( "Could not find user $username" ) unless $user;
cf7e4e7b 516
4d4c5789 517 if($password) {
52dcc672 518 throw( "Bad password" ) unless $self->validate_password($password);
4d4c5789 519 $user->password(crypt_password($password));
520 }
521 if($role) {
522 $user->role($role);
523 }
cf7e4e7b 524
525 $self->update($user);
526
527 return $user;
528}
529
a445ce40 530=head2 deactivate_user( $userinfo )
cf7e4e7b 531
532Takes a hashref of C<username>.
533
534Sets the users C<active> flag to false (0), and sets all traditions
535assigned to them to non-public, updates the storage and returns the
536deactivated user.
537
538Returns undef if user not found.
539
540=cut
541
542sub deactivate_user {
543 my ($self, $userinfo) = @_;
544 my $username = $userinfo->{username};
545
b77f6c1b 546 throw( "Need to specify a username for deactivation" ) unless $username;
cf7e4e7b 547
548 my $user = $self->find_user({ username => $username });
b77f6c1b 549 throw( "User $username not found" ) unless $user;
cf7e4e7b 550
551 $user->active(0);
552 foreach my $tradition (@{ $user->traditions }) {
553 ## Not implemented yet
554 # $tradition->public(0);
555 }
cf7e4e7b 556
557 ## Should we be using Text::Tradition::Directory also?
558 $self->update(@{ $user->traditions });
559
560 $self->update($user);
561
562 return $user;
563}
564
a445ce40 565=head2 reactivate_user( $userinfo )
cf7e4e7b 566
567Takes a hashref of C<username>.
568
569Returns the user object if already activated. Activates (sets the
570active flag to true (1)), updates the storage and returns the user.
571
572Returns undef if the user is not found.
573
574=cut
575
576sub reactivate_user {
577 my ($self, $userinfo) = @_;
578 my $username = $userinfo->{username};
579
b77f6c1b 580 throw( "Need to specify a username for reactivation" ) unless $username;
cf7e4e7b 581
df8c12f0 582 my $user = $self->lookup(Text::Tradition::User->id_for_user($username));
b77f6c1b 583 throw( "User $username not found" ) unless $user;
cf7e4e7b 584
585 return $user if $user->active;
586
587 $user->active(1);
588 $self->update($user);
589
590 return $user;
591}
592
a445ce40 593=head2 delete_user( $userinfo )
cf7e4e7b 594
770f7a2b 595CAUTION: Deletes actual data!
cf7e4e7b 596
597Takes a hashref of C<username>.
598
599Returns undef if the user doesn't exist.
600
601Removes the user from the store and returns 1.
602
603=cut
604
605sub delete_user {
606 my ($self, $userinfo) = @_;
607 my $username = $userinfo->{username};
608
b77f6c1b 609 throw( "Need to specify a username for deletion" ) unless $username;
cf7e4e7b 610
cf7e4e7b 611 my $user = $self->find_user({ username => $username });
b77f6c1b 612 throw( "User $username not found" ) unless $user;
cf7e4e7b 613
614 ## Should we be using Text::Tradition::Directory for this bit?
615 $self->delete( @{ $user->traditions });
616
617 ## Poof, gone.
618 $self->delete($user);
619
620 return 1;
621}
622
a445ce40 623=head2 validate_password( $password )
cf7e4e7b 624
625Takes a password string. Returns true if it is longer than
626L</MIN_PASS_LEN>, false otherwise.
627
628Used internally by L</add_user>.
629
630=cut
631
632sub validate_password {
633 my ($self, $password) = @_;
634
635 return if !$password;
636 return if length($password) < $self->MIN_PASS_LEN;
637
638 return 1;
639}
640
a445ce40 641=head2 user_traditionlist( $user )
642
643Returns a tradition list (see specification above) but containing only
644those traditions visible to the specified user. If $user is the string
645'public', returns only publicly-viewable traditions.
646
647=cut
648
649sub user_traditionlist {
650 my ($self, $user) = @_;
651
652 my @tlist;
653 if(ref $user && $user->is_admin) {
654 ## Admin sees all
655 return $self->traditionlist();
656 } elsif(ref $user) {
657 ## We have a user object already, so just fetch its traditions and use tose
658 foreach my $t (@{ $user->traditions }) {
659 push( @tlist, { 'id' => $self->object_to_id( $t ),
660 'name' => $t->name } );
661 }
662 return @tlist;
663 } elsif($user ne 'public') {
664 die "Passed neither a user object nor 'public' to user_traditionlist";
665 }
666
667 ## Search for all traditions which allow public viewing
668 my @list = grep { $_->{public} } $self->traditionlist();
669 return @list;
670}
671
8d9a1cd8 6721;
12523041 673
027d819c 674=head1 LICENSE
675
676This package is free software and is provided "as is" without express
677or implied warranty. You can redistribute it and/or modify it under
678the same terms as Perl itself.
679
8943ff68 680=head1 AUTHORS
681
682Tara L Andrews E<lt>aurum@cpan.orgE<gt> (initial release)
683
684Shadowcat Systems L<http://www.scsys.co.uk/> (user functionality; making it all work)
027d819c 685