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