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