<field reporter:label="Title" name="title" reporter:datatype="text"/>
<field reporter:label="User" name="usr" reporter:datatype="link" />
<field reporter:label="Message" name="message" reporter:datatype="text"/>
+ <field reporter:label="Patron Visible?" name="pub" reporter:datatype="bool"/>
+ <field reporter:label="Stop Date/Time" name="stop_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Editor" name="editor" reporter:datatype="link" />
+ <field reporter:label="Edit Date/Time" name="edit_date" reporter:datatype="timestamp"/>
</fields>
<links>
<link field="usr" reltype="has_a" key="id" map="" class="au"/>
+ <link field="editor" reltype="has_a" key="id" map="" class="au"/>
<link field="sending_lib" reltype="has_a" key="id" map="" class="aou"/>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<field reporter:label="Title" name="title" reporter:datatype="text"/>
<field reporter:label="User" name="usr" reporter:datatype="link" />
<field reporter:label="Message" name="message" reporter:datatype="text"/>
+ <field reporter:label="Patron Visible?" name="pub" reporter:datatype="bool"/>
+ <field reporter:label="Stop Date/Time" name="stop_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Editor" name="usr" reporter:datatype="link" />
+ <field reporter:label="Edit Date/Time" name="edit_date" reporter:datatype="timestamp"/>
</fields>
<links>
<link field="usr" reltype="has_a" key="id" map="" class="au"/>
+ <link field="editor" reltype="has_a" key="id" map="" class="au"/>
<link field="sending_lib" reltype="has_a" key="id" map="" class="aou"/>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
</actions>
</permacrud>
</class>
- <class id="aun" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::usr_note" oils_persist:tablename="actor.usr_note" reporter:label="User Note">
- <fields oils_persist:primary="id" oils_persist:sequence="actor.usr_note_id_seq">
+ <class id="aump" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::usr_message_penalty" oils_persist:tablename="actor.usr_message_penalty" reporter:label="User Message Penalty">
+ <fields oils_persist:primary="id">
+ <field reporter:label="ID" name="id" reporter:datatype="id"/>
<field reporter:label="Creation Date/Time" name="create_date" reporter:datatype="timestamp"/>
- <field reporter:label="Creating Staff" name="creator" reporter:datatype="link"/>
- <field reporter:label="Note ID" name="id" reporter:datatype="id" />
- <field reporter:label="Is OPAC Visible?" name="pub" reporter:datatype="bool"/>
- <field reporter:label="Note Title" name="title" reporter:datatype="text"/>
+ <field reporter:label="Debug: Set Date (Penalty)" name="ausp_set_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Debug: Creation Date/Time (Message)" name="aum_create_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Read Date/Time" name="read_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Creating Library" name="org_unit" reporter:datatype="link"/>
+ <field reporter:label="Debug: Creating Library (Penalty)" name="ausp_org_unit" reporter:datatype="link"/>
+ <field reporter:label="Debug: Creating Library (Message)" name="aum_sending_lib" reporter:datatype="link"/>
+ <field reporter:label="Debug: Penalty ID" name="ausp_id" reporter:datatype="id" />
+ <field reporter:label="Debug: Message ID" name="aum_id" reporter:datatype="id" />
+ <field reporter:label="Deleted?" name="deleted" reporter:datatype="bool"/>
+ <field reporter:label="Title" name="title" reporter:datatype="text"/>
<field reporter:label="User" name="usr" reporter:datatype="link" />
- <field reporter:label="Note Content" name="value" reporter:datatype="text"/>
+ <field reporter:label="Debug: User (Penalty)" name="ausp_usr" reporter:datatype="link" />
+ <field reporter:label="Debug: User (Message)" name="aum_usr" reporter:datatype="link" />
+ <field reporter:label="Message" name="message" reporter:datatype="text"/>
+ <field reporter:label="Patron Visible?" name="pub" reporter:datatype="bool"/>
+ <field reporter:label="Stop Date/Time" name="stop_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Debug: Stop Date/Time (Penalty)" name="ausp_stop_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Debug: Stop Date/Time (Message)" name="aum_stop_date" reporter:datatype="timestamp"/>
+ <field reporter:label="Editor" name="editor" reporter:datatype="link" />
+ <field reporter:label="Edit Date/Time" name="edit_date" reporter:datatype="timestamp"/>
+ <field name="staff" reporter:datatype="link" reporter:label="Staff"/>
+ <field name="standing_penalty" reporter:datatype="link" reporter:label="Standing Penalty"/>
+ <field name="ausp_usr_message" reporter:datatype="link" reporter:label="Debug: User Message (Penalty)"/>
</fields>
<links>
+ <link field="ausp_id" reltype="has_a" key="id" map="" class="ausp"/>
+ <link field="aum_id" reltype="has_a" key="id" map="" class="aum"/>
<link field="usr" reltype="has_a" key="id" map="" class="au"/>
- <link field="creator" reltype="has_a" key="id" map="" class="au"/>
+ <link field="ausp_usr" reltype="has_a" key="id" map="" class="au"/>
+ <link field="aum_usr" reltype="has_a" key="id" map="" class="au"/>
+ <link field="editor" reltype="has_a" key="id" map="" class="au"/>
+ <link field="staff" reltype="has_a" key="id" map="" class="au"/>
+ <link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="aum_sending_lib" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="ausp_org_unit" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="standing_penalty" reltype="has_a" key="id" map="" class="csp"/>
+ <link field="ausp_usr_message" reltype="has_a" key="id" map="" class="aum"/>
</links>
- <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
- <actions>
- <create permission="UPDATE_USER" context_field="owner">
- <context link="usr" field="home_ou"/>
- </create>
- <!-- note: public notes are still accessible via API -->
- <retrieve permission="UPDATE_USER">
- <context link="usr" field="home_ou"/>
- </retrieve>
- <update permission="UPDATE_USER">
- <context link="usr" field="home_ou"/>
- </update>
- <delete permission="UPDATE_USER">
- <context link="usr" field="home_ou"/>
- </delete>
- </actions>
- </permacrud>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions> <!-- created magically, so no create action -->
+ <retrieve permission="VIEW_USER"><context link="usr" field="home_ou"/></retrieve>
+ <update permission="UPDATE_USER"><context link="usr" field="home_ou"/></update>
+ </actions>
+ </permacrud>
</class>
+
<class id="aupw" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::usr_privacy_waiver" oils_persist:tablename="actor.usr_privacy_waiver" reporter:label="Privacy Waiver">
<fields oils_persist:primary="id" oils_persist:sequence="actor.usr_privacy_waiver_id_seq">
<field reporter:label="ID" name="id" reporter:datatype="id" />
<field reporter:label="Workstation Org Unit" name="ws_ou" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Workstation ID" name="wsid" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Active" name="active" reporter:datatype="bool"/>
- <field reporter:label="Alert Message" name="alert_message" reporter:datatype="text"/>
<field reporter:label="Barred" name="barred" reporter:datatype="bool"/>
<field reporter:label="Physical Address" name="billing_address" reporter:datatype="link"/>
<field reporter:label="Current Library Card" name="card" reporter:datatype="link"/>
<link field="standing_penalties" reltype="has_many" key="usr" map="" class="ausp"/>
<link field="addresses" reltype="has_many" key="usr" map="" class="aua"/>
<link field="survey_responses" reltype="has_many" key="usr" map="" class="asvr"/>
- <link field="notes" reltype="has_many" key="usr" map="" class="aun"/>
+ <link field="notes" reltype="has_many" key="usr" map="" class="aum"/>
<link field="checkins" reltype="has_many" key="checkin_staff" map="" class="circ"/>
<link field="cards" reltype="has_many" key="usr" map="" class="ac"/>
<link field="performed_circulations" reltype="has_many" key="circ_staff" map="" class="circ"/>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
<create permission="ADMIN_STANDING_PENALTY" global_required="true"/>
- <retrieve permission="ADMIN_STANDING_PENALTY VIEW_STANDING_PENALTY" global_required="true"/>
+ <retrieve permission="STAFF_LOGIN" global_required="true"/>
<update permission="ADMIN_STANDING_PENALTY" global_required="true"/>
<delete permission="ADMIN_STANDING_PENALTY" global_required="true"/>
</actions>
<field name="standing_penalty" reporter:datatype="link" reporter:label="Standing Penalty"/>
<field name="org_unit" reporter:datatype="link" reporter:label="Org Unit"/>
<field name="stop_date" reporter:datatype="timestamp" reporter:label="Stop Date"/>
- <field name="note" reporter:datatype="text" reporter:label="Note"/>
+ <field name="usr_message" reporter:datatype="link" reporter:label="User Message"/>
</fields>
<links>
<link field="usr" reltype="has_a" key="id" map="" class="au"/>
<link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
<link field="staff" reltype="has_a" key="id" map="" class="au"/>
<link field="standing_penalty" reltype="has_a" key="id" map="" class="csp"/>
+ <link field="usr_message" reltype="has_a" key="id" map="" class="aum"/>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
}
__PACKAGE__->register_method(
+ method => "get_my_org_ancestor_at_depth",
+ api_name => "open-ils.actor.org_unit.ancestor_at_depth.retrieve"
+);
+
+sub get_my_org_ancestor_at_depth {
+ my( $self, $client, $auth, $org_id, $depth ) = @_;
+ my $e = new_editor(authtoken=>$auth);
+ return $e->event unless $e->checkauth;
+ $org_id = $e->requestor->ws_ou unless defined $org_id;
+
+ return $apputils->org_unit_ancestor_at_depth( $org_id, $depth );
+}
+
+__PACKAGE__->register_method(
method => "patron_adv_search",
api_name => "open-ils.actor.patron.search.advanced"
);
$out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue/;
my $unread_msgs = $e->search_actor_usr_message([
- {usr => $user_id, read_date => undef, deleted => 'f'},
+ {usr => $user_id, read_date => undef, deleted => 'f',
+ 'pub' => 't', # this is for the unread message count in the opac
+ '-or' => [
+ {stop_date => undef},
+ {stop_date => {'>' => 'now'}}
+ ],
+ },
{idlist => 1}
]);
);
sub apply_penalty {
- my($self, $conn, $auth, $penalty) = @_;
+ my($self, $conn, $auth, $penalty, $msg) = @_;
+
+ $msg ||= {};
my $e = new_editor(authtoken=>$auth, xact => 1);
return $e->die_event unless $e->checkauth;
my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
- my $ctx_org =
- (defined $ptype->org_depth) ?
- $U->org_unit_ancestor_at_depth($penalty->org_unit, $ptype->org_depth) :
- $penalty->org_unit;
+ my $ctx_org = $penalty->org_unit; # csp org_depth is now considered in the UI for the org drop-down menu
+
+ if (($msg->{title} || $msg->{message}) && ($msg->{title} ne '' || $msg->{message} ne '')) {
+ my $aum = Fieldmapper::actor::usr_message->new;
+
+ $aum->create_date('now');
+ $aum->sending_lib($e->requestor->ws_ou);
+ $aum->title($msg->{title});
+ $aum->usr($penalty->usr);
+ $aum->message($msg->{message});
+ $aum->pub($msg->{pub});
+
+ $aum = $e->create_actor_usr_message($aum)
+ or return $e->die_event;
+
+ $penalty->usr_message($aum->id);
+ }
$penalty->org_unit($ctx_org);
$penalty->staff($e->requestor->id);
}
__PACKAGE__->register_method(
+ method => "modify_penalty",
+ api_name => "open-ils.actor.user.penalty.modify"
+);
+
+sub modify_penalty {
+ my($self, $conn, $auth, $penalty, $usr_msg) = @_;
+
+ my $e = new_editor(authtoken=>$auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+
+ my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
+ return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
+
+ $usr_msg->editor($e->requestor->id);
+ $usr_msg->edit_date('now');
+
+ if ($usr_msg->isnew) {
+ $usr_msg = $e->create_actor_usr_message($usr_msg)
+ or return $e->die_event;
+ $penalty->usr_message($usr_msg->id);
+ } else {
+ $usr_msg = $e->update_actor_usr_message($usr_msg)
+ or return $e->die_event;
+ }
+
+ if ($penalty->isnew) {
+ $penalty = $e->create_actor_user_standing_penalty($penalty)
+ or return $e->die_event;
+ } else {
+ $penalty = $e->update_actor_user_standing_penalty($penalty)
+ or return $e->die_event;
+ }
+
+ $e->commit;
+ return 1;
+}
+
+__PACKAGE__->register_method(
method => "remove_penalty",
api_name => "open-ils.actor.user.penalty.remove"
);
my $e = new_editor(authtoken=>$auth, xact => 1);
return $e->die_event unless $e->checkauth;
for my $penalty_id (@$penalty_ids) {
- my $penalty = $e->search_actor_user_standing_penalty( { id => $penalty_id } )->[0];
+ my $penalty = $e->search_actor_user_standing_penalty([
+ { id => $penalty_id },
+ { flesh => 1,
+ flesh_fields => {aum => ['usr_message']}
+ }
+ ])->[0];
if (! $penalty ) { return $e->die_event; }
my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
- $penalty->note( $note ); $penalty->ischanged( 1 );
+ my $aum = $penalty->usr_message();
+ if (!$aum) {
+ $aum = Fieldmapper::actor::usr_message->new;
+
+ $aum->create_date('now');
+ $aum->sending_lib($e->requestor->ws_ou);
+ $aum->title('');
+ $aum->usr($penalty->usr);
+ $aum->message($note);
+ $aum->pub(0);
+ $aum->isnew(1);
- $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
+ $aum = $e->create_actor_usr_message($aum)
+ or return $e->die_event;
+
+ $penalty->usr_message($aum->id);
+ $penalty->ischanged(1);
+ $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
+ } else {
+ $aum = $e->retrieve_actor_usr_message($aum) or return $e->die_event;
+ $aum->message($note); $aum->ischanged(1);
+ $e->update_actor_usr_message($aum) or return $e->die_event;
+ }
}
$e->commit;
return 1;
$fetch_penalties = 1;
}
+ my $fetch_notes = 0;
+ if(grep {$_ eq 'notes'} @$fields) {
+ $fields = [grep {$_ ne 'notes'} @$fields];
+ $fetch_notes = 1;
+ }
+
my $fetch_usr_act = 0;
if(grep {$_ eq 'usr_activity'} @$fields) {
$fields = [grep {$_ ne 'usr_activity'} @$fields];
org_unit => $U->get_org_full_path($e->requestor->ws_ou)
},
{ flesh => 1,
- flesh_fields => {ausp => ['standing_penalty']}
+ flesh_fields => {ausp => ['standing_penalty','usr_message']}
}
])
);
}
+ if($fetch_notes) {
+ # grab undeleted notes (now actor.usr_message_penalty) that have not hit their stop_date
+ $user->notes(
+ $e->search_actor_usr_message_penalty([
+ { usr => $id,
+ deleted => 'f',
+ '-or' => [
+ {stop_date => undef},
+ {stop_date => {'>' => 'now'}}
+ ],
+ }, {}
+ ])
+ );
+ }
+
# retrieve the most recent usr_activity entry
if ($fetch_usr_act) {
$pen->usr($user_id);
$pen->standing_penalty(30); # PATRON_IN_COLLECTIONS
$pen->staff($e->requestor->id);
- $pen->note($fee_note) if $fee_note;
- $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen);
+ my $msg = { 'pub' => 0, 'title' => 'PATRON_IN_COLLECTIONS', 'message' => $fee_note };
+ $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen, $msg);
return OpenILS::Event->new('SUCCESS');
}
ident_type2 ident_value2 net_access_level alias
photo_url create_date expire_date credit_forward_balance
super_user usrgroup passwd card last_xact_id
- standing barred profile prefix suffix alert_message
+ standing barred profile prefix suffix
day_phone evening_phone other_phone mailing_address
claims_never_checked_out_count last_update_time/ );
$usr_message->message( $message_template_output );
$usr_message->usr( $env->{usr_message}{usr}->id );
$usr_message->sending_lib( $env->{usr_message}{sending_lib}->id );
+ $usr_message->pub('t');
if ($self->editor->xact_begin) {
if ($self->editor->create_actor_usr_message( $usr_message )) {
$penalty->usr($user->id);
$penalty->org_unit($context_org);
$penalty->standing_penalty($ptype->id);
- $penalty->note($self->run_TT($env));
+
+ my $aum = Fieldmapper::actor::usr_message->new;
+ $aum->create_date('now');
+ $aum->sending_lib($context_org);
+ $aum->title('');
+ $aum->usr($penalty->usr);
+ $aum->message($self->run_TT($env));
+ $aum->pub(0);
+
+ $aum = $e->create_actor_usr_message($aum);
+ unless($aum) {
+ $e->rollback;
+ return 0;
+ }
+
+ $penalty->usr_message($aum->id);
unless($e->create_actor_user_standing_penalty($penalty)) {
$e->rollback;
title => $aum->title,
message => $aum->message,
create_date => $aum->create_date,
+ edit_date => $aum->edit_date,
is_read => defined($aum->read_date) ? 1 : 0,
library => $aum->sending_lib->name,
};
my $offset = $self->cgi->param('offset') || 0;
$self->ctx->{search_ou} = $self->_get_search_lib();
$self->ctx->{user}->notes(
- $self->editor->search_actor_usr_note({
+ $self->editor->search_actor_usr_message({
usr => $self->ctx->{user}->id,
pub => 't'
})
$penalty->set_date('now');
$penalty->staff($staff->id());
$penalty->org_unit(1); # Consortium-wide.
- $penalty->note('LP 1499123 csp.ignore_proximity test');
+ #$penalty->note('LP 1499123 csp.ignore_proximity test');
my $r = $apputils->simplereq(
'open-ils.actor',
'open-ils.actor.user.penalty.apply',
$penalty->set_date('now');
$penalty->staff($staff->id());
$penalty->org_unit(1); # Consortium-wide.
- $penalty->note('LP 1592891 SIP standing penalties test');
+ #$penalty->note('LP 1592891 SIP standing penalties test');
my $r = $apputils->simplereq(
'open-ils.actor',
'open-ils.actor.user.penalty.apply',
--- /dev/null
+#!perl
+use strict; use warnings;
+
+use Test::More tests => 12;
+use Data::Dumper;
+
+diag("Test actor.usr_message_penalty feature.");
+
+use OpenILS::Utils::TestUtils;
+use OpenILS::SIP::Patron;
+my $script = OpenILS::Utils::TestUtils->new();
+our $apputils = 'OpenILS::Application::AppUtils';
+
+use constant WORKSTATION_NAME => 'BR1-test-30-lp1846354_actor_usr_message_penalty.t';
+use constant WORKSTATION_LIB => 4;
+
+sub retrieve_user_by_barcode {
+ my $barcode = shift;
+ return $apputils->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.fleshed.retrieve_by_barcode',
+ $script->authtoken,
+ $barcode
+ );
+}
+
+sub retrieve_user_messages {
+ my $patron = shift;
+ return $apputils->simplereq(
+ 'open-ils.pcrud',
+ 'open-ils.pcrud.search.aum.atomic',
+ $script->authtoken,
+ { 'usr' => $patron->id() }
+ );
+}
+
+sub apply_staff_chr_to_patron_with_msg {
+ my $patron = shift;
+ my $penalty = Fieldmapper::actor::user_standing_penalty->new();
+ $penalty->standing_penalty(25);
+ $penalty->usr($patron->id());
+ $penalty->set_date('now');
+ $penalty->staff(1); # admin
+ $penalty->org_unit(1); # Consortium-wide.
+ my $msg = {
+ pub => 't',
+ title => 'lp1846354 test title',
+ message => 'lp1846354 test message'
+ };
+ my $r = $apputils->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.penalty.apply',
+ $script->authtoken,
+ $penalty,
+ $msg
+ );
+ if (ref($r)) {
+ undef($penalty);
+ } else {
+ $penalty->id($r);
+ }
+ return $penalty;
+}
+
+sub remove_staff_chr_from_patron {
+ my $penalty = shift;
+ return $apputils->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.penalty.remove',
+ $script->authtoken,
+ $penalty
+ );
+}
+
+# In concerto, we need to register a workstation.
+$script->authenticate({
+ username => 'admin',
+ password => 'demo123',
+ type => 'staff',
+});
+ok($script->authtoken, 'Initial Login');
+
+SKIP: {
+ my $ws = $script->find_workstation(WORKSTATION_NAME, WORKSTATION_LIB);
+ skip 'Workstation exists', 1 if ($ws);
+ $ws = $script->register_workstation(WORKSTATION_NAME, WORKSTATION_LIB) unless ($ws);
+ ok(! ref $ws, 'Registered a new workstation');
+}
+
+$script->logout();
+$script->authenticate({
+ username => 'admin',
+ password => 'demo123',
+ type => 'staff',
+ workstation => WORKSTATION_NAME
+});
+ok($script->authtoken, 'Login with workstaion');
+
+my $patron = retrieve_user_by_barcode("99999350419");
+isa_ok($patron, 'Fieldmapper::actor::user', 'Patron');
+
+# Patron should have no penalties.
+ok(! scalar(@{$patron->standing_penalties()}), 'Patron has no penalties');
+
+# Patron should have no user messages
+my $user_messages = retrieve_user_messages($patron);
+ok(! scalar(@{$user_messages}), 'Patron has no user messages');
+
+# Add the STAFF_CHR to the patron
+my $penalty = apply_staff_chr_to_patron_with_msg($patron);
+ok(ref $penalty, 'Added STAFF_CHR to patron');
+
+# Patron should have one user message
+$user_messages = retrieve_user_messages($patron);
+ok(scalar(@{$user_messages} == 1), 'Patron has a user message');
+
+# It should be public/patron-visible
+ok(@{$user_messages}[0]->pub() eq 't', 'User message pub flag is true');
+
+# It should be flagged as not deleted
+ok(@{$user_messages}[0]->deleted() eq 'f', 'User message is not flagged deleted');
+
+# We remove the STAFF_CHR from our test patron.
+my $r = remove_staff_chr_from_patron($penalty);
+ok( ! ref $r, 'STAFF_CHR removed from patron');
+
+# It should be flagged as not deleted
+$user_messages = retrieve_user_messages($patron);
+ok(@{$user_messages}[0]->deleted() eq 'f', 'User message is not flagged deleted');
+# worth noting that the Remove Note action in the staff client will delete both
+# the penalty and its user message
+
+$script->logout();
claims_returned_count INT NOT NULL DEFAULT 0,
credit_forward_balance NUMERIC(6,2) NOT NULL DEFAULT 0.00,
last_xact_id TEXT NOT NULL DEFAULT 'none',
- alert_message TEXT,
create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
expire_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() + '3 years'::INTERVAL),
claims_never_checked_out_count INT NOT NULL DEFAULT 0,
BEFORE INSERT OR UPDATE ON actor.usr
FOR EACH ROW EXECUTE PROCEDURE actor.user_ingest_name_keywords();
-CREATE TABLE actor.usr_note (
- id BIGSERIAL PRIMARY KEY,
- usr BIGINT NOT NULL REFERENCES actor.usr ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
- creator BIGINT NOT NULL REFERENCES actor.usr ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
- create_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
- pub BOOL NOT NULL DEFAULT FALSE,
- title TEXT NOT NULL,
- value TEXT NOT NULL
-);
-CREATE INDEX actor_usr_note_usr_idx ON actor.usr_note (usr);
-CREATE INDEX actor_usr_note_creator_idx ON actor.usr_note ( creator );
-
CREATE TABLE actor.usr_setting (
id BIGSERIAL PRIMARY KEY,
usr INT NOT NULL REFERENCES actor.usr ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
standing_penalty INT NOT NULL REFERENCES config.standing_penalty (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
staff INT REFERENCES actor.usr (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
set_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
- stop_date TIMESTAMP WITH TIME ZONE,
- note TEXT
+ stop_date TIMESTAMP WITH TIME ZONE
);
COMMENT ON TABLE actor.usr_standing_penalty IS $$
User standing penalties
create_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted BOOL NOT NULL DEFAULT FALSE,
read_date TIMESTAMP WITH TIME ZONE,
- sending_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
+ sending_lib INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
+ pub BOOL NOT NULL DEFAULT FALSE,
+ stop_date TIMESTAMP WITH TIME ZONE,
+ editor BIGINT REFERENCES actor.usr (id),
+ edit_date TIMESTAMP WITH TIME ZONE
+
);
CREATE INDEX aum_usr ON actor.usr_message (usr);
+ALTER TABLE actor.usr_standing_penalty ADD COLUMN usr_message BIGINT REFERENCES actor.usr_message(id);
CREATE RULE protect_usr_message_delete AS
ON DELETE TO actor.usr_message DO INSTEAD (
WHERE OLD.id = actor.usr_message.id
);
-CREATE FUNCTION actor.convert_usr_note_to_message () RETURNS TRIGGER AS $$
-DECLARE
- sending_ou INTEGER;
-BEGIN
- IF NEW.pub THEN
- IF TG_OP = 'UPDATE' THEN
- IF OLD.pub = TRUE THEN
- RETURN NEW;
- END IF;
- END IF;
-
- SELECT INTO sending_ou aw.owning_lib
- FROM auditor.get_audit_info() agai
- JOIN actor.workstation aw ON (aw.id = agai.eg_ws);
- IF sending_ou IS NULL THEN
- SELECT INTO sending_ou home_ou
- FROM actor.usr
- WHERE id = NEW.creator;
- END IF;
- INSERT INTO actor.usr_message (usr, title, message, sending_lib)
- VALUES (NEW.usr, NEW.title, NEW.value, sending_ou);
- END IF;
-
- RETURN NEW;
-END;
-$$ LANGUAGE PLPGSQL;
-
-CREATE TRIGGER convert_usr_note_to_message_tgr
- AFTER INSERT OR UPDATE ON actor.usr_note
- FOR EACH ROW EXECUTE PROCEDURE actor.convert_usr_note_to_message();
-
-- limited view to ensure that a library user who somehow
-- manages to figure out how to access pcrud cannot change
-- the text of messages sent them
CREATE VIEW actor.usr_message_limited
-AS SELECT * FROM actor.usr_message;
+AS SELECT * FROM actor.usr_message WHERE pub AND NOT deleted;
CREATE FUNCTION actor.restrict_usr_message_limited () RETURNS TRIGGER AS $$
BEGIN
INSTEAD OF UPDATE OR INSERT OR DELETE ON actor.usr_message_limited
FOR EACH ROW EXECUTE PROCEDURE actor.restrict_usr_message_limited();
+-- combined view of actor.usr_standing_penalty and actor.usr_message for populating
+-- staff Notes (formerly Messages) interface
+
+CREATE VIEW actor.usr_message_penalty
+AS SELECT
+ COALESCE(ausp.id::TEXT,'') || ':' || COALESCE(aum.id::TEXT,'') AS "id",
+ ausp.id AS "ausp_id",
+ aum.id AS "aum_id",
+ COALESCE(ausp.org_unit,aum.sending_lib) AS "org_unit",
+ ausp.org_unit AS "ausp_org_unit",
+ aum.sending_lib AS "aum_sending_lib",
+ COALESCE(ausp.usr,aum.usr) AS "usr",
+ ausp.usr as "ausp_usr",
+ aum.usr as "aum_usr",
+ ausp.standing_penalty AS "standing_penalty",
+ ausp.staff AS "staff",
+ LEAST(ausp.set_date,aum.create_date) AS "create_date",
+ ausp.set_date AS "ausp_set_date",
+ aum.create_date AS "aum_create_date",
+ LEAST(ausp.stop_date,aum.stop_date) AS "stop_date",
+ ausp.stop_date AS "ausp_stop_date",
+ aum.stop_date AS "aum_stop_date",
+ ausp.usr_message AS "ausp_usr_message",
+ aum.title AS "title",
+ aum.message AS "message",
+ aum.deleted AS "deleted",
+ aum.read_date AS "read_date",
+ aum.pub AS "pub",
+ aum.editor AS "editor",
+ aum.edit_date AS "edit_date"
+FROM
+ actor.usr_standing_penalty ausp
+FULL OUTER JOIN
+ actor.usr_message aum
+ON (
+ ausp.usr_message = aum.id
+)
+WHERE
+ NOT (ausp.id IS NULL AND aum.deleted);
+;
+
CREATE TABLE actor.passwd_type (
code TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
'VIEW_MERGE_PROFILE', 'ppl', 'description' )),
( 479, 'VIEW_SERIAL_SUBSCRIPTION', oils_i18n_gettext( 479,
'VIEW_SERIAL_SUBSCRIPTION', 'ppl', 'description' )),
- ( 480, 'VIEW_STANDING_PENALTY', oils_i18n_gettext( 480,
- 'VIEW_STANDING_PENALTY', 'ppl', 'description' )),
( 481, 'ADMIN_SERIAL_CAPTION_PATTERN', oils_i18n_gettext( 481,
'ADMIN_SERIAL_CAPTION_PATTERN', 'ppl', 'description' )),
( 482, 'ADMIN_SERIAL_DISTRIBUTION', oils_i18n_gettext( 482,
'VIEW_BOOKING_RESERVATION',
'VIEW_BOOKING_RESERVATION_ATTR_MAP',
'VIEW_GROUP_PENALTY_THRESHOLD',
- 'VIEW_STANDING_PENALTY',
'VOID_BILLING',
'VOLUME_HOLDS');
'VIEW_BOOKING_RESERVATION',
'VIEW_BOOKING_RESERVATION_ATTR_MAP',
'VIEW_REPORT_OUTPUT',
- 'VIEW_STANDING_PENALTY',
'VOID_BILLING',
'TRANSIT_CHECKIN_INTERVAL_BLOCK.override',
'VOLUME_HOLDS',
'coust', 'description'),
'bool', null)
-,( 'ui.patron.edit.au.alert_message.show', 'gui',
- oils_i18n_gettext('ui.patron.edit.au.alert_message.show',
- 'Show alert_message field on patron registration',
- 'coust', 'label'),
- oils_i18n_gettext('ui.patron.edit.au.alert_message.show',
- 'The alert_message field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.',
- 'coust', 'description'),
- 'bool', null)
-
-,( 'ui.patron.edit.au.alert_message.suggest', 'gui',
- oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest',
- 'Suggest alert_message field on patron registration',
- 'coust', 'label'),
- oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest',
- 'The alert_message field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.',
- 'coust', 'description'),
- 'bool', null)
-
,( 'ui.patron.edit.au.alias.show', 'gui',
oils_i18n_gettext('ui.patron.edit.au.alias.show',
'Show alias field on patron registration',
,( 'ui.staff.require_initials.patron_standing_penalty', 'gui',
oils_i18n_gettext('ui.staff.require_initials.patron_standing_penalty',
- 'Require staff initials for entry/edit of patron standing penalties and messages.',
+ 'Require staff initials for entry/edit of patron standing penalties and notes.',
'coust', 'label'),
oils_i18n_gettext('ui.staff.require_initials.patron_standing_penalty',
- 'Appends staff initials and edit date into patron standing penalties and messages.',
- 'coust', 'description'),
- 'bool', null)
-
-,( 'ui.staff.require_initials.patron_info_notes', 'gui',
- oils_i18n_gettext('ui.staff.require_initials.patron_info_notes',
- 'Require staff initials for entry/edit of patron notes.',
- 'coust', 'label'),
- oils_i18n_gettext('ui.staff.require_initials.patron_info_notes',
- 'Appends staff initials and edit date into patron note content.',
+ 'Require staff initials for entry/edit of patron standing penalties and notes.',
'coust', 'description'),
'bool', null)
-
,( 'ui.staff.require_initials.copy_notes', 'gui',
oils_i18n_gettext('ui.staff.require_initials.copy_notes',
'Require staff initials for entry/edit of copy notes.',
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+ALTER TABLE actor.usr_message ADD COLUMN pub BOOL NOT NULL DEFAULT FALSE;
+ALTER TABLE actor.usr_message ADD COLUMN stop_date TIMESTAMP WITH TIME ZONE;
+ALTER TABLE actor.usr_message ADD COLUMN editor BIGINT REFERENCES actor.usr (id);
+ALTER TABLE actor.usr_message ADD COLUMN edit_date TIMESTAMP WITH TIME ZONE;
+
+DROP VIEW actor.usr_message_limited;
+CREATE VIEW actor.usr_message_limited
+AS SELECT * FROM actor.usr_message WHERE pub AND NOT deleted;
+
+ALTER TABLE actor.usr_standing_penalty ADD COLUMN usr_message BIGINT REFERENCES actor.usr_message(id);
+
+-- alright, let's set all existing user messages to public
+
+UPDATE actor.usr_message SET pub = TRUE;
+
+-- alright, let's migrate penalty notes to usr_messages and link the messages back to the penalties:
+
+CREATE TABLE actor.XXXX_penalty_notes AS
+ SELECT id, usr, org_unit, set_date, note
+ FROM actor.usr_standing_penalty
+ WHERE NULLIF(BTRIM(note),'') IS NOT NULL;
+
+-- here is our staging table which will be shaped exactly like
+-- actor.usr_message and use the same id sequence
+CREATE TABLE actor.XXXX_usr_message_for_penalty_notes (
+ LIKE actor.usr_message INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+INSERT INTO actor.XXXX_usr_message_for_penalty_notes (
+ usr,
+ title,
+ message,
+ create_date,
+ sending_lib,
+ pub
+) SELECT
+ usr,
+ 'Penalty Note ID ' || id,
+ note,
+ set_date,
+ org_unit,
+ FALSE
+FROM
+ actor.XXXX_penalty_notes
+;
+
+-- so far so good, let's push this into production
+
+INSERT INTO actor.usr_message
+ SELECT * FROM actor.XXXX_usr_message_for_penalty_notes;
+
+-- and link the production penalties to these new user messages
+
+UPDATE actor.usr_standing_penalty p SET usr_message = m.id
+ FROM actor.XXXX_usr_message_for_penalty_notes m
+ WHERE m.title = 'Penalty Note ID ' || p.id;
+
+-- and remove the temporary overloading of the message title we used for this:
+
+UPDATE
+ actor.usr_message
+SET
+ title = 'Penalty Note'
+WHERE
+ id IN (SELECT id FROM actor.XXXX_usr_message_for_penalty_notes)
+;
+
+-- probably redundant here, but the spec calls for an assertion before removing
+-- the note column from actor.usr_standing_penalty, so being extra cautious:
+
+do $$ begin
+ assert (
+ select count(*)
+ from actor.XXXX_usr_message_for_penalty_notes
+ where id not in (
+ select id from actor.usr_message
+ )
+ ) = 0, 'failed migrating to actor.usr_message';
+end; $$;
+
+ALTER TABLE actor.usr_standing_penalty DROP COLUMN note;
+
+-- combined view of actor.usr_standing_penalty and actor.usr_message for populating
+-- staff Notes (formerly Messages) interface
+
+CREATE VIEW actor.usr_message_penalty
+AS SELECT
+ COALESCE(ausp.id::TEXT,'') || ':' || COALESCE(aum.id::TEXT,'') AS "id",
+ ausp.id AS "ausp_id",
+ aum.id AS "aum_id",
+ COALESCE(ausp.org_unit,aum.sending_lib) AS "org_unit",
+ ausp.org_unit AS "ausp_org_unit",
+ aum.sending_lib AS "aum_sending_lib",
+ COALESCE(ausp.usr,aum.usr) AS "usr",
+ ausp.usr as "ausp_usr",
+ aum.usr as "aum_usr",
+ ausp.standing_penalty AS "standing_penalty",
+ ausp.staff AS "staff",
+ LEAST(ausp.set_date,aum.create_date) AS "create_date",
+ ausp.set_date AS "ausp_set_date",
+ aum.create_date AS "aum_create_date",
+ LEAST(ausp.stop_date,aum.stop_date) AS "stop_date",
+ ausp.stop_date AS "ausp_stop_date",
+ aum.stop_date AS "aum_stop_date",
+ ausp.usr_message AS "ausp_usr_message",
+ aum.title AS "title",
+ aum.message AS "message",
+ aum.deleted AS "deleted",
+ aum.read_date AS "read_date",
+ aum.pub AS "pub",
+ aum.editor AS "editor",
+ aum.edit_date AS "edit_date"
+FROM
+ actor.usr_standing_penalty ausp
+FULL OUTER JOIN
+ actor.usr_message aum
+ON (
+ ausp.usr_message = aum.id
+)
+WHERE
+ NOT (ausp.id IS NULL AND aum.deleted);
+;
+
+-- fun part where we migrate the following alert messages:
+
+CREATE TABLE actor.XXXX_note_and_message_consolidation AS
+ SELECT id, home_ou, alert_message
+ FROM actor.usr
+ WHERE NOT deleted AND NULLIF(BTRIM(alert_message),'') IS NOT NULL;
+
+-- here is our staging table which will be shaped exactly like
+-- actor.usr_message and use the same id sequence
+CREATE TABLE actor.XXXX_usr_message (
+ LIKE actor.usr_message INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+INSERT INTO actor.XXXX_usr_message (
+ usr,
+ title,
+ message,
+ create_date,
+ sending_lib,
+ pub
+) SELECT
+ id,
+ 'converted Alert Message, real date unknown',
+ alert_message,
+ NOW(), -- best we can do
+ 1, -- it's this or home_ou
+ FALSE
+FROM
+ actor.XXXX_note_and_message_consolidation
+;
+
+-- another staging table, but for actor.usr_standing_penalty
+CREATE TABLE actor.XXXX_usr_standing_penalty (
+ LIKE actor.usr_standing_penalty INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+INSERT INTO actor.XXXX_usr_standing_penalty (
+ org_unit,
+ usr,
+ standing_penalty,
+ staff,
+ set_date,
+ usr_message
+) SELECT
+ sending_lib,
+ usr,
+ 20, -- ALERT_NOTE
+ 1, -- admin user, usually; best we can do
+ create_date,
+ id
+FROM
+ actor.XXXX_usr_message
+;
+
+-- so far so good, let's push these into production
+
+INSERT INTO actor.usr_message
+ SELECT * FROM actor.XXXX_usr_message;
+INSERT INTO actor.usr_standing_penalty
+ SELECT * FROM actor.XXXX_usr_standing_penalty;
+
+-- probably redundant here, but the spec calls for an assertion before removing
+-- the alert message column from actor.usr, so being extra cautious:
+
+do $$ begin
+ assert (
+ select count(*)
+ from actor.XXXX_usr_message
+ where id not in (
+ select id from actor.usr_message
+ )
+ ) = 0, 'failed migrating to actor.usr_message';
+end; $$;
+
+do $$ begin
+ assert (
+ select count(*)
+ from actor.XXXX_usr_standing_penalty
+ where id not in (
+ select id from actor.usr_standing_penalty
+ )
+ ) = 0, 'failed migrating to actor.usr_standing_penalty';
+end; $$;
+
+-- WARNING: we're going to lose the history of alert_message
+ALTER TABLE actor.usr DROP COLUMN alert_message CASCADE;
+SELECT auditor.update_auditors();
+
+-- fun part where we migrate actor.usr_notes as penalties to preserve
+-- their creator, and then the private ones to private user messages.
+-- For public notes, we try to link to existing user messages if we
+-- can, but if we can't, we'll create new, but archived, user messages
+-- for the note contents.
+
+CREATE TABLE actor.XXXX_usr_message_for_private_notes (
+ LIKE actor.usr_message INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+INSERT INTO actor.XXXX_usr_message_for_private_notes (
+ usr,
+ title,
+ message,
+ create_date,
+ sending_lib,
+ pub
+) SELECT
+ usr,
+ title,
+ value,
+ create_date,
+ (select home_ou from actor.usr where id = creator), -- best we can do
+ FALSE
+FROM
+ actor.usr_note
+WHERE
+ NOT pub
+;
+
+CREATE TABLE actor.XXXX_usr_message_for_unmatched_public_notes (
+ LIKE actor.usr_message INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+INSERT INTO actor.XXXX_usr_message_for_unmatched_public_notes (
+ usr,
+ title,
+ message,
+ create_date,
+ stop_date,
+ sending_lib,
+ pub
+) SELECT
+ usr,
+ title,
+ value,
+ create_date,
+ NOW(), -- we want these "archived" since the patron probably already saw a corresponding usr_message
+ (select home_ou from actor.usr where id = creator), -- best we can do
+ FALSE
+FROM
+ actor.usr_note n
+WHERE
+ pub AND NOT EXISTS (SELECT 1 FROM actor.usr_message m WHERE n.usr = m.usr AND n.create_date = m.create_date)
+;
+
+-- now, in order to preserve the creator from usr_note, we want to create standing SILENT_NOTE penalties for
+-- 1) actor.XXXX_usr_message_for_private_notes and associated usr_note entries
+-- 2) actor.XXXX_usr_message_for_unmatched_public_notes and associated usr_note entries, but archive these
+-- 3) usr_note and usr_message entries that can be matched
+
+CREATE TABLE actor.XXXX_usr_standing_penalties_for_notes (
+ LIKE actor.usr_standing_penalty INCLUDING DEFAULTS EXCLUDING CONSTRAINTS
+);
+
+-- 1) actor.XXXX_usr_message_for_private_notes and associated usr_note entries
+INSERT INTO actor.XXXX_usr_standing_penalties_for_notes (
+ org_unit,
+ usr,
+ standing_penalty,
+ staff,
+ set_date,
+ stop_date,
+ usr_message
+) SELECT
+ m.sending_lib,
+ m.usr,
+ 21, -- SILENT_NOTE
+ n.creator,
+ m.create_date,
+ m.stop_date,
+ m.id
+FROM
+ actor.usr_note n,
+ actor.XXXX_usr_message_for_private_notes m
+WHERE
+ n.usr = m.usr AND n.create_date = m.create_date AND NOT n.pub AND NOT m.pub
+;
+
+-- 2) actor.XXXX_usr_message_for_unmatched_public_notes and associated usr_note entries, but archive these
+INSERT INTO actor.XXXX_usr_standing_penalties_for_notes (
+ org_unit,
+ usr,
+ standing_penalty,
+ staff,
+ set_date,
+ stop_date,
+ usr_message
+) SELECT
+ m.sending_lib,
+ m.usr,
+ 21, -- SILENT_NOTE
+ n.creator,
+ m.create_date,
+ m.stop_date,
+ m.id
+FROM
+ actor.usr_note n,
+ actor.XXXX_usr_message_for_unmatched_public_notes m
+WHERE
+ n.usr = m.usr AND n.create_date = m.create_date AND n.pub AND m.pub
+;
+
+-- 3) usr_note and usr_message entries that can be matched
+INSERT INTO actor.XXXX_usr_standing_penalties_for_notes (
+ org_unit,
+ usr,
+ standing_penalty,
+ staff,
+ set_date,
+ stop_date,
+ usr_message
+) SELECT
+ m.sending_lib,
+ m.usr,
+ 21, -- SILENT_NOTE
+ n.creator,
+ m.create_date,
+ m.stop_date,
+ m.id
+FROM
+ actor.usr_note n,
+ actor.usr_message m
+WHERE
+ n.usr = m.usr AND n.create_date = m.create_date AND m.id NOT IN (
+ SELECT id FROM actor.XXXX_usr_message_for_private_notes
+ UNION
+ SELECT id FROM actor.XXXX_usr_message_for_unmatched_public_notes
+ )
+;
+
+-- so far so good, let's push these into production
+
+INSERT INTO actor.usr_message
+ SELECT * FROM actor.XXXX_usr_message_for_private_notes
+ UNION SELECT * FROM actor.XXXX_usr_message_for_unmatched_public_notes;
+INSERT INTO actor.usr_standing_penalty
+ SELECT * FROM actor.XXXX_usr_standing_penalties_for_notes;
+
+-- probably redundant here, but the spec calls for an assertion before dropping
+-- the actor.usr_note table, so being extra cautious:
+
+do $$ begin
+ assert (
+ select count(*)
+ from actor.XXXX_usr_message_for_private_notes
+ where id not in (
+ select id from actor.usr_message
+ )
+ ) = 0, 'failed migrating to actor.usr_message';
+end; $$;
+
+DROP TABLE actor.usr_note;
+
+-- preserve would-be collisions for migrating
+-- ui.staff.require_initials.patron_info_notes
+-- to ui.staff.require_initials.patron_standing_penalty
+
+\o ui.staff.require_initials.patron_info_notes.collisions.txt
+SELECT a.*
+FROM actor.org_unit_setting a
+WHERE
+ a.name = 'ui.staff.require_initials.patron_info_notes'
+ -- hits on org_unit
+ AND a.org_unit IN (
+ SELECT b.org_unit
+ FROM actor.org_unit_setting b
+ WHERE b.name = 'ui.staff.require_initials.patron_standing_penalty'
+ )
+ -- but doesn't hit on org_unit + value
+ AND CONCAT_WS('|',a.org_unit::TEXT,a.value::TEXT) NOT IN (
+ SELECT CONCAT_WS('|',b.org_unit::TEXT,b.value::TEXT)
+ FROM actor.org_unit_setting b
+ WHERE b.name = 'ui.staff.require_initials.patron_standing_penalty'
+ );
+\o
+
+-- and preserve the _log data
+
+\o ui.staff.require_initials.patron_info_notes.log_data.txt
+SELECT *
+FROM config.org_unit_setting_type_log
+WHERE field_name = 'ui.staff.require_initials.patron_info_notes';
+\o
+
+-- migrate the non-collisions
+
+INSERT INTO actor.org_unit_setting (org_unit, name, value)
+SELECT a.org_unit, 'ui.staff.require_initials.patron_standing_penalty', a.value
+FROM actor.org_unit_setting a
+WHERE
+ a.name = 'ui.staff.require_initials.patron_info_notes'
+ AND a.org_unit NOT IN (
+ SELECT b.org_unit
+ FROM actor.org_unit_setting b
+ WHERE b.name = 'ui.staff.require_initials.patron_standing_penalty'
+ )
+;
+
+-- and now delete the old patron_info_notes settings
+
+DELETE FROM actor.org_unit_setting
+ WHERE name = 'ui.staff.require_initials.patron_info_notes';
+DELETE FROM config.org_unit_setting_type_log
+ WHERE field_name = 'ui.staff.require_initials.patron_info_notes';
+DELETE FROM config.org_unit_setting_type
+ WHERE name = 'ui.staff.require_initials.patron_info_notes';
+
+-- relabel the org unit setting type
+
+UPDATE config.org_unit_setting_type
+SET
+ label = oils_i18n_gettext('ui.staff.require_initials.patron_standing_penalty',
+ 'Require staff initials for entry/edit of patron standing penalties and notes.',
+ 'coust', 'label'),
+ description = oils_i18n_gettext('ui.staff.require_initials.patron_standing_penalty',
+ 'Require staff initials for entry/edit of patron standing penalties and notes.',
+ 'coust', 'description')
+WHERE
+ name = 'ui.staff.require_initials.patron_standing_penalty'
+;
+
+-- preserve _log data for some different settings on their way out
+
+\o ui.patron.edit.au.alert_message.show_suggest.log_data.txt
+SELECT *
+FROM config.org_unit_setting_type_log
+WHERE field_name IN (
+ 'ui.patron.edit.au.alert_message.show',
+ 'ui.patron.edit.au.alert_message.suggest'
+);
+\o
+
+-- remove patron editor alert message settings
+
+DELETE FROM actor.org_unit_setting
+ WHERE name = 'ui.patron.edit.au.alert_message.show';
+DELETE FROM config.org_unit_setting_type_log
+ WHERE field_name = 'ui.patron.edit.au.alert_message.show';
+DELETE FROM config.org_unit_setting_type
+ WHERE name = 'ui.patron.edit.au.alert_message.show';
+
+DELETE FROM actor.org_unit_setting
+ WHERE name = 'ui.patron.edit.au.alert_message.suggest';
+DELETE FROM config.org_unit_setting_type_log
+ WHERE field_name = 'ui.patron.edit.au.alert_message.suggest';
+DELETE FROM config.org_unit_setting_type
+ WHERE name = 'ui.patron.edit.au.alert_message.suggest';
+
+-- comment these out if you want the staging tables to stick around
+DROP TABLE actor.XXXX_note_and_message_consolidation;
+DROP TABLE actor.XXXX_penalty_notes;
+DROP TABLE actor.XXXX_usr_message_for_penalty_notes;
+DROP TABLE actor.XXXX_usr_message;
+DROP TABLE actor.XXXX_usr_standing_penalty;
+DROP TABLE actor.XXXX_usr_message_for_private_notes;
+DROP TABLE actor.XXXX_usr_message_for_unmatched_public_notes;
+DROP TABLE actor.XXXX_usr_standing_penalties_for_notes;
+
+COMMIT;
+
<tr fmclass='au' fmfield='master_account'></tr>
<tr fmclass='au' fmfield='claims_returned_count' wclass='dijit.form.NumberSpinner' wconstraints="{min:0,places:0}" wvalue='0'></tr>
<tr fmclass='au' fmfield='claims_never_checked_out_count' wclass='dijit.form.NumberSpinner' wconstraints="{min:0,places:0}" wvalue='0'></tr>
- <tr fmclass='au' fmfield='alert_message' wclass='dijit.form.Textarea' wstyle='height:5em'></tr>
<tr class='divider hidden' id='uedit-settings-divider'><td colspan='0' id='userSettings'></td></tr>
<tr class='hidden' id='uedit-user-setting-template'>
<input type="checkbox" name="message_id" value="[% message.id %]"
[% html_text_attr('title', l('Select message [_1]', message.title)) %]/>
</td>
+ [% IF message.edit_date %]
+ <td>[% date.format(ctx.parse_datetime(message.edit_date), DATE_FORMAT); %]</td>
+ [% ELSE %]
<td>[% date.format(ctx.parse_datetime(message.create_date), DATE_FORMAT); %]</td>
+ [% END %]
<td>[% message.library | html %]</td>
<td><a href="[% mkurl('messages', { single => 1, message_id => message.id } ) %]">[% message.title | html %]</a></td>
</tr>
class='color_4 light_border'>[% l("Date") %]</td>
<td class='myopac_message_date'>[% date.format(ctx.parse_datetime(ctx.patron_messages.0.create_date), DATE_FORMAT); %]</td>
</tr>
+ [% IF ctx.patron_messages.0.edit_date %]
+ <tr>
+ <td width='30%'
+ class='color_4 light_border'>[% l("Edit Date") %]</td>
+ <td class='myopac_message_date'>[% date.format(ctx.parse_datetime(ctx.patron_messages.0.edit_date), DATE_FORMAT); %]</td>
+ </tr>
+ [% END %]
<tr>
<td width='30%'
class='color_4 light_border'>[% l("Library") %]</td>
s.PAGE_TITLE_PATRON_SEARCH = "[% l('Patron Search') %]";
s.PAGE_TITLE_PATRON_NAME = "[% l('[_1], [_2] [_3]', '{{lname}}','{{fname}}','{{mname}}') %]";
s.PAGE_TITLE_PATRON_CHECKOUT = "[% l('Checkout') %]";
- s.PAGE_TITLE_PATRON_MESSAGES = "[% l('Messages') %]";
+ s.PAGE_TITLE_PATRON_MESSAGES = "[% l('Notes') %]";
/* TODO: The "Other" page title could be smarter.. */
s.PAGE_TITLE_PATRON_OTHER = "[% l('Other') %]";
s.PAGE_TITLE_PATRON_BILLS = "[% l('Bills') %]";
<span ng-if="patron().name_keywords()"> <a title="[% l('Name keywords: ') %]{{patron().name_keywords()}}" class="glyphicon glyphicon-tags"></a>
</div>
- <p ng-show="patron().notes().length > 0"><a class='patron-summary-has-notes' href="./circ/patron/{{patron().id()}}/notes"><span class="label label-warning">Notes {{patron().notes().length}}</span></a></p>
<div ng-show="tab != 'search'">
<a href ng-click="toggle_expand_summary()"
title="[% l('Collapse Patron Summary Display') %]"
</a>
</li>
<li ng-class="{active : tab == 'messages', disabled : !patron()}">
- <a a-disabled="!patron()" href="./circ/patron/{{patron().id()}}/messages">[% l('Messages') %]</a>
+ <a a-disabled="!patron()" href="./circ/patron/{{patron().id()}}/messages">[% l('Notes') %]
+ <span ng-if="visible_notes().length > 0">
+ ({{visible_notes().length}})
+ </span>
+ </a>
</li>
<li ng-class="{active : tab == 'edit', disabled : !patron()}">
<a href="./circ/patron/{{patron().id()}}/edit">[% l('Edit') %]</a>
<ul uib-dropdown-menu>
<li>
<a href="./circ/patron/{{patron().id()}}/alerts">
- [% l('Display Alert and Messages') %]
- </a>
- </li>
- <li>
- <a href="./circ/patron/{{patron().id()}}/notes">
- [% l('Notes') %]
+ [% l('Display Alerts') %]
</a>
</li>
<li>
[% l('Patron account has invalid addresses.') %]
</div>
- <!-- alert message -->
- <div class="row" ng-if="patron().alert_message()">
- <div class="col-md-12">
- <div class="panel panel-warning">
- <div class="panel-heading">
- <div class="panel-title text-center">[% l('Alert Message') %]</div>
- </div>
- <div class="panel-body">
- {{patron().alert_message()}}
- </div>
- </div>
- </div>
- </div>
-
<!-- penalties -->
<div class="row" ng-if="alert_penalties().length">
<div class="col-md-12">
<div class="panel panel-warning">
<div class="panel-heading">
- <div class="panel-title text-center">[% l('Penalties') %]</div>
+ <div class="panel-title text-center">[% l('Alerts') %]</div>
</div>
<div class="panel-body">
<div class="row"
{{penalty.org_unit().shortname()}}
</div>
<div class="col-md-8"
- title="{{penalty.standing_penalty().name()}}">
- {{penalty.standing_penalty().label()}}
- <div>{{penalty.note()}}</div><!-- force newline -->
+ title="{{penalty.standing_penalty().name()}} (id {{penalty.id()}})">
+ {{penalty.usr_message().title() || penalty.standing_penalty().label()}}
+ <div>{{penalty.usr_message().message()}}</div><!-- force newline -->
+ <div> </div><!-- should use CSS for this, but spacing out the notes -->
</div>
<div class="col-md-2">
{{penalty.set_date() | date:$root.egDateFormat}}
</div>
</div>
-<!-- ALERT_MESSAGE -->
-
-<div class="row reg-field-row" ng-show="show_field('au.alert_message')">
- [% draw_field_label('au', 'alert_message') %]
- <div class="col-md-3 reg-field-input">
- <textarea
- class="form-control"
- aria-labelledby="{{idl_fields.au.alert_message.name}}"
- ng-model="patron.alert_message"
- ng-pattern="field_pattern('au', 'alert_message')"
- ng-change="field_modified()"
- ng-blur="handle_field_changed(patron, 'alert_message')">
- </textarea>
- </div>
- <div class="col-md-6 patron-reg-example">
- [% draw_example_text('au', 'alert_message') %]
- </div>
-</div>
-
<div ng-if="!offline">
<div class="alert alert-success row" role="alert">
-
-<div class="strong-text-2">[% l('Penalties and Messages') %]</div>
+<div class="strong-text-2">[% l('Notes') %]</div>
<div class="pad-vert"></div>
<eg-grid
- idl-class="ausp"
+ idl-class="aump"
grid-controls="activeGridControls"
dateformat="{{$root.egDateAndTimeFormat}}"
persist-key="circ.patron.staff_messages">
<eg-grid-menu-item handler="createPenalty"
- label="[% l('Apply Penalty / Message') %]"></eg-grid-menu-item>
+ label="[% l('Create Note') %]"></eg-grid-menu-item>
- <eg-grid-action label="[% l('Remove Penalty / Message') %]"
- handler="removePenalty"></eg-grid-action>
- <eg-grid-action label="[% l('Modify Penalty / Message') %]"
+ <eg-grid-action label="[% l('Remove Note') %]"
+ disabled="test_for_disable_remove_penalty" handler="removePenalty"></eg-grid-action>
+ <eg-grid-action label="[% l('Edit Note') %]"
handler="editPenalty"></eg-grid-action>
- <eg-grid-action label="[% l('Archive Penalty / Message') %]"
+ <eg-grid-action label="[% l('Archive Note') %]"
handler="archivePenalty"></eg-grid-action>
- <eg-grid-field path="set_date" label="[% l('Applied On') %]" datatype="timestamp"></eg-grid-field>
- <eg-grid-field path="standing_penalty.label"></eg-grid-field>
- <eg-grid-field path="org_unit.shortname" label="[% l('Library') %]"></eg-grid-field>
- <eg-grid-field path="note"></eg-grid-field>
- <eg-grid-field path="id" required hidden></eg-grid-field>
+ <eg-grid-field path="read_date"></eg-grid-field>
+ <eg-grid-field path="deleted" required hidden></eg-grid-field>
+ <eg-grid-field path="usr.usrname" label="[% l('User') %]" required hidden></eg-grid-field>
+ <eg-grid-field path="message" label="[% l('Note Text') %]"></eg-grid-field>
+ <eg-grid-field path="title"></eg-grid-field>
+ <eg-grid-field path="pub"></eg-grid-field>
+ <eg-grid-field path="stop_date" hidden></eg-grid-field>
+ <eg-grid-field path="editor.usrname" label="[% l('Editor') %]" required hidden></eg-grid-field>
+ <eg-grid-field path="edit_date" hidden></eg-grid-field>
+ <eg-grid-field path="staff.usrname" label="[% l('Staff') %]" required></eg-grid-field>
+ <eg-grid-field path="standing_penalty.name" required label="[% l('Penalty Name') %]"></eg-grid-field>
<eg-grid-field path="standing_penalty.block_list" required hidden></eg-grid-field>
<eg-grid-field path="standing_penalty.*" hidden></eg-grid-field>
-
+ <eg-grid-field path="org_unit.shortname" label="[% l('Location') %]" required></eg-grid-field>
+ <eg-grid-field path="create_date"></eg-grid-field>
+ <eg-grid-field path="ausp_id" required hidden></eg-grid-field>
+ <eg-grid-field path="aum_id" required hidden></eg-grid-field>
+ <eg-grid-field path="id" required hidden></eg-grid-field>
</eg-grid>
<div class="pad-vert"><hr/></div>
<div class="pad-vert row padded">
<div class="col-md-4">
- <div class="strong-text-2">[% l('Archived Penalties / Messages') %]</div>
+ <div class="strong-text-2">[% l('Archived Notes') %]</div>
</div>
<div class="col-md-4">
<label>[% l('Set Date Start:') %]</label>
</div>
</div>
<eg-grid
- idl-class="ausp"
+ idl-class="aump"
grid-controls="archiveGridControls"
dateformat="{{$root.egDateAndTimeFormat}}"
+ features="-multiselect"
persist-key="circ.patron.archived_messages">
- <eg-grid-field path="set_date" label="[% l('Applied On') %]" datatype="timestamp"></eg-grid-field>
- <eg-grid-field path="standing_penalty.label"></eg-grid-field>
- <eg-grid-field path="org_unit.shortname" label="[% l('Library') %]"></eg-grid-field>
- <eg-grid-field path="note"></eg-grid-field>
- <eg-grid-field path="id" required hidden></eg-grid-field>
+ <eg-grid-field path="read_date"></eg-grid-field>
+ <eg-grid-field path="deleted" required hidden></eg-grid-field>
+ <eg-grid-field path="usr.usrname" label="[% l('User') %]" required hidden></eg-grid-field>
+ <eg-grid-field path="message" label="[% l('Note Text') %]"></eg-grid-field>
+ <eg-grid-field path="title"></eg-grid-field>
+ <eg-grid-field path="pub"></eg-grid-field>
+ <eg-grid-field path="stop_date" hidden></eg-grid-field>
+ <eg-grid-field path="editor.usrname" label="[% l('Editor') %]" required hidden></eg-grid-field>
+ <eg-grid-field path="edit_date" hidden></eg-grid-field>
+ <eg-grid-field path="staff.usrname" label="[% l('Staff') %]" required></eg-grid-field>
+ <eg-grid-field path="standing_penalty.name" required label="[% l('Penalty Name') %]"></eg-grid-field>
<eg-grid-field path="standing_penalty.block_list" required hidden></eg-grid-field>
<eg-grid-field path="standing_penalty.*" hidden></eg-grid-field>
-
+ <eg-grid-field path="org_unit.shortname" label="[% l('Location') %]" required></eg-grid-field>
+ <eg-grid-field path="create_date"></eg-grid-field>
+ <eg-grid-field path="ausp_id" required hidden></eg-grid-field>
+ <eg-grid-field path="aum_id" required hidden></eg-grid-field>
+ <eg-grid-field path="id" required hidden></eg-grid-field>
</eg-grid>
ng-repeat="penalty in alert_penalties()">
<div
class="col-md-9 patron-summary-alert"
- title="{{penalty.standing_penalty().name()}}">
- {{penalty.note() || penalty.standing_penalty().label()}}
+ title="{{penalty.standing_penalty().name()}} (id {{penalty.id()}}): {{penalty.usr_message().message()}}">
+ {{penalty.usr_message().title() || penalty.standing_penalty().label() | limitTo: 40}}
</div>
<div class="col-md-3">
{{penalty.set_date() | date:$root.egDateFormat}}
</div>
</div>
- <div class="row patron-summary-divider">
- <div
- class="col-md-9 patron-summary-alert"
- title="[% l('Patron Alert Message') %]"
- ng-if="patron().alert_message()">
- {{patron().alert_message()}}
- </div>
- </div>
<div class="row" ng-class="{'patron-summary-alert' : doesPatronExpireSoon()}">
<div ng-if="doesPatronExpireSoon()" class="col-md-12">[% l('Patron account will expire soon. Please renew.') %]</div>
</div>
<div class="col-md-8 patron_photo_wrap"><img class="img-responsive img-rounded" src="{{patron().photo_url()}}" alt=""></div>
</div>
<div class="row"
- ng-class="{'patron-summary-divider' : alert_penalties().length || patron().alert_message()}">
+ ng-class="{'patron-summary-divider' : alert_penalties().length}">
<div class="col-md-5">[% l('Profile') %]</div>
<div class="col-md-7">{{patron().profile().name()}}</div>
</div>
-<form ng-submit="ok(args)" role="form">
+<form id="patron-notes-container" ng-submit="ok(args)" role="form">
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()"
aria-hidden="true">×</button>
- <h4 class="modal-title">[% l('Apply Standing Penalty / Message') %]</h4>
+ <h4 class="modal-title">[% l('Create or Edit Note') %]</h4>
</div>
<div class="modal-body">
<div class="row">
- <div class="col-md-8">
+ <div class="col-md-6 pull-left">
<ul class="nav nav-pills">
<!-- 21 == SILENT_NOTE -->
<li ng-class="{active : args.penalty == 21}">
- <a href ng-click="args.penalty=21">[% l('Note') %]</a>
+ <a href ng-click="set_penalty(21);">[% l('Note') %]</a>
</li>
<!-- 20 == ALERT_NOTE -->
<li ng-class="{active : args.penalty == 20}">
- <a href ng-click="args.penalty=20">[% l('Alert') %]</a>
+ <a href ng-click="set_penalty(20);">[% l('Alert') %]</a>
</li>
<!-- 25 == STAFF_CHR -->
<li ng-class="{active : args.penalty == 25}">
- <a href ng-click="args.penalty=25">[% l('Block') %]</a>
+ <a href ng-click="set_penalty(25);">[% l('Block') %]</a>
</li>
</ul>
</div>
- <div class="col-md-4 pull-right nullable">
- <select class="form-control"
- ng-model="args.custom_penalty"
- ng-options="penalty.id() as penalty.label() for penalty in penalties">
- <option value="">[% l('Penalty Type') %]</option>
- </select>
+ <div class="col-md-6 pull-left">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6 nullable">
+ <label>[% l('Penalty Type:') %]
+ <select class="form-control"
+ ng-model="args.custom_penalty"
+ ng-disabled="(args.pub && args.read_date) || args.deleted"
+ ng-options="penalty.id() as penalty.label() for penalty in penalties">
+ </select>
+ </label>
+ </div>
+ <div class="col-md-6 pull-right">
+ <div>
+ <label ng-disabled="(args.pub && args.read_date) || args.deleted">[% l('Depth:') %]
+ <eg-share-depth-selector id="org_depth" useOpacLabel maxDepth="{{args.max_depth}}" ng-model="args.custom_depth"></eg-share-depth-selector>
+ </label>
+ </div>
+ <div>
+ <label>[% l('Location:') %]</label><span> {{args.org.shortname()}}</span>
+ <!--<eg-org-selector selected="args.org" onchange="update_org"
+ disable-test="cant_use_org"></eg-org-selector>-->
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3 pull-left">
+ <label>
+ <input type="checkbox" ng-model="args.pub"
+ ng-disabled="(args.pub && args.read_date) || args.deleted"/>
+ [% l('Patron Visible') %]
+ </label>
+ </div>
+ <div class="col-md-3 pull-left">
+ <label ng-if="args.pub && args.read_date">
+ [% l('Read on [_1]', '{{args.read_date | date:$root.egDateAndTimeFormat}}') %]
+ </label>
+ <label ng-if="args.pub && !args.read_date">
+ [% l('Unread') %]
+ </label>
+ </div>
+ <div class="col-md-3 pull-left">
+ <label ng-if="args.edit_date">
+ [% l('Edited on [_1] by [_2]', '{{args.edit_date | date:$root.egDateAndTimeFormat}}', '{{args.editor.usrname()}}') %]
+ </label>
+ </div>
+ <div class="col-md-3 pull-left">
+ <label class="patron-summary-alert" ng-if="args.deleted">
+ [% l('Deleted') %]
+ </label>
+ </div>
+ </div>
+ <div class="form-group row pad-vert">
+ <div class="col-md-12">
+ <textarea class="form-control"
+ ng-model="args.title" placeholder="[% l('Title...') %]"
+ ng-required="true" ng-disabled="(args.pub && args.read_date) || args.deleted">
+ </textarea>
</div>
</div>
<div class="form-group row pad-vert">
<div class="col-md-12">
<textarea class="form-control"
- ng-model="args.note" placeholder="[% l('Note...') %]">
+ ng-model="args.note" placeholder="[% l('Note Text...') %]"
+ ng-disabled="(args.pub && args.read_date) || args.deleted">
</textarea>
</div>
</div>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" ng-hide="!require_initials"
+ ng-disabled="(args.pub && args.read_date) || args.deleted"
ng-model="args.initials" placeholder="[% l('Initials') %]" ng-required="require_initials"/>
</div>
<div class="col-md-10 pull-right">
- <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="(args.pub && args.read_date) || args.deleted || !args.org" value="[% l('OK') %]"/>
<button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
</div>
</div>
/* Angular applies these classes based on the field's
* required and pattern settings */
+#patron-notes-container .ng-invalid-required,
#patron-reg-container .ng-invalid,
#patron-reg-container .ng-invalid-required,
#patron-pay-by-credit-form .ng-invalid {
<div>Is Group Lead Account: {{patron.master_account}}</div>
<div>Claims-Returned Count: {{patron.claims_returned_count}}</div>
<div>Claims-Never-Checked-Out Count: {{patron.claims_never_checked_out_count}}</div>
- <div>Alert Message: {{patron.alert_message}}</div>
<div>
<div ng-repeat="address in patron.addresses">
'ui.patron.edit.au.claims_returned_count.suggest',
'ui.patron.edit.au.claims_never_checked_out_count.show',
'ui.patron.edit.au.claims_never_checked_out_count.suggest',
- 'ui.patron.edit.au.alert_message.show',
- 'ui.patron.edit.au.alert_message.suggest',
'ui.patron.edit.aua.post_code.regex',
'ui.patron.edit.aua.post_code.example',
'ui.patron.edit.aua.county.require',
master_account : 'f',
claims_returned_count : '0',
claims_never_checked_out_count : '0',
- alert_message : 'Coat is in the lost-and-found behind the circ desk',
ident_type: {name: function() {return 'Drivers License'}},
ident_value: '11332445',
ident_type2: {name: function() {return 'Other'}},
});
}])
+.factory("hasPermAt",function(){
+ return {};
+})
+
.config(function($routeProvider, $locationProvider, $compileProvider) {
$locationProvider.html5Mode(true);
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
// data loaded at startup which only requires an authtoken goes
// here. this allows the requests to be run in parallel instead of
// waiting until startup has completed.
- var resolver = {delay : ['egCore','egUser', function(egCore , egUser) {
+ var resolver = {delay : ['egCore','egUser','hasPermAt', function(egCore , egUser , hasPermAt) {
// fetch the org settings we care about during egStartup
// and toss them into egCore.env as egCore.env.aous[name] = value.
]);
}
- return egCore.startup.go();
+ return egCore.startup.go().then(function(go_promise) {
+ // FIXME: the following is really just for PatronMessagesCtrl
+ // and PatronCtrl, so we could refactor to avoid calling it
+ // for every controller
+ return egCore.perm.hasPermFullPathAt('VIEW_USER')
+ .then(function(orgList) {
+ hasPermAt['VIEW_USER'] = orgList;
+ return go_promise;
+ });
+ });
}]};
$routeProvider.when('/circ/patron/search', {
*
* */
.controller('PatronCtrl',
- ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc','egCirc',
-function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc , egCirc) {
+ ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc','egCirc','hasPermAt',
+function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc , egCirc , hasPermAt) {
$scope.is_patron_edit = function() {
return Boolean($location.path().match(/patron\/\d+\/edit$/));
}
$scope.patron = function() { return patronSvc.current }
+ $scope.visible_notes = function() {
+ var p = patronSvc.current;
+ if (p) {
+ var org_ids = hasPermAt['VIEW_USER'];
+ var filtered_notes = p.notes().filter(function(n) { return org_ids.indexOf(n.org_unit()) > -1; });
+ return filtered_notes;
+ }
+ return [];
+ }
$scope.patron_stats = function() { return patronSvc.patron_stats }
$scope.summary_stat_cats = function() { return patronSvc.summary_stat_cats }
$scope.hasAlerts = function() { return patronSvc.hasAlerts }
* Manages messages
*/
.controller('PatronMessagesCtrl',
- ['$scope','$q','$routeParams','egCore','$uibModal','patronSvc','egCirc',
-function($scope , $q , $routeParams, egCore , $uibModal , patronSvc , egCirc) {
+ ['$scope','$q','$routeParams','egCore','$uibModal','patronSvc','egCirc','hasPermAt',
+function($scope , $q , $routeParams, egCore , $uibModal , patronSvc , egCirc , hasPermAt ) {
$scope.initTab('messages', $routeParams.id);
var usr_id = $routeParams.id;
- var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
+ var org_ids = hasPermAt.VIEW_USER;
// setup date filters
var start = new Date(); // now - 1 year
var activeGrid = $scope.activeGridControls = {
setSort : function() {
- return ['set_date'];
+ return ['create_date'];
},
setQuery : function() {
return {
var archiveGrid = $scope.archiveGridControls = {
setSort : function() {
- return ['set_date'];
+ return ['create_date'];
},
watchQuery : function() {
return {
usr : usr_id,
org_unit : org_ids,
stop_date : {'<=' : 'now'},
- set_date : {between : date_range()}
+ create_date : {between : date_range()}
};
}
};
+ $scope.test_for_disable_remove_penalty = function() {
+ var selected = $scope.activeGridControls.selectedItems();
+ var found_pub_and_read_and_not_deleted = false;
+ angular.forEach(selected, function(s) {
+ if (Boolean(s.pub == 't') && Boolean(s.read_date) && !Boolean(s.deleted == 't')) {
+ found_pub_and_read_and_not_deleted = true;
+ }
+ });
+ return found_pub_and_read_and_not_deleted;
+ }
+
$scope.removePenalty = function(selected) {
- // the grid stores flattened penalties. Fetch penalty objects first
+ if (selected.length == 0) return;
- var ids = selected.map(function(s){ return s.id });
- egCore.pcrud.search('ausp',
- {id : ids}, {},
- {atomic : true, authoritative : true}
+ // TODO: need confirmation dialog
- // then delete them
- ).then(function(penalties) {
- return egCore.pcrud.remove(penalties);
+ var promises = [];
+ // figure out the view components
+ var aum_ids = [];
+ var ausp_ids = [];
+ angular.forEach(selected, function(s) {
+ if (s.aum_id) { aum_ids.push(s.aum_id); }
+ if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
+ });
- // then refresh the grid
- }).then(function() {
+ // fetch all of them since trying to pull them
+ // off of patronSvc.current isn't reliable
+ if (ausp_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('ausp',
+ {id : ausp_ids}, {},
+ {atomic : true, authoritative : true}
+ ).then(function(penalties) {
+ return egCore.pcrud.remove(penalties);
+ })
+ );
+ }
+ if (aum_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('aum',
+ {id : aum_ids}, {},
+ {atomic : true, authoritative : true}
+ ).then(function(messages) {
+ return egCore.pcrud.remove(messages);
+ })
+ );
+ }
+ $q.all(promises).then(function() {
activeGrid.refresh();
+ archiveGrid.refresh();
+ // force a refresh of the user
+ patronSvc.setPrimary(patronSvc.current.id(), null, true);
});
}
$scope.archivePenalty = function(selected) {
- // the grid stores flattened penalties. Fetch penalty objects first
+ if (selected.length == 0) return;
- var ids = selected.map(function(s){ return s.id });
- egCore.pcrud.search('ausp',
- {id : ids}, {},
- {atomic : true, authoritative : true}
+ // TODO: need confirmation dialog
- // then delete them
- ).then(function(penalties) {
- angular.forEach(penalties, function(p){ p.stop_date('now') });
- return egCore.pcrud.update(penalties);
+ var promises = [];
+ // figure out the view components
+ var aum_ids = [];
+ var ausp_ids = [];
+ angular.forEach(selected, function(s) {
+ if (s.aum_id) { aum_ids.push(s.aum_id); }
+ if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
+ });
- // then refresh the grid
- }).then(function() {
+ // fetch all of them since trying to pull them
+ // off of patronSvc.current isn't reliable
+ if (ausp_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('ausp',
+ {id : ausp_ids}, {},
+ {atomic : true, authoritative : true}
+ ).then(function(penalties) {
+ angular.forEach(penalties, function(p) {
+ p.stop_date('now');
+ });
+ return egCore.pcrud.update(penalties);
+ })
+ );
+ }
+ if (aum_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('aum',
+ {id : aum_ids}, {},
+ {atomic : true, authoritative : true}
+ ).then(function(messages) {
+ angular.forEach(messages, function(m) {
+ m.stop_date('now');
+ });
+ return egCore.pcrud.update(messages);
+ })
+ );
+ }
+ $q.all(promises).then(function() {
activeGrid.refresh();
archiveGrid.refresh();
+ // force a refresh of the user
+ patronSvc.setPrimary(patronSvc.current.id(), null, true);
});
}
$scope.editPenalty = function(selected) {
if (selected.length == 0) return;
- // grab the penalty from the user object
- var penalty = patronSvc.current.standing_penalties().filter(
- function(p) {return p.id() == selected[0].id})[0];
+ var promises = [];
+ // figure out the view components
+ var aum_ids = []; var aum_objs = {};
+ var ausp_ids = []; var ausp_objs = {};
+ var pairs = [];
+ angular.forEach(selected, function(s) {
+ if (s.aum_id) { aum_ids.push(s.aum_id); }
+ if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
+ pairs.push( { aum_id : s.aum_id, ausp_id : s.ausp_id } );
+ });
- egCirc.edit_penalty(penalty).then(function() {
- activeGrid.refresh();
- // force a refresh of the user, since they may now
- // have blocking penalties, etc.
- patronSvc.setPrimary(patronSvc.current.id(), null, true);
+ // fetch all of them since trying to pull them
+ // off of patronSvc.current isn't reliable
+ // (we want deleted user messages too)
+ if (ausp_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('ausp',
+ {id : ausp_ids}, {},
+ {atomic : true, authoritative : true}
+ ).then(function(penalties) {
+ angular.forEach(penalties, function(p) {
+ ausp_objs[p.id()] = p;
+ });
+ return $q.when();
+ })
+ );
+ }
+ if (aum_ids.length > 0) {
+ promises.push(
+ egCore.pcrud.search('aum',
+ {id : aum_ids}, {
+ flesh : 1,
+ flesh_fields : {
+ aum : ['editor']
+ }
+ },
+ {atomic : true, authoritative : true}
+ ).then(function(messages) {
+ angular.forEach(messages, function(m) {
+ aum_objs[m.id()] = m;
+ });
+ return $q.when();
+ })
+ );
+ }
+ $q.all(promises).then(function() {
+ angular.forEach(pairs, function(pair) {
+ egCirc.edit_penalty(ausp_objs[pair.ausp_id],aum_objs[pair.aum_id]).then(function() {
+ activeGrid.refresh();
+ // force a refresh of the user, since they may now
+ // have blocking penalties, etc.
+ patronSvc.setPrimary(patronSvc.current.id(), null, true);
+ });
+ });
});
}
}])
'ui.patron.edit.au.claims_returned_count.suggest',
'ui.patron.edit.au.claims_never_checked_out_count.show',
'ui.patron.edit.au.claims_never_checked_out_count.suggest',
- 'ui.patron.edit.au.alert_message.show',
- 'ui.patron.edit.au.alert_message.suggest',
'ui.patron.edit.aua.post_code.regex',
'ui.patron.edit.aua.post_code.example',
'ui.patron.edit.aua.county.require',
});
}
- service.get_staff_penalty_types = function() {
+ service.get_all_penalty_types = function() {
if (egCore.env.csp)
return $q.when(egCore.env.csp.list);
- return egCore.pcrud.search(
- // id <= 100 are reserved for system use
- 'csp', {id : {'>': 100}}, {}, {atomic : true})
- .then(function(penalties) {
- return egCore.env.absorbList(penalties, 'csp').list;
- });
+ return egCore.pcrud.retrieveAll('csp', {}, {atomic : true}).then(
+ function(penalties) {
+ return egCore.env.absorbList(penalties, 'csp').list;
+ }
+ );
}
// ideally all of these data should be returned with the response,
});
}
+ function generate_penalty_dialog_watch_callback($scope,egCore,allPenalties) {
+ return function(newval) {
+ if (newval) {
+ var selected_penalty = allPenalties.filter(function(p) {
+ return p.id() == newval; })[0];
+ var penalty_id = selected_penalty.id();
+ if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
+ $scope.args.custom_penalty = penalty_id;
+ $scope.args.penalty = penalty_id;
+ }
+ if (penalty_id > 100) {
+ $scope.args.custom_penalty = penalty_id;
+ $scope.args.penalty = null;
+ }
+ // there's a $watch on custom_depth
+ if (selected_penalty.org_depth() || selected_penalty.org_depth() == 0) {
+ $scope.args.custom_depth = selected_penalty.org_depth();
+ } else {
+ $scope.args.custom_depth = $scope.args.org.ou_type().depth();
+ }
+ }
+ };
+ }
+
service.create_penalty = function(user_id) {
return $uibModal.open({
templateUrl: './circ/share/t_new_message_dialog',
backdrop: 'static',
controller:
- ['$scope','$uibModalInstance','staffPenalties',
- function($scope , $uibModalInstance , staffPenalties) {
+ ['$scope','$uibModalInstance','allPenalties','goodOrgs',
+ function($scope , $uibModalInstance , allPenalties , goodOrgs) {
$scope.focusNote = true;
- $scope.penalties = staffPenalties;
+ $scope.penalties = allPenalties.filter(
+ function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
+ $scope.set_penalty = function(id) {
+ if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
+ $scope.args.penalty = id;
+ }
+ }
$scope.require_initials = service.require_initials;
- $scope.args = {penalty : 21}; // default to Note
- $scope.setPenalty = function(id) {
- args.penalty = id;
+ $scope.update_org = function(org) {
+ if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
+ $scope.args.org = org;
+ }
+ }
+ $scope.cant_use_org = function(org_id) {
+ return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
}
+ $scope.args = {
+ pub : false,
+ penalty : 21, // default to Note
+ org : egCore.org.get(egCore.auth.user().ws_ou())
+ };
+ $scope.args.max_depth = $scope.args.org.ou_type().depth();
$scope.ok = function(count) { $uibModalInstance.close($scope.args) }
$scope.cancel = function($event) {
$uibModalInstance.dismiss();
$event.preventDefault();
}
+ $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
+ $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
+ $scope.$watch('args.custom_depth', function(org_depth) {
+ if (org_depth || org_depth == 0) {
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
+ egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
+ ).then(function(ctx_org) {
+ if (ctx_org) {
+ $scope.args.org = egCore.org.get(ctx_org);
+ }
+ });
+ }
+ });
}],
- resolve : { staffPenalties : service.get_staff_penalty_types }
+ resolve : {
+ allPenalties : service.get_all_penalty_types,
+ goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
+ }
}).result.then(
function(args) {
var pen = new egCore.idl.ausp();
+ var msg = {
+ pub : args.pub,
+ title : args.title,
+ message : args.note ? args.note : ''
+ };
pen.usr(user_id);
- pen.org_unit(egCore.auth.user().ws_ou());
- pen.note(args.note);
- if (args.initials) pen.note(args.note + ' [' + args.initials + ']');
+ pen.org_unit(args.org.id());
+ if (args.initials) msg.message = (args.note ? args.note : '') + ' [' + args.initials + ']';
if (args.custom_penalty) {
pen.standing_penalty(args.custom_penalty);
} else {
return egCore.net.request(
'open-ils.actor',
'open-ils.actor.user.penalty.apply',
- egCore.auth.token(), pen
+ egCore.auth.token(), pen, msg
);
}
);
}
// assumes, for now anyway, penalty type is fleshed onto usr_penalty.
- service.edit_penalty = function(usr_penalty) {
+ service.edit_penalty = function(pen,aum) {
return $uibModal.open({
templateUrl: './circ/share/t_new_message_dialog',
backdrop: 'static',
controller:
- ['$scope','$uibModalInstance','staffPenalties',
- function($scope , $uibModalInstance , staffPenalties) {
+ ['$scope','$uibModalInstance','allPenalties','goodOrgs',
+ function($scope , $uibModalInstance , allPenalties , goodOrgs) {
+ // We may need to vivicate usr_penalty (pen) or usr_message (aum)
+ if (!pen) {
+ pen = new egCore.idl.ausp();
+ pen.usr(aum.usr());
+ pen.org_unit(aum.sending_lib()); // FIXME: preserve sending_lib or use ws_ou?
+ pen.staff(egCore.auth.user().id());
+ pen.set_date('now');
+ pen.usr_message(aum.id());
+ pen.isnew(true);
+ aum.ischanged(true);
+ }
+ if (!aum) {
+ aum = new egCore.idl.aum();
+ aum.create_date('now');
+ aum.sending_lib(pen.org_unit());
+ aum.pub(false);
+ aum.usr(pen.usr());
+ aum.isnew(true);
+ pen.ischanged(true);
+ }
+
$scope.focusNote = true;
- $scope.penalties = staffPenalties;
+ $scope.penalties = allPenalties.filter(
+ function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
+ $scope.set_penalty = function(id) {
+ if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
+ $scope.args.penalty = id;
+ }
+ }
$scope.require_initials = service.require_initials;
+ $scope.update_org = function(org) {
+ if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
+ $scope.args.org = org;
+ }
+ }
+ $scope.cant_use_org = function(org_id) {
+ return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
+ }
+ var penalty_id = pen.standing_penalty();
$scope.args = {
- penalty : usr_penalty.standing_penalty().id(),
- note : usr_penalty.note()
+ penalty : pen.isnew()
+ ? 21 // default to Note
+ : penalty_id,
+ pub : typeof aum.pub() == 'boolean'
+ ? aum.pub()
+ : aum.pub() == 't',
+ title : aum.title(),
+ note : aum.message() ? aum.message() : '',
+ org : egCore.org.get(pen.org_unit()),
+ deleted : typeof aum.deleted() == 'boolean'
+ ? aum.deleted()
+ : aum.deleted() == 't',
+ read_date : aum.read_date(),
+ edit_date : aum.edit_date(),
+ editor : aum.editor()
+ }
+ $scope.args.max_depth = $scope.args.org.ou_type().depth();
+ $scope.original_org = $scope.args.org;
+ $scope.workstation_depth = egCore.org.get(egCore.auth.user().ws_ou()).ou_type().depth();
+ if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
+ $scope.args.custom_penalty = penalty_id;
+ }
+ if (penalty_id > 100) {
+ $scope.args.custom_penalty = penalty_id;
+ $scope.args.penalty = null;
}
- $scope.setPenalty = function(id) { args.penalty = id; }
$scope.ok = function(count) { $uibModalInstance.close($scope.args) }
$scope.cancel = function($event) {
$uibModalInstance.dismiss();
$event.preventDefault();
}
+ $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
+ $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
+ $scope.$watch('args.custom_depth', function(org_depth) {
+ if (org_depth || org_depth == 0) {
+ if (org_depth > $scope.workstation_depth) {
+ $scope.args.org = $scope.original_org;
+ } else {
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
+ egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
+ ).then(function(ctx_org) {
+ if (ctx_org) {
+ $scope.args.org = egCore.org.get(ctx_org);
+ }
+ });
+ }
+ }
+ });
}],
- resolve : { staffPenalties : service.get_staff_penalty_types }
+ resolve : {
+ allPenalties : service.get_all_penalty_types,
+ goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
+ }
}).result.then(
function(args) {
- usr_penalty.note(args.note);
- if (args.initials) usr_penalty.note(args.note + ' [' + args.initials + ']');
- usr_penalty.standing_penalty(args.penalty);
- return egCore.pcrud.update(usr_penalty);
+ aum.pub(args.pub);
+ aum.title(args.title);
+ aum.message(args.note);
+ aum.sending_lib(egCore.org.get(egCore.auth.user().ws_ou()).id());
+ pen.org_unit(egCore.org.get(args.org).id());
+ if (args.initials) aum.message((args.note ? args.note : '') + ' [' + args.initials + ']');
+ if (args.custom_penalty) {
+ pen.standing_penalty(args.custom_penalty);
+ } else {
+ pen.standing_penalty(args.penalty);
+ }
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.penalty.modify',
+ egCore.auth.token(), pen, aum
+ );
}
);
}
});
}
+ /*
+ * Returns a union of the full org path of each org unit at which the
+ * currently logged in user has the selected permissions.
+ * @permList - list or string. Unlike hasPermAt, the response object
+ * is always a list of org ids (or an empty list).
+ */
+ service.hasPermFullPathAt = function(permList) {
+ return service.hasPermAt(permList, true)
+ .then(function(orgs) {
+ var orgHash = {};
+ if (permList.constructor != Array) {
+ orgHash[permList] = orgs;
+ } else {
+ orgHash = orgs;
+ }
+ var org_seen = {};
+ angular.forEach(orgHash, function(orgList) {
+ angular.forEach(orgList, function(org) {
+ var full_path = egOrg.fullPath(org,true);
+ angular.forEach(full_path, function(org2) {
+ org_seen[org2] = true;
+ });
+ });
+ });
+ return Object.keys(org_seen).map(function(o) { return Number(o); });
+ });
+ }
+
return service;
}])
var p = service.current;
if (service.alert_penalties.length ||
- p.alert_message() ||
p.active() == 'f' ||
p.barred() == 't' ||
service.patron_stats.holds.ready) {
transclude : true,
scope : {
ngModel : '=',
+ useOpacLabel : '@',
+ maxDepth : '@',
},
require: 'ngModel',
templateUrl : './share/t_share_depth_selector',
var scratch = [];
angular.forEach(list, function(aout) {
var depth = parseInt(aout.depth());
- if (depth in scratch) {
- scratch[depth].push(aout.name());
- } else {
- scratch[depth] = [ aout.name() ]
+ if (typeof $scope.maxDepth == 'undefined' || depth <= $scope.maxDepth) {
+ var text = $scope.useOpacLabel ? aout.opac_label() : aout.name();
+ if (depth in scratch) {
+ scratch[depth].push(text);
+ } else {
+ scratch[depth] = [ text ]
+ }
}
});
scratch.forEach(function(val, idx) {
$scope.values.push({ id : idx, name : scratch[idx].join(' / ') });
});
});
- }]
+ }],
+ link : function(scope, elm, attrs) {
+ if ('useOpacLabel' in attrs)
+ scope.useOpacLabel = true;
+ if ('maxDepth' in attrs) // I feel like I'm doing this wrong :)
+ scope.maxDepth = parseInt(attrs.maxdepth);
+ }
}
})
--- /dev/null
+Consolidate Patron Notes, Alerts, and Messages
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Patron notes, messages, alert messages, and standing penalties have been folded into one Notes interface. Notes designated as public will show in the My Account -> Message Center in the OPAC for patrons.
+
+The underlying data structure has also changed with all notes living in the actor.usr_message table, so report writers will need to change the following paths in existing reports:
+
+ actor.usr_note -> all columns
+ actor.usr -> alert_message
+ actor.usr_standing_penalty -> note
+
+And for actor.usr_message, there is now both a pub column and a deleted column.
+
+Depending on privacy policies, system adminstrators may wish to set up a recurring process to truly delete older entries in actor.usr_message that have been flagged as deleted.
+
+WARNING: The upgrade script will remote the alert_message field from the auditor table, so if you care about preserving those you should run a query to create a backup.
+
+For example:
+
+[source,sql]
+CREATE TABLE auditor.backup_usr_alert_msg AS CREATE audit_id, audit_time, audit_action, audit_user, audit_ws, id as "usr_id", last_update_time, alert_message FROM auditor.actor_usr_history WHERE alert_message IS NOT NULL;