lp1846354 toward consolidated patron notes
authorJason Etheridge <jason@EquinoxInitiative.org>
Wed, 8 Jan 2020 14:02:52 +0000 (09:02 -0500)
committerChris Sharp <csharp@georgialibraries.org>
Mon, 20 Sep 2021 19:45:31 +0000 (15:45 -0400)
* changes to IDL, DB, and upgrade script

A new view actor.usr_message_penalty and associated IDL entry will be created
for use with populating the staff Messages interface.  It will perform a full
outer join between penalties and user messages.

The default IDL permissions for user messages will remain VIEW_USER and
UPDATE_USER.

The view that populates the Message Center will be changed slightly to ensure
that staff-only messages are never retrieved for patrons.  The Date column in
the grid/list view will show the edit_date value if populated in lieu of the
create_date value.  The message view will explicitly show an Edit Date label and
value below the Date row if set.

The actor.usr_message table will gain a new boolean column called pub, which
will default to false.

The actor.usr_message table will also gain a stop_date column for handling
archival as with standing penalties.

The actor.usr_message table will also gain editor and edit_date columns.

The actor.usr_message_limited view will be changed to only include rows where
pub is true.

As part of the upgrade script, Patron alert messages will be migrated as
ALERT_NOTE penalties and linked User Messages.  A database assertion will ensure
that this has happened prior to the removal of the alert_message field from the
actor.usr table.  The field will also be removed from the corresponding IDL
entry.  The sending_lib column for these user messages and the org unit field
for these penalties will be set to the top org from the org hierarchy.

Also, non-public entries in the actor.usr_note table will be migrated as
non-public User Messages. Subject to a database assertion that this has
happened, the table actor.usr_note and the actor.convert_usr_note_to_message
trigger will be removed.  The corresponding IDL entry will also be removed.  The
sending_lib column for these user messages will be set to the top org from the
org hierarchy.

Also, rows from actor.org_unit_setting for
ui.staff.require_initials.patron_info_notes will be migrated to
ui.staff.require_initials.patron_standing_penalty.  Collisions will be recorded
in a text file but otherwise dropped.  The row in config.org_unit_setting_type
for ui.staff.require_initials.patron_info_notes will then be removed.  The label
and description for ui.staff.require_initials.patron_standing_penalty in
config.org_unit_setting_type will be edited to reflect the Notes/Penalties
labeling from the UI.

The actor.usr_standing_penalty table and associated IDL entry will gain a
foreign key column linking to actor.usr_message called usr_message, and the note
column itself will be migrated to User Messages.  The usr_message field on the
penalties will be updated accordingly.  Subject to a database assertion that
this has happened, the note field on the actor.usr_standing_penalty table will
be removed.  The corresponding IDL entry will also be removed.  The sending_lib
column for these user messages will be set to match the org_unit column from
their associated penalties.

* middle layer changes

The ApplyPatronPenalty A/T Reactor will be modified to create a user message if
needed instead of setting the note field on the created penalty, and will set
the new usr_message field accordingly.

The API method open-ils.actor.user.penalty.note.update will be similarly
adjusted, though it may edit or create a user message as needed.

The API method open-ils.actor.user.penalty.apply will also be modified to
create user messages in lieu of setting a note field.  The sending_lib field
for such messages will be set to match the session's workstation library,
regardless of the org unit that actually gets applied to the penalty's org_unit
field.  The org_unit field will use the value as passed by the caller, and no
longer do the org_unit_ancestor_at_depth adjustment that happens today.

Both penalty.note.update and penalty.apply will set the editor and edit date
columns on any linked user messages to the current time and staff member for
any already existing message that has its title or message value modified.  The
read_date column will also be NULL'ed in such circumstances so that the message
will appear unread to the patron if public.

The react sub in Event.pm will be tweaked to explicitly set the pub field on
created user messages to true for Action/Trigger.

* OPAC changes

Surface the edit date for patron visible notes in the Message Center.

* staff client changes

References to Message or Messages in the UI, including the button bar that
spawns the interface, will be changed to Note and Notes.

The Alert Message field in the patron editor will be removed.

The Notes interface under Other -> Notes will be removed.

The Notes (and count) indicator in the patron summary sidebar will be removed.
The note count will instead be presented as part of the Notes nav button.

The patron summary sidebar and the "stop sign page" will be modified to
retrieve and display user messages linked to standing penalties that are
flagged as staff alerting.  It will retain the same styling (i.e. red text) by
default.

The Messages interface, including the archived view, will be populated with a
combination of user messages and user penalties, which may or may not be linked
in pairs via a new foreign key on the penalties.  For messages without
associated standing penalties, the sending_lib column will be used for the
implicit filtering on org units that standing penalties get now, based on the
workstation library.  This combined view will result in new columns for the
interface.  At minimum the following columns (subject to label changes) will be
displayed by default: Title, Message, Create Date, Creator, Library, and Patron
Visible.  Columns such as Staff Alert, Org Depth, Block List, Ignore Proximity,
and Penalty Label will still be available.

The Apply Penalty / Message dialog will gain a new widget for toggling whether
or not a message is intended to be visible to the patron.  The Edit message
version of the dialog will also show when/if a patron has read and/or deleted
the message.  An entry field for the message title will be added.  The dialog
will also gain an org selector to the right of the penalty type selector, which
will default to the workstation library, and will change based on the org depth
of any standing penalty type selected in the UI (either via the
Note/Alert/Block buttons or the penalty menu).  This will be passed as the
org_unit field for a penalty instead of the workstation library and org depth
that is implicitly used today  The sending_lib field for the user message will
always be set to the workstation library.

The Archive Penalty / Message action will be modified to work with both
penalties and user messages depending on what it is selected.  If an archived
penalty has a linked user message, then both will have their archive fields
set.  An archived user message may still be otherwise visible to the patron if
it is not marked as deleted or staff only.

The Remove Penalty / Message action will behave as normal for a selected
penalty.  For a user message, linked or unlinked, the message will be flagged
as deleted and, even if public, will no longer show up in the patron's Message
Center.

* release notes

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 certain
reports may need to be adjusted.

* qa tests

* alert message wording

* set existing user messages to public

before adding new ones; also, remove a lingering reference to alert_message in
Storage/CDBI/actor.pm

* upgrade script tweaks

the main change is that we're creating SILENT_NOTE penalties for migrated
actor.usr_note's in order to preserve the creator.  I'm also removing explicit
TEMP tables in case admins do want to preserve these staging tables (by
commenting out the DROP's at the bottom of the script).

* update auditors

fixes updating patrons and allowing them to login

one consequence of this is that old alert_messages in the auditor will be deleted

* add a WARNING to the release notes

about the auditor table

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Ruth Frasur <rfrasur@gmail.com>
Signed-off-by: Dawn Dale <ddale@georgialibraries.org>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>

33 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Reactor/ApplyPatronPenalty.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/perlmods/live_t/14-lp1499123_csp_ignore_proximity.t
Open-ILS/src/perlmods/live_t/18-lp1592891_sip_standing_penalties.t
Open-ILS/src/perlmods/live_t/30-lp1846354_actor_usr_message_penalty.t [new file with mode: 0644]
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.note_and_message_consolidation [new file with mode: 0644]
Open-ILS/src/templates/actor/user/register_table.tt2
Open-ILS/src/templates/opac/myopac/messages/list.tt2
Open-ILS/src/templates/opac/myopac/messages/single_message.tt2
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/circ/patron/t_alerts.tt2
Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
Open-ILS/src/templates/staff/circ/patron/t_messages.tt2
Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
Open-ILS/src/templates/staff/circ/share/t_new_message_dialog.tt2
Open-ILS/src/templates/staff/css/circ.css.tt2
Open-ILS/src/templates/staff/share/print_templates/t_patron_data.tt2
Open-ILS/web/js/ui/default/actor/user/register.js
Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
Open-ILS/web/js/ui/default/staff/circ/services/circ.js
Open-ILS/web/js/ui/default/staff/services/auth.js
Open-ILS/web/js/ui/default/staff/services/patron_search.js
Open-ILS/web/js/ui/default/staff/services/ui.js
docs/RELEASE_NOTES_NEXT/Client/lp1846354_consolidate_patron_notes.adoc [new file with mode: 0644]

index 03319ba..dc8e55d 100644 (file)
@@ -2312,9 +2312,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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">
@@ -2335,9 +2340,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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">
@@ -2347,38 +2357,56 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        </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" />
@@ -3902,7 +3930,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
@@ -3988,7 +4015,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
@@ -4604,7 +4631,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         <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>
@@ -4662,13 +4689,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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>
index 0faeea2..8681e5d 100644 (file)
@@ -1508,6 +1508,20 @@ sub retrieve_coordinates {
 }
 
 __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"
 );
@@ -1975,7 +1989,13 @@ sub user_opac_vitals {
     $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}
     ]);
 
@@ -3097,7 +3117,9 @@ __PACKAGE__->register_method(
 );
 
 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;
@@ -3107,10 +3129,23 @@ sub apply_penalty {
 
     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);
@@ -3121,6 +3156,44 @@ sub apply_penalty {
 }
 
 __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"
 );
@@ -3147,14 +3220,39 @@ sub update_penalty_note {
     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;
@@ -3224,6 +3322,12 @@ sub new_flesh_user {
         $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];
@@ -3272,12 +3376,27 @@ sub new_flesh_user {
                     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) {
 
index b451cbd..5b9a3c7 100644 (file)
@@ -487,8 +487,8 @@ sub put_into_collections {
     $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');
 }
index a4047f7..4db37c2 100644 (file)
@@ -17,7 +17,7 @@ __PACKAGE__->columns( Essential => qw/usrname email first_given_name
                 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/ );
 
index 4291eeb..1c0286b 100644 (file)
@@ -209,6 +209,7 @@ sub react {
                         $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 )) {
index cbe4a08..808d3f1 100644 (file)
@@ -60,7 +60,22 @@ sub handler {
     $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;
index f50a9ce..3449a7c 100644 (file)
@@ -584,6 +584,7 @@ sub load_myopac_messages {
             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,
         };
@@ -2637,7 +2638,7 @@ sub load_myopac_main {
     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'
         })
index 709fcb0..6201c03 100644 (file)
@@ -61,7 +61,7 @@ sub apply_staff_chr_to_patron {
     $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',
index ca8754e..72e96cf 100644 (file)
@@ -43,7 +43,7 @@ sub apply_penalty_to_patron {
     $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',
diff --git a/Open-ILS/src/perlmods/live_t/30-lp1846354_actor_usr_message_penalty.t b/Open-ILS/src/perlmods/live_t/30-lp1846354_actor_usr_message_penalty.t
new file mode 100644 (file)
index 0000000..68e23da
--- /dev/null
@@ -0,0 +1,133 @@
+#!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();
index b3b2b7a..9e39ccc 100644 (file)
@@ -68,7 +68,6 @@ CREATE TABLE actor.usr (
        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,
@@ -192,18 +191,6 @@ CREATE TRIGGER user_ingest_name_keywords_tgr
     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,
@@ -681,8 +668,7 @@ CREATE TABLE actor.usr_standing_penalty (
        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
@@ -804,9 +790,15 @@ CREATE TABLE actor.usr_message (
        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 (
@@ -815,42 +807,11 @@ CREATE RULE protect_usr_message_delete AS
                  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
@@ -869,6 +830,47 @@ CREATE TRIGGER restrict_usr_message_limited_tgr
     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,
index 3f5bf12..7f39bb3 100644 (file)
@@ -1656,8 +1656,6 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
     '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, 
@@ -2399,7 +2397,6 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
                        'VIEW_BOOKING_RESERVATION',
                        'VIEW_BOOKING_RESERVATION_ATTR_MAP',
                        'VIEW_GROUP_PENALTY_THRESHOLD',
-                       'VIEW_STANDING_PENALTY',
                        'VOID_BILLING',
                        'VOLUME_HOLDS');
 
@@ -2565,7 +2562,6 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
                        'VIEW_BOOKING_RESERVATION',
                        'VIEW_BOOKING_RESERVATION_ATTR_MAP',
                        'VIEW_REPORT_OUTPUT',
-                       'VIEW_STANDING_PENALTY',
                        'VOID_BILLING',
             'TRANSIT_CHECKIN_INTERVAL_BLOCK.override',
                        'VOLUME_HOLDS',
@@ -4848,24 +4844,6 @@ INSERT into config.org_unit_setting_type
         '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',
@@ -5417,22 +5395,12 @@ INSERT into config.org_unit_setting_type
 
 ,( '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.',
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.note_and_message_consolidation b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.note_and_message_consolidation
new file mode 100644 (file)
index 0000000..5da0cac
--- /dev/null
@@ -0,0 +1,486 @@
+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;
+
index 5ad5dfd..27c0c65 100644 (file)
@@ -61,7 +61,6 @@
     <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'>
index 7702c1e..ed9d9d0 100644 (file)
                         <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>
index 2e49ddb..ff83039 100644 (file)
                     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>
index b5b92f4..b6a72bd 100644 (file)
@@ -76,7 +76,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
   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') %]";
@@ -116,7 +116,6 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
                <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 &nbsp;{{patron().notes().length}}</span></a></p>
           <div ng-show="tab != 'search'">
             <a href ng-click="toggle_expand_summary()"
               title="[% l('Collapse Patron Summary Display') %]"
@@ -164,7 +163,11 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
         </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>
@@ -177,12 +180,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
         <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>
index 3c7929e..4e4e692 100644 (file)
     [% 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>&nbsp;</div><!-- should use CSS for this, but spacing out the notes -->
             </div>
             <div class="col-md-2">
               {{penalty.set_date() | date:$root.egDateFormat}}
index 4b4925d..e9a166b 100644 (file)
@@ -704,25 +704,6 @@ within the "form" by name for validation.
   </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">
index 081d325..017cbf4 100644 (file)
@@ -1,37 +1,46 @@
-
-<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>
 
 
index 60145f2..af98a66 100644 (file)
@@ -6,21 +6,13 @@
       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>
@@ -28,7 +20,7 @@
          <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>
index d588f8b..0637a63 100644 (file)
@@ -1,39 +1,93 @@
-<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">&times;</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>&nbsp;{{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>
index 2ac4d9b..5a92101 100644 (file)
@@ -167,6 +167,7 @@ but the ones I'm finding aren't quite cutting it..*/
 
 /* 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 {
index 48d6152..619ab0e 100644 (file)
@@ -32,7 +32,6 @@ Template for printing a patron's data, including addresses and statistical categ
   <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">
index 9b3677a..f7b29ef 100644 (file)
@@ -188,8 +188,6 @@ function load() {
         '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',
index ebe7b39..f5b278c 100644 (file)
@@ -412,7 +412,6 @@ function($scope , $q , egCore , ngToast) {
         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'}},
index 1cfcba6..24b8dc7 100644 (file)
@@ -15,6 +15,10 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
     });
 }])
 
+.factory("hasPermAt",function(){
+    return {};
+})
+
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
@@ -22,7 +26,7 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
     // 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.
@@ -61,7 +65,16 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
             ]);
         }
 
-        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', {
@@ -225,8 +238,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
  *
  * */
 .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$/));
@@ -333,6 +346,15 @@ function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDi
     }
 
     $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 }
@@ -746,11 +768,11 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore ,
  * 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
@@ -772,7 +794,7 @@ function($scope , $q , $routeParams,  egCore , $uibModal , patronSvc , egCirc) {
    
     var activeGrid = $scope.activeGridControls = {
         setSort : function() {
-            return ['set_date'];
+            return ['create_date'];
         },
         setQuery : function() {
             return {
@@ -788,53 +810,120 @@ function($scope , $q , $routeParams,  egCore , $uibModal , patronSvc , egCirc) {
 
     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);
         });
     }
 
@@ -863,15 +952,60 @@ function($scope , $q , $routeParams,  egCore , $uibModal , patronSvc , egCirc) {
     $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);
+                });
+            });
         });
     }
 }])
index 29725cb..ffcb7a4 100644 (file)
@@ -402,8 +402,6 @@ angular.module('egCoreMod')
             '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',
index 975e3ce..32fc43d 100644 (file)
@@ -576,15 +576,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         });
     }
 
-    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,
@@ -2106,34 +2105,96 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         });
     }
 
+    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 {
@@ -2145,41 +2206,133 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                 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
+                );
             }
         );
     }
index 50818e5..6a5643a 100644 (file)
@@ -467,6 +467,34 @@ function($q , egNet , egAuth , egOrg) {
         });
     }
 
+    /*
+     * 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;
 }])
 
index 07e961e..136fcdc 100644 (file)
@@ -331,7 +331,6 @@ function($q , $timeout , $location , egCore,  egUser , egConfirmDialog , $locale
         var p = service.current;
 
         if (service.alert_penalties.length ||
-            p.alert_message() ||
             p.active() == 'f' ||
             p.barred() == 't' ||
             service.patron_stats.holds.ready) {
index f8f7cd1..c903811 100644 (file)
@@ -1484,6 +1484,8 @@ https://stackoverflow.com/questions/24764802/angular-js-automatically-focus-inpu
         transclude : true,
         scope : {
             ngModel : '=',
+            useOpacLabel : '@',
+            maxDepth : '@',
         },
         require: 'ngModel',
         templateUrl : './share/t_share_depth_selector',
@@ -1497,17 +1499,26 @@ https://stackoverflow.com/questions/24764802/angular-js-automatically-focus-inpu
                 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);
+        }
     }
 })
 
diff --git a/docs/RELEASE_NOTES_NEXT/Client/lp1846354_consolidate_patron_notes.adoc b/docs/RELEASE_NOTES_NEXT/Client/lp1846354_consolidate_patron_notes.adoc
new file mode 100644 (file)
index 0000000..0d2f9ca
--- /dev/null
@@ -0,0 +1,20 @@
+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;