Release a new CPAN version of Analysis
[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;
6f9cd3b7 7use Encode qw/ encode 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
6f9cd3b7 244has '_mysql_utf8_hack' => (
245 is => 'ro',
246 isa => 'Bool',
247 default => undef,
248);
249
98a6cab2 250# Push some columns into the extra_args
251around BUILDARGS => sub {
252 my $orig = shift;
253 my $class = shift;
254 my $args;
255 if( @_ == 1 ) {
256 $args = $_[0];
257 } else {
258 $args = { @_ };
259 }
f3f26624 260 my @column_args;
6f9cd3b7 261 if( $args->{'dsn'} =~ /^dbi:(\w+):/ ) { # We're using Backend::DBI
262 my $dbtype = $1;
f3f26624 263 @column_args = ( 'columns',
52dcc672 264 [ 'name' => { 'data_type' => 'varchar', 'is_nullable' => 1 },
265 'public' => { 'data_type' => 'bool', 'is_nullable' => 1 } ] );
6f9cd3b7 266 if( $dbtype eq 'mysql' &&
267 exists $args->{extra_args}->{dbi_attrs} &&
268 $args->{extra_args}->{dbi_attrs}->{mysql_enable_utf8} ) {
269 # There is a bad interaction with MySQL in utf-8 mode.
270 # Work around it here.
271 # TODO fix the underlying storage problem
272 $args->{extra_args}->{dbi_attrs}->{mysql_enable_utf8} = undef;
273 $args->{_mysql_utf8_hack} = 1;
274 }
98a6cab2 275 }
f3f26624 276 my $ea = $args->{'extra_args'};
277 if( ref( $ea ) eq 'ARRAY' ) {
278 push( @$ea, @column_args );
279 } elsif( ref( $ea ) eq 'HASH' ) {
280 $ea = { %$ea, @column_args };
281 } else {
282 $ea = { @column_args };
283 }
284 $args->{'extra_args'} = $ea;
285
98a6cab2 286 return $class->$orig( $args );
287};
288
f3f26624 289override _build_directory => sub {
290 my($self) = @_;
291 Text::Tradition::Store->connect(@{ $self->_connect_args },
292 resolver_constructor => sub {
293 my($class) = @_;
294 $class->new({ typemap => $self->directory->merged_typemap,
295 fallback_entry => Text::Tradition::TypeMap::Entry->new() });
296 });
297};
298
7cb56251 299## These checks don't cover store($id, $obj)
fc7b6388 300# before [ qw/ store update insert delete / ] => sub {
301before [ qw/ delete / ] => sub {
8d9a1cd8 302 my $self = shift;
861c3e27 303 my @nontrad;
304 foreach my $obj ( @_ ) {
951ddfe8 305 if( ref( $obj ) && !$obj->$_isa( 'Text::Tradition' )
306 && !$obj->$_isa('Text::Tradition::User') ) {
861c3e27 307 # Is it an id => Tradition hash?
308 if( ref( $obj ) eq 'HASH' && keys( %$obj ) == 1 ) {
309 my( $k ) = keys %$obj;
951ddfe8 310 next if $obj->{$k}->$_isa('Text::Tradition');
8d9a1cd8 311 }
861c3e27 312 push( @nontrad, $obj );
8d9a1cd8 313 }
12523041 314 }
861c3e27 315 if( @nontrad ) {
316 throw( "Cannot directly save non-Tradition object of type "
317 . ref( $nontrad[0] ) );
318 }
319};
12523041 320
d7ba60b4 321# TODO Garbage collection doesn't work. Suck it up and live with the
322# inflated DB.
d94224d9 323after delete => sub {
324 my $self = shift;
325 my $gc = KiokuDB::GC::Naive->new( backend => $self->directory->backend );
326 $self->directory->backend->delete( $gc->garbage->members );
327};
56cf65bd 328
329sub save {
861c3e27 330 my $self = shift;
331 return $self->store( @_ );
12523041 332}
333
56cf65bd 334sub tradition {
335 my( $self, $id ) = @_;
336 my $obj = $self->lookup( $id );
ad39942e 337 unless( $obj ) {
338 # Try looking up by name.
339 foreach my $item ( $self->traditionlist ) {
340 if( $item->{'name'} eq $id ) {
341 $obj = $self->lookup( $item->{'id'} );
342 last;
343 }
344 }
345 }
951ddfe8 346 if( $obj && !$obj->$_isa('Text::Tradition') ) {
861c3e27 347 throw( "Retrieved object is a " . ref( $obj ) . ", not a Text::Tradition" );
12523041 348 }
56cf65bd 349 return $obj;
12523041 350}
8d9a1cd8 351
98a6cab2 352sub traditionlist {
861c3e27 353 my $self = shift;
fefeeeda 354 my ($user) = @_;
355
356 return $self->user_traditionlist($user) if($user);
02d9cb95 357 return $self->_get_object_idlist( 'Text::Tradition' );
358}
fefeeeda 359
02d9cb95 360sub _get_object_idlist {
361 my( $self, $objclass ) = @_;
fefeeeda 362 my @tlist;
98a6cab2 363 # If we are using DBI, we can do it the easy way; if not, the hard way.
364 # Easy way still involves making a separate DBI connection. Ew.
0a900793 365 if( $self->dsn =~ /^dbi:(\w+):/ ) {
366 my $dbtype = $1;
98a6cab2 367 my @connection = @{$self->directory->backend->connect_info};
368 # Get rid of KiokuDB-specific arg
369 pop @connection if scalar @connection > 4;
0a900793 370 $connection[3]->{'sqlite_unicode'} = 1 if $dbtype eq 'SQLite';
371 $connection[3]->{'pg_enable_utf8'} = 1 if $dbtype eq 'Pg';
98a6cab2 372 my $dbh = DBI->connect( @connection );
02d9cb95 373 my $q = $dbh->prepare( 'SELECT id, name, public from entries WHERE class = "'
374 . $objclass . '"' );
98a6cab2 375 $q->execute();
376 while( my @row = $q->fetchrow_array ) {
6f9cd3b7 377 # Horrible horrible hack. Re-convert the name to UTF-8.
378 if( $self->_mysql_utf8_hack ) {
379 # Convert the chars into a raw bytestring.
380 my $octets = encode( 'ISO-8859-1', $row[1] );
381 $row[1] = decode_utf8( $octets );
382 }
52dcc672 383 push( @tlist, { 'id' => $row[0], 'name' => $row[1], 'public' => $row[2] } );
98a6cab2 384 }
385 } else {
386 $self->scan( sub { my $o = shift;
387 push( @tlist, { 'id' => $self->object_to_id( $o ),
52dcc672 388 'name' => $o->name,
02d9cb95 389 'public' => $o->public } )
390 if( ref $o eq $objclass ) } );
98a6cab2 391 }
392 return @tlist;
861c3e27 393}
394
395sub throw {
396 Text::Tradition::Error->throw(
397 'ident' => 'database error',
398 'message' => $_[0],
399 );
400}
401
cf7e4e7b 402
403# has 'directory' => (
404# is => 'rw',
405# isa => 'KiokuX::Model',
406# handles => []
407# );
408
409## TODO: Some of these methods should probably optionally take $user objects
410## instead of hashrefs.
411
412## It also occurs to me that all these methods don't need to be named
413## XX_user, but leaving that way for now incase we merge this code
414## into ::Directory for one-store.
415
a445ce40 416=head1 USER DIRECTORY METHODS
cf7e4e7b 417
a445ce40 418=head2 add_user( $userinfo )
cf7e4e7b 419
420Takes a hashref of C<username>, C<password>.
421
422Create a new user object, store in the KiokuDB backend, and return it.
423
424=cut
425
426sub add_user {
427 my ($self, $userinfo) = @_;
10ef7653 428
429 my $username = $userinfo->{username};
cf7e4e7b 430 my $password = $userinfo->{password};
7cb56251 431 my $role = $userinfo->{role} || 'user';
cf7e4e7b 432
b77f6c1b 433 throw( "No username given" ) unless $username;
434 throw( "Invalid password - must be at least " . $self->MIN_PASS_LEN
435 . " characters long" )
436 unless ( $self->validate_password($password) || $username =~ /^https?:/ );
cf7e4e7b 437
438 my $user = Text::Tradition::User->new(
439 id => $username,
440 password => ($password ? crypt_password($password) : ''),
a528f0f6 441 email => ($userinfo->{email} ? $userinfo->{email} : $username),
7cb56251 442 role => $role,
cf7e4e7b 443 );
444
cf7e4e7b 445 $self->store($user->kiokudb_object_id, $user);
446
447 return $user;
448}
449
a445ce40 450=head2 create_user( $userinfo )
451
452Takes a hashref that can either be suitable for add_user (see above) or be
453a hash of OpenID user information from Credential::OpenID.
454
455=cut
456
cf7e4e7b 457sub create_user {
10ef7653 458 my ($self, $userinfo) = @_;
459
460 ## No username means probably an OpenID based user
461 if(!exists $userinfo->{username}) {
a445ce40 462 _extract_openid_data($userinfo);
10ef7653 463 }
464
465 return $self->add_user($userinfo);
466}
467
468## Not quite sure where this method should be.. Auth /
469## Credential::OpenID just pass us back the chunk of extension data
a445ce40 470sub _extract_openid_data {
10ef7653 471 my ($userinfo) = @_;
472
473 ## Spec says SHOULD use url as identifier
474 $userinfo->{username} = $userinfo->{url};
475
476 ## Use email addy as display if available
477 if(exists $userinfo->{extensions} &&
478 exists $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'} &&
479 defined $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'}{'value.email'}) {
480 ## Somewhat ugly attribute extension reponse, contains
481 ## google-email string which we can use as the id
482
a528f0f6 483 $userinfo->{email} = $userinfo->{extensions}{'http://openid.net/srv/ax/1.0'}{'value.email'};
10ef7653 484 }
485
486 return;
cf7e4e7b 487}
488
a445ce40 489=head2 find_user( $userinfo )
cf7e4e7b 490
a4c19656 491Takes a hashref of C<username> or C<email>, and possibly openIDish results from
10ef7653 492L<Net::OpenID::Consumer>.
cf7e4e7b 493
494Fetches the user object for the given username and returns it.
495
496=cut
497
498sub find_user {
499 my ($self, $userinfo) = @_;
10ef7653 500
a4c19656 501 ## A URL field means probably an OpenID based user
502 if( exists $userinfo->{url} ) {
a445ce40 503 _extract_openid_data($userinfo);
10ef7653 504 }
505
a4c19656 506 my $user;
507 if( exists $userinfo->{username} ) {
508 my $username = $userinfo->{username};
509 ## No logins if user is deactivated (use lookup to fetch to re-activate)
510 $user = $self->lookup(Text::Tradition::User->id_for_user($username));
511 ## If there is an inactive user, skip it
512 return if( $user && !$user->active );
513 } elsif( exists $userinfo->{email} ) {
514 ## Scan the users looking for a matching email
515 my @matches;
516 $self->scan( sub { push( @matches, @_ )
517 if $_[0]->isa('Text::Tradition::User')
518 && $_[0]->email eq $userinfo->{email} } );
519 $user = shift @matches;
520 }
c80a73ea 521# print STDERR "Found user, $username, email is :", $user->email, ":\n";
df8c12f0 522 return $user;
cf7e4e7b 523}
524
a445ce40 525=head2 modify_user( $userinfo )
cf7e4e7b 526
527Takes a hashref of C<username> and C<password> (same as add_user).
528
529Retrieves the user, and updates it with the new information. Username
530changing is not currently supported.
531
532Returns the updated user object, or undef if not found.
533
534=cut
535
536sub modify_user {
537 my ($self, $userinfo) = @_;
538 my $username = $userinfo->{username};
539 my $password = $userinfo->{password};
4d4c5789 540 my $role = $userinfo->{role};
cf7e4e7b 541
52dcc672 542 throw( "Missing username" ) unless $username;
cf7e4e7b 543
cf7e4e7b 544 my $user = $self->find_user({ username => $username });
b77f6c1b 545 throw( "Could not find user $username" ) unless $user;
cf7e4e7b 546
4d4c5789 547 if($password) {
52dcc672 548 throw( "Bad password" ) unless $self->validate_password($password);
4d4c5789 549 $user->password(crypt_password($password));
550 }
551 if($role) {
552 $user->role($role);
553 }
cf7e4e7b 554
555 $self->update($user);
556
557 return $user;
558}
559
a445ce40 560=head2 deactivate_user( $userinfo )
cf7e4e7b 561
562Takes a hashref of C<username>.
563
564Sets the users C<active> flag to false (0), and sets all traditions
565assigned to them to non-public, updates the storage and returns the
566deactivated user.
567
568Returns undef if user not found.
569
570=cut
571
572sub deactivate_user {
573 my ($self, $userinfo) = @_;
574 my $username = $userinfo->{username};
575
b77f6c1b 576 throw( "Need to specify a username for deactivation" ) unless $username;
cf7e4e7b 577
578 my $user = $self->find_user({ username => $username });
b77f6c1b 579 throw( "User $username not found" ) unless $user;
cf7e4e7b 580
581 $user->active(0);
582 foreach my $tradition (@{ $user->traditions }) {
583 ## Not implemented yet
584 # $tradition->public(0);
585 }
cf7e4e7b 586
587 ## Should we be using Text::Tradition::Directory also?
588 $self->update(@{ $user->traditions });
589
590 $self->update($user);
591
592 return $user;
593}
594
a445ce40 595=head2 reactivate_user( $userinfo )
cf7e4e7b 596
597Takes a hashref of C<username>.
598
599Returns the user object if already activated. Activates (sets the
600active flag to true (1)), updates the storage and returns the user.
601
602Returns undef if the user is not found.
603
604=cut
605
606sub reactivate_user {
607 my ($self, $userinfo) = @_;
608 my $username = $userinfo->{username};
609
b77f6c1b 610 throw( "Need to specify a username for reactivation" ) unless $username;
cf7e4e7b 611
df8c12f0 612 my $user = $self->lookup(Text::Tradition::User->id_for_user($username));
b77f6c1b 613 throw( "User $username not found" ) unless $user;
cf7e4e7b 614
615 return $user if $user->active;
616
617 $user->active(1);
618 $self->update($user);
619
620 return $user;
621}
622
a445ce40 623=head2 delete_user( $userinfo )
cf7e4e7b 624
770f7a2b 625CAUTION: Deletes actual data!
cf7e4e7b 626
627Takes a hashref of C<username>.
628
629Returns undef if the user doesn't exist.
630
631Removes the user from the store and returns 1.
632
633=cut
634
635sub delete_user {
636 my ($self, $userinfo) = @_;
637 my $username = $userinfo->{username};
638
b77f6c1b 639 throw( "Need to specify a username for deletion" ) unless $username;
cf7e4e7b 640
cf7e4e7b 641 my $user = $self->find_user({ username => $username });
b77f6c1b 642 throw( "User $username not found" ) unless $user;
cf7e4e7b 643
644 ## Should we be using Text::Tradition::Directory for this bit?
645 $self->delete( @{ $user->traditions });
646
647 ## Poof, gone.
648 $self->delete($user);
649
650 return 1;
651}
652
a445ce40 653=head2 validate_password( $password )
cf7e4e7b 654
655Takes a password string. Returns true if it is longer than
656L</MIN_PASS_LEN>, false otherwise.
657
658Used internally by L</add_user>.
659
660=cut
661
662sub validate_password {
663 my ($self, $password) = @_;
664
665 return if !$password;
666 return if length($password) < $self->MIN_PASS_LEN;
667
668 return 1;
669}
670
a445ce40 671=head2 user_traditionlist( $user )
672
673Returns a tradition list (see specification above) but containing only
674those traditions visible to the specified user. If $user is the string
675'public', returns only publicly-viewable traditions.
676
677=cut
678
679sub user_traditionlist {
680 my ($self, $user) = @_;
681
682 my @tlist;
683 if(ref $user && $user->is_admin) {
684 ## Admin sees all
685 return $self->traditionlist();
686 } elsif(ref $user) {
687 ## We have a user object already, so just fetch its traditions and use tose
688 foreach my $t (@{ $user->traditions }) {
689 push( @tlist, { 'id' => $self->object_to_id( $t ),
690 'name' => $t->name } );
691 }
692 return @tlist;
693 } elsif($user ne 'public') {
694 die "Passed neither a user object nor 'public' to user_traditionlist";
695 }
696
697 ## Search for all traditions which allow public viewing
698 my @list = grep { $_->{public} } $self->traditionlist();
699 return @list;
700}
701
8d9a1cd8 7021;
12523041 703
027d819c 704=head1 LICENSE
705
706This package is free software and is provided "as is" without express
707or implied warranty. You can redistribute it and/or modify it under
708the same terms as Perl itself.
709
8943ff68 710=head1 AUTHORS
711
712Tara L Andrews E<lt>aurum@cpan.orgE<gt> (initial release)
713
714Shadowcat Systems L<http://www.scsys.co.uk/> (user functionality; making it all work)
027d819c 715