use strict; use warnings; package Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata; use Jifty::DBI::Schema; use base 'Jifty::DBI::Record::Plugin'; our @EXPORT = qw(current_user_can current_user_is_owner); my %column_names; =head1 NAME Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata - ActorMetadata mixin =head1 SYNOPSIS package MyApp::Model::CoffeeShop; use Jifty::DBI::Schema; use MyApp::Record schema { # custom column defrinitions }; use Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata; # created_by, created_on, updated_on and updated_by =head1 DESCRIPTION =head1 SCHEMA This mixin adds the following columns to the model schema: =head2 created_by =head2 created_on =head2 updated_on =head2 updated_by =cut # XXX: move this to somewhere BEGIN { Jifty::DBI::Schema->register_types( Date => sub { type is 'date', input_filters are qw/Jifty::DBI::Filter::Date/ }, Time => sub { type is 'time', input_filters are qw/Jifty::DBI::Filter::Time/ }, DateTime => sub { type is 'datetime', input_filters are qw/Jifty::DBI::Filter::DateTime/ }, TimeStamp => sub { type is 'timestamp', filters are qw( Jifty::Filter::DateTime Jifty::DBI::Filter::DateTime), } ); } =head1 METHODS # XXX: podcoverage should count parent classes. these pods are useless =head2 register_triggers Adds the triggers to the model this mixin is added to. =cut sub register_triggers { my $self = shift; $self->add_trigger(name => 'before_create', callback => \&before_create); } =head2 register_triggers_for_column =cut sub register_triggers_for_column { my $self = shift; my $column = shift; return unless $column_names{ ref $self || $self }{'updated_on'} && $column eq $column_names{ ref $self || $self }{'updated_on'} || $column_names{ ref $self || $self }{'updated_by'} && $column eq $column_names{ ref $self || $self }{'updated_by'}; $self->add_trigger(name => 'after_set', callback => \&after_set); return 1; } =head2 before_create Sets C, C, C and C based on the current user and time. =cut sub before_create { my $self = shift; my $args = shift; for my $by (qw/created_by updated_by/) { if ( $column_names{ ref $self || $self }{$by} ) { $args->{$column_names{ ref $self || $self }{$by}} ||= $self->current_user->id; } } for my $time ( qw/created_on updated_on/ ) { if ( $column_names{ ref $self || $self }{$time} ) { $args->{$column_names{ ref $self || $self }{$time}} ||= Jifty::DateTime->now; } } return 1; } =head2 after_set update C and C based on the current user and current time. =cut sub after_set { my $self = shift; if ( $column_names{ ref $self || $self }{'updated_on'} ) { $self->__set( column => $column_names{ ref $self || $self }{'updated_on'}, value => Jifty::DateTime->now ); } if ( $column_names{ ref $self || $self }{'updated_by'} ) { $self->__set( column => $column_names{ ref $self || $self }{'updated_by'}, value => $self->current_user->id ); } return 1; } =head2 current_user_can Rejects creation unless there's a current_user. =cut # XXX: Move this to an abortable trigger sub current_user_can { my $self = shift; my $action = shift; my %args = (@_); if ($action eq 'create') { return unless $self->current_user and $self->current_user->id || $self->current_user->is_bootstrap_user; } #Rejects update or deletion unless the current_user is the creator. (Jesse says: this feels like wrong logic for this mixin) # if ($action eq 'update' or $action eq 'delete') { # return undef unless $self->current_user_is_owner; # } return 1; } =head2 current_user_is_owner =cut sub current_user_is_owner { my $self = shift; my $created_by = $self->__value($column_names{ref $self || $self}{'created_by'}); return unless $self->current_user && $created_by; return unless $self->current_user->id; return $self->current_user->id == $created_by; } =head2 import to be more flexible, we allow some configurations like: e.g. use Jifty::Plugin::ActorMetadata::Mixin::Model::ActorMetadata user_class => 'Foo::Model::Principal', map => { created_by => 'creator', created_on => 'created' } current valid args are: user_class => 'Foo::Model::User' class that you want created_by and updated_by to be refers_to map => { created_by => 'creator', ... } the real column name you want to use. this also controls whether a column will be added or not. i.e. if the hashref is { created_by => 'creator', created_on => 'created' }, then columns 'updated_by' and 'updated_on' will not be added. =cut sub import { my $self = shift; my %args = @_; my $user_class = $args{'user_class'} || Jifty->app_class('Model', 'User'); my @columns = qw/created_on created_by updated_on updated_by/; my %map; # fiddle map if ( $args{'map'} && ref $args{'map'} eq 'HASH' ) { for my $column ( keys %{$args{'map'}} ) { $map{$column} = $args{'map'}{$column}; } } else { @map{@columns} = @columns; } $column_names{scalar caller} = \%map; Jifty::DBI::Schema->import; # to import subs like schema, references my @ret = schema { if ( $map{'created_by'} ) { column $map{'created_by'} => references $user_class, render_as 'hidden'; } if ( $map{'created_on'} ) { column $map{'created_on'} => is TimeStamp, render_as 'hidden'; } if ( $map{'updated_by'} ) { column $map{'updated_by'} => references $user_class, render_as 'hidden'; } if ( $map{'updated_on'} ) { column $map{'updated_on'} => is TimeStamp, render_as 'hidden'; } }; require Jifty::Record; Jifty::Record->import( @ret ); # TODO # below is the import sub from Jifty::DBI::Record::Plugin, # because of some caller stuff, I can't just call SUPER # need to refactor, either here or Jifty::DBI::Record::Plugin my $caller = caller; for ($self->columns) { $caller->_init_methods_for_column($_); # virtual will be handled later $caller->COLUMNS->{$_->name} = $_ unless $_->virtual; } $self->export_to_level(1,undef); if (my $triggers = $self->can('register_triggers') ) { $triggers->($caller) } if (my $triggers_for_column = $self->can('register_triggers_for_column') ) { for my $column (keys %{$caller->_columns_hashref}) { $triggers_for_column->($caller, $column) } } push(@{ $caller->RECORD_MIXINS }, $self); $self->COLUMNS(undef); # reset columns for ActorMetadata.pm } 1;