</actions>
</permacrud>
</class>
- <class id="ccbi" controller="open-ils.cstore" oils_obj:fieldmapper="container::copy_bucket_item" oils_persist:tablename="container.copy_bucket_item" reporter:label="Copy Bucket Item">
+ <class id="ccbi" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::copy_bucket_item" oils_persist:tablename="container.copy_bucket_item" reporter:label="Copy Bucket Item">
<fields oils_persist:primary="id" oils_persist:sequence="container.copy_bucket_item_id_seq">
<field name="bucket" />
<field name="id" reporter:datatype="id" />
<link field="bucket" reltype="has_a" key="id" map="" class="ccb"/>
<link field="notes" reltype="has_many" map="" key="item" class="ccbin"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_COPY_BUCKET">
+ <context link="bucket" owning_lib="owning_lib"/>
+ </retrieve>
+ </actions>
+ </permacrud>
</class>
<class id="ccbin" controller="open-ils.cstore" oils_obj:fieldmapper="container::copy_bucket_item_note" oils_persist:tablename="container.copy_bucket_item_note" reporter:label="Copy Bucket Item Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.copy_bucket_item_note_id_seq">
</permacrud>
</class>
- <class id="ccnbi" controller="open-ils.cstore" oils_obj:fieldmapper="container::call_number_bucket_item" oils_persist:tablename="container.call_number_bucket_item" reporter:label="Call Number Bucket Item">
+ <class id="ccnbi" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::call_number_bucket_item" oils_persist:tablename="container.call_number_bucket_item" reporter:label="Call Number Bucket Item">
<fields oils_persist:primary="id" oils_persist:sequence="container.call_number_bucket_item_id_seq">
<field name="bucket" reporter:datatype="link"/>
<field name="id" reporter:datatype="id" />
<link field="bucket" reltype="has_a" key="id" map="" class="ccnb"/>
<link field="notes" reltype="has_many" map="" key="item" class="ccnbin"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_CALLNUMBER_BUCKET">
+ <context link="bucket" owning_lib="owning_lib"/>
+ </retrieve>
+ </actions>
+ </permacrud>
</class>
<class id="ccnbin" controller="open-ils.cstore" oils_obj:fieldmapper="container::call_number_bucket_item_note" oils_persist:tablename="container.call_number_bucket_item_note" reporter:label="Call Number Bucket Item Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.call_number_bucket_item_note_id_seq">
<link field="item" reltype="has_a" key="id" map="" class="ccnbi"/>
</links>
</class>
- <class id="cbreb" controller="open-ils.cstore" oils_obj:fieldmapper="container::biblio_record_entry_bucket" oils_persist:tablename="container.biblio_record_entry_bucket" reporter:label="Bibliographic Record Entry Bucket">
+ <class id="cbreb" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::biblio_record_entry_bucket" oils_persist:tablename="container.biblio_record_entry_bucket" reporter:label="Bibliographic Record Entry Bucket">
<fields oils_persist:primary="id" oils_persist:sequence="container.biblio_record_entry_bucket_id_seq">
<field name="items" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="btype" reporter:datatype="text"/>
<field name="owner" reporter:datatype="link"/>
<field name="pub" reporter:datatype="bool"/>
<field name="create_time" reporter:datatype="timestamp" />
+ <field name="owning_lib" reporter:datatype="org_unit" />
</fields>
<links>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="owner" reltype="has_a" key="id" map="" class="au"/>
<link field="items" reltype="has_many" key="bucket" map="" class="cbrebi"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_BIB_BUCKET ADMIN_BIB_BUCKET" context="owning_lib" owning_user="owner"/>
+ <retrieve permission="CREATE_BIB_BUCKET ADMIN_BIB_BUCKET" context="owning_lib" owning_user="owner"/>
+ <update permission="CREATE_BIB_BUCKET ADMIN_BIB_BUCKET" context="owning_lib" owning_user="owner"/>
+ <delete permission="CREATE_BIB_BUCKET ADMIN_BIB_BUCKET" context="owning_lib" owning_user="owner"/>
+ </actions>
+ </permacrud>
</class>
<class id="cbrebn" controller="open-ils.cstore" oils_obj:fieldmapper="container::biblio_record_entry_bucket_note" oils_persist:tablename="container.biblio_record_entry_bucket_note" reporter:label="Bibliographic Record Entry Bucket Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.biblio_record_entry_bucket_note_id_seq">
</actions>
</permacrud>
</class>
- <class id="ccnb" controller="open-ils.cstore" oils_obj:fieldmapper="container::call_number_bucket" oils_persist:tablename="container.call_number_bucket" reporter:label="Call Number Bucket">
+ <class id="ccnb" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::call_number_bucket" oils_persist:tablename="container.call_number_bucket" reporter:label="Call Number Bucket">
<fields oils_persist:primary="id" oils_persist:sequence="container.call_number_bucket_id_seq">
<field name="items" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="btype" reporter:datatype="text"/>
<field name="owner" reporter:datatype="link"/>
<field name="pub" reporter:datatype="bool"/>
<field name="create_time" reporter:datatype="timestamp" />
+ <field name="owning_lib" reporter:datatype="org_unit" />
</fields>
<links>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="owner" reltype="has_a" key="id" map="" class="au"/>
<link field="items" reltype="has_many" key="bucket" map="" class="ccnbi"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_CALLNUMBER_BUCKET ADMIN_CALLNUMBER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <retrieve permission="CREATE_CALLNUMBER_BUCKET ADMIN_CALLNUMBER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <update permission="CREATE_CALLNUMBER_BUCKET ADMIN_CALLNUMBER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <delete permission="CREATE_CALLNUMBER_BUCKET ADMIN_CALLNUMBER_BUCKET" context="owning_lib" owning_user="owner"/>
+ </actions>
+ </permacrud>
</class>
<class id="ccnbn" controller="open-ils.cstore" oils_obj:fieldmapper="container::call_number_bucket_note" oils_persist:tablename="container.call_number_bucket_note" reporter:label="Call Number Bucket Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.call_number_bucket_note_id_seq">
<link field="field" reltype="has_a" key="id" map="" class="cmf"/>
</links>
</class>
- <class id="cub" controller="open-ils.cstore" oils_obj:fieldmapper="container::user_bucket" oils_persist:tablename="container.user_bucket" reporter:label="User Bucket">
+ <class id="cub" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::user_bucket" oils_persist:tablename="container.user_bucket" reporter:label="User Bucket">
<fields oils_persist:primary="id" oils_persist:sequence="container.user_bucket_id_seq">
<field name="items" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="btype" reporter:datatype="text"/>
<field name="owner" reporter:datatype="link"/>
<field name="pub" reporter:datatype="bool"/>
<field name="create_time" reporter:datatype="timestamp" />
+ <field name="owning_lib" reporter:datatype="org_unit" />
</fields>
<links>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="owner" reltype="has_a" key="id" map="" class="au"/>
<link field="items" reltype="has_many" key="bucket" map="" class="cubi"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_USER_BUCKET ADMIN_USER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <retrieve permission="CREATE_USER_BUCKET ADMIN_USER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <update permission="CREATE_USER_BUCKET ADMIN_USER_BUCKET" context="owning_lib" owning_user="owner"/>
+ <delete permission="CREATE_USER_BUCKET ADMIN_USER_BUCKET" context="owning_lib" owning_user="owner"/>
+ </actions>
+ </permacrud>
</class>
<class id="cubn" controller="open-ils.cstore" oils_obj:fieldmapper="container::user_bucket_note" oils_persist:tablename="container.user_bucket_note" reporter:label="User Bucket Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.user_bucket_note_id_seq">
<link field="stat_cat_entry" reltype="has_a" key="id" map="" class="actsce"/>
</links>
</class>
- <class id="cubi" controller="open-ils.cstore" oils_obj:fieldmapper="container::user_bucket_item" oils_persist:tablename="container.user_bucket_item" reporter:label="User Bucket Item">
+ <class id="cubi" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::user_bucket_item" oils_persist:tablename="container.user_bucket_item" reporter:label="User Bucket Item">
<fields oils_persist:primary="id" oils_persist:sequence="container.user_bucket_item_id_seq">
<field name="bucket" reporter:datatype="link"/>
<field name="id" reporter:datatype="id" />
<link field="bucket" reltype="has_a" key="id" map="" class="cub"/>
<link field="notes" reltype="has_many" map="" key="item" class="cubin"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_USER_BUCKET">
+ <context link="bucket" owning_lib="owning_lib"/>
+ </retrieve>
+ </actions>
+ </permacrud>
</class>
<class id="cubin" controller="open-ils.cstore" oils_obj:fieldmapper="container::user_bucket_item_note" oils_persist:tablename="container.user_bucket_item_note" reporter:label="User Bucket Item Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.user_bucket_item_note_id_seq">
</actions>
</permacrud>
</class>
- <class id="ccb" controller="open-ils.cstore" oils_obj:fieldmapper="container::copy_bucket" oils_persist:tablename="container.copy_bucket" reporter:label="Copy Bucket">
+ <class id="ccb" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::copy_bucket" oils_persist:tablename="container.copy_bucket" reporter:label="Copy Bucket">
<fields oils_persist:primary="id" oils_persist:sequence="container.copy_bucket_id_seq">
<field name="items" oils_persist:virtual="true" />
<field name="btype" reporter:datatype="text"/>
<field name="owner" reporter:datatype="link"/>
<field name="pub" reporter:datatype="bool"/>
<field name="create_time" reporter:datatype="timestamp" />
+ <field name="owning_lib" reporter:datatype="org_unit" />
</fields>
<links>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="owner" reltype="has_a" key="id" map="" class="au"/>
<link field="items" reltype="has_many" key="bucket" map="" class="ccbi"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_COPY_BUCKET ADMIN_COPY_BUCKET" context="owning_lib" owning_user="owner"/>
+ <retrieve permission="CREATE_COPY_BUCKET ADMIN_COPY_BUCKET" context="owning_lib" owning_user="owner"/>
+ <update permission="CREATE_COPY_BUCKET ADMIN_COPY_BUCKET" context="owning_lib" owning_user="owner"/>
+ <delete permission="CREATE_COPY_BUCKET ADMIN_COPY_BUCKET" context="owning_lib" owning_user="owner"/>
+ </actions>
+ </permacrud>
</class>
<class id="ccbn" controller="open-ils.cstore" oils_obj:fieldmapper="container::copy_bucket_note" oils_persist:tablename="container.copy_bucket_note" reporter:label="Copy Bucket Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.copy_bucket_note_id_seq">
<link field="cash_drawer" reltype="has_a" key="id" map="" class="aws"/>
</links>
</class>
- <class id="cbrebi" controller="open-ils.cstore" oils_obj:fieldmapper="container::biblio_record_entry_bucket_item" oils_persist:tablename="container.biblio_record_entry_bucket_item" reporter:label="Biblio Record Entry Bucket Item">
+ <class id="cbrebi" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::biblio_record_entry_bucket_item" oils_persist:tablename="container.biblio_record_entry_bucket_item" reporter:label="Biblio Record Entry Bucket Item">
<fields oils_persist:primary="id" oils_persist:sequence="container.biblio_record_entry_bucket_item_id_seq">
<field name="bucket" reporter:datatype="link"/>
<field name="id" reporter:datatype="id" />
<link field="bucket" reltype="has_a" key="id" map="" class="cbreb"/>
<link field="notes" reltype="has_many" map="" key="item" class="cbrebin"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_COPY_BUCKET">
+ <context link="bucket" owning_lib="owning_lib"/>
+ </retrieve>
+ </actions>
+ </permacrud>
</class>
<class id="cbrebin" controller="open-ils.cstore" oils_obj:fieldmapper="container::biblio_record_entry_bucket_item_note" oils_persist:tablename="container.biblio_record_entry_bucket_item_note" reporter:label="Biblio Record Entry Bucket Item Note">
<fields oils_persist:primary="id" oils_persist:sequence="container.biblio_record_entry_bucket_item_note_id_seq">
</fields>
</class>
- <class id="afs" controller="open-ils.cstore" oils_obj:fieldmapper="action::fieldset" oils_persist:tablename="action.fieldset" reporter:label="Fieldset">
+ <class id="afsg" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::fieldset_group" oils_persist:tablename="action.fieldset_group" reporter:label="Fieldset Group">
+ <fields oils_persist:primary="id" oils_persist:sequence="action.fieldset_group_id_seq">
+ <field reporter:label="Fieldset Group ID" name="id" reporter:datatype="id"/>
+ <field reporter:label="Fieldset Group Name" name="name" reporter:datatype="text" oils_obj:required="true"/>
+ <field reporter:label="Creation Time" name="create_time" reporter:datatype="timestamp"/>
+ <field reporter:label="Complete Time" name="complete_time" reporter:datatype="timestamp"/>
+ <field reporter:label="Container ID" name="container" reporter:datatype="int" oils_obj:required="true"/> <!-- not an fkey because could be on multiple tables -->
+ <field reporter:label="Container Type" name="container_type" reporter:datatype="text" oils_obj:required="true"/>
+ <field reporter:label="Rollback Group" name="rollback_group" reporter:datatype="link"/>
+ <field reporter:label="Rollback Time" name="rollback_time" reporter:datatype="timestamp"/>
+ <field reporter:label="Creator" name="creator" reporter:datatype="link"/>
+ <field reporter:label="Owning Library" name="owning_lib" reporter:datatype="org_unit"/>
+ </fields>
+ <links>
+ <link field="creator" reltype="has_a" key="id" map="" class="au"/>
+ <link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="rollback_group" reltype="has_a" key="id" map="" class="afsg"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_FIELDSET_GROUP ADMIN_FIELDSET_GROUP" context_field="owning_lib" owning_user="creator"/>
+ <retrieve permission="RETRIEVE_FIELDSET_GROUP ADMIN_FIELDSET_GROUP" context_field="owning_lib" owning_user="creator"/>
+ <update permission="UPDATE_FIELDSET_GROUP ADMIN_FIELDSET_GROUP" context_field="owning_lib" owning_user="creator"/>
+ <delete permission="DELETE_FIELDSET_GROUP ADMIN_FIELDSET_GROUP" context_field="owning_lib" owning_user="creator"/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="afs" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::fieldset" oils_persist:tablename="action.fieldset" reporter:label="Fieldset">
<fields oils_persist:primary="id" oils_persist:sequence="action.fieldset_id_seq">
<field reporter:label="Fieldset ID" name="id" reporter:datatype="id"/>
<field reporter:label="Owner" name="owner" reporter:datatype="link"/>
<field reporter:label="Fieldset Name" name="name" reporter:datatype="text"/>
<field reporter:label="Stored Query" name="stored_query" reporter:datatype="link"/>
<field reporter:label="Primary Key Value" name="pkey_value" reporter:datatype="text"/>
+ <field reporter:label="Fieldset Group" name="fieldset_group" reporter:datatype="link"/>
+ <field reporter:label="Error Message" name="error_msg" reporter:datatype="text"/>
</fields>
<links>
<link field="owner" reltype="has_a" key="id" map="" class="au"/>
<link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
<link field="stored_query" reltype="has_a" key="id" map="" class="qsq"/>
+ <link field="fieldset_group" reltype="has_a" key="id" map="" class="afsg"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="CREATE_FIELDSET ADMIN_FIELDSET" context_field="owning_lib" owning_user="owner"/>
+ <retrieve permission="RETRIEVE_FIELDSET ADMIN_FIELDSET" context_field="owning_lib" owning_user="owner"/>
+ <update permission="UPDATE_FIELDSET ADMIN_FIELDSET" context_field="owning_lib" owning_user="owner"/>
+ <delete permission="DELETE_FIELDSET ADMIN_FIELDSET" context_field="owning_lib" owning_user="owner"/>
+ </actions>
+ </permacrud>
</class>
- <class id="afscv" controller="open-ils.cstore" oils_obj:fieldmapper="action::fieldset_col_val" oils_persist:tablename="action.fieldset_col_val" reporter:label="Fieldset Column Value">
+ <class id="afscv" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::fieldset_col_val" oils_persist:tablename="action.fieldset_col_val" reporter:label="Fieldset Column Value">
<fields oils_persist:primary="id" oils_persist:sequence="action.fieldset_col_val_id_seq">
<field reporter:label="Column Value ID" name="id" reporter:datatype="id"/>
<field reporter:label="Fieldset ID" name="fieldset" reporter:datatype="link"/>
<links>
<link field="fieldset" reltype="has_a" key="id" map="" class="afs"/>
</links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> <!-- NOTE: foreign context does not support owning_user today -->
+ <actions>
+ <create permission="CREATE_FIELDSET_ENTRY ADMIN_FIELDSET_ENTRY">
+ <context link="fieldset" field="owning_lib"/>
+ </create>
+ <retrieve permission="RETRIEVE_FIELDSET_ENTRY ADMIN_FIELDSET_ENTRY">
+ <context link="fieldset" field="owning_lib"/>
+ </retrieve>
+ <update permission="UPDATE_FIELDSET_ENTRY ADMIN_FIELDSET_ENTRY">
+ <context link="fieldset" field="owning_lib"/>
+ </update>
+ <delete permission="DELETE_FIELDSET_ENTRY ADMIN_FIELDSET_ENTRY">
+ <context link="fieldset" field="owning_lib"/>
+ </delete>
+ </actions>
+ </permacrud>
</class>
<class id="aufhl" controller="open-ils.cstore" oils_obj:fieldmapper="action::unfulfilled_hold_loops" oils_persist:tablename="action.unfulfilled_hold_loops" oils_persist:readonly="true">
my $meth = 'open-ils.cstore.direct.container';
my %types;
my %ctypes;
+my %itypes;
+my %htypes;
+my %qtypes;
+my %ttypes;
+my %batch_perm;
+my %table;
+
+$batch_perm{'biblio'} = ['UPDATE_MARC'];
+$batch_perm{'callnumber'} = ['UPDATE_VOLUME'];
+$batch_perm{'copy'} = ['UPDATE_COPY'];
+$batch_perm{'user'} = ['UPDATE_USER'];
+
$types{'biblio'} = "$meth.biblio_record_entry_bucket";
$types{'callnumber'} = "$meth.call_number_bucket";
$types{'copy'} = "$meth.copy_bucket";
$types{'user'} = "$meth.user_bucket";
+
$ctypes{'biblio'} = "container_biblio_record_entry_bucket";
$ctypes{'callnumber'} = "container_call_number_bucket";
$ctypes{'copy'} = "container_copy_bucket";
$ctypes{'user'} = "container_user_bucket";
+
+$itypes{'biblio'} = "biblio_record_entry";
+$itypes{'callnumber'} = "asset_call_number";
+$itypes{'copy'} = "asset_copy";
+$itypes{'user'} = "actor_user";
+
+$ttypes{'biblio'} = "biblio_record_entry";
+$ttypes{'callnumber'} = "call_number";
+$ttypes{'copy'} = "copy";
+$ttypes{'user'} = "user";
+
+$htypes{'biblio'} = "bre";
+$htypes{'callnumber'} = "acn";
+$htypes{'copy'} = "acp";
+$htypes{'user'} = "au";
+
+$table{'biblio'} = "biblio.record_entry";
+$table{'callnumber'} = "asset.call_number";
+$table{'copy'} = "asset.copy";
+$table{'user'} = "actor.usr";
+
+#$qtypes{'biblio'} = 0
+#$qtypes{'callnumber'} = 0;
+#$qtypes{'copy'} = 0;
+$qtypes{'user'} = 1;
+
my $event;
sub _sort_buckets {
}
}
+sub batch_statcat_apply {
+ my $self = shift;
+ my $client = shift;
+ my $ses = shift;
+ my $c_id = shift;
+ my $changes = shift;
+
+ # $changes is a hashref that looks like:
+ # {
+ # remove => [ qw/ stat cat ids to remove / ],
+ # apply => { $statcat_id => $value_string, ... }
+ # }
+
+ my $class = 'user';
+ my $max = 0;
+ my $count = 0;
+ my $stage = 0;
+
+ my $e = new_editor(xact=>1, authtoken=>$ses);
+ return $e->die_event unless $e->checkauth;
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
+ return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
+
+ my $meth = 'retrieve_' . $ctypes{$class};
+ my $bkt = $e->$meth($c_id) or return $e->die_event;
+
+ unless($bkt->owner eq $e->requestor->id) {
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
+ my $owner = $e->retrieve_actor_user($bkt->owner)
+ or return $e->die_event;
+ return $e->die_event unless (
+ $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
+ );
+ }
+
+ $meth = 'search_' . $ctypes{$class} . '_item';
+ my $contents = $e->$meth({bucket => $c_id});
+
+ if ($self->{perms}) {
+ $max = scalar(@$contents);
+ $client->respond({ ord => $stage, max => $max, count => 0, stage => 'ITEM_PERM_CHECK' });
+ for my $item (@$contents) {
+ $count++;
+ $meth = 'retrieve_' . $itypes{$class};
+ my $field = 'target_'.$ttypes{$class};
+ my $obj = $e->$meth($item->$field);
+
+ for my $perm_field (keys %{$self->{perms}}) {
+ my $perm_def = $self->{perms}->{$perm_field};
+ my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
+ for my $p (@$pwhat) {
+ $e->allowed($p, $obj->$pwhere) or return $e->die_event;
+ }
+ }
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
+ }
+ $stage++;
+ }
+
+ my @users = map { $_->target_user } @$contents;
+ $max = scalar(@users) * scalar(@{$changes->{remove}});
+ $count = 0;
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
+
+ my $chunk = int($max / 10) || 1;
+ my $to_remove = $e->search_actor_stat_cat_entry_user_map({ target_usr => \@users, stat_cat => $changes->{remove} });
+ for my $t (@$to_remove) {
+ $e->delete_actor_stat_cat_entry_user_map($t);
+ $count++;
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
+ unless ($count % $chunk);
+ }
+
+ $stage++;
+
+ $max = scalar(@users) * scalar(keys %{$changes->{apply}});
+ $count = 0;
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
+
+ $chunk = int($max / 10) || 1;
+ for my $item (@$contents) {
+ for my $astatcat (keys %{$changes->{apply}}) {
+ my $new_value = $changes->{apply}->{$astatcat};
+ my $to_change = $e->search_actor_stat_cat_entry_user_map({ target_usr => $item->target_user, stat_cat => $astatcat });
+ if (@$to_change) {
+ $to_change = $$to_change[0];
+ $to_change->stat_cat_entry($new_value);
+ $e->update_actor_stat_cat_entry_user_map($to_change);
+ } else {
+ $to_change = new Fieldmapper::actor::stat_cat_entry_user_map;
+ $to_change->stat_cat_entry($new_value);
+ $to_change->stat_cat($astatcat);
+ $to_change->target_usr($item->target_user);
+ $e->create_actor_stat_cat_entry_user_map($to_change);
+ }
+ $count++;
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
+ unless ($count % $chunk);
+ }
+ }
+
+ $e->commit;
+
+ return { stage => 'COMPLETE' };
+}
+
+__PACKAGE__->register_method(
+ method => "batch_statcat_apply",
+ api_name => "open-ils.actor.container.user.batch_statcat_apply",
+ ctype => 'user',
+ perms => {
+ home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
+ },
+ fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
+ signature => {
+ desc => 'Edits allowed fields on users in a bucket',
+ params => [{
+ desc => 'Session key', type => 'string',
+ desc => 'User container id',
+ desc => 'Hash of statcats to apply or remove', type => 'hash',
+ }],
+ return => {
+ desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
+ type => 'hash'
+ }
+ }
+);
+
+
+sub apply_rollback {
+ my $self = shift;
+ my $client = shift;
+ my $ses = shift;
+ my $c_id = shift;
+ my $main_fsg = shift;
+
+ my $max = 0;
+ my $count = 0;
+ my $stage = 0;
+
+ my $class = $self->{ctype} or return undef;
+
+ my $e = new_editor(xact=>1, authtoken=>$ses);
+ return $e->die_event unless $e->checkauth;
+
+ for my $bp (@{$batch_perm{$class}}) {
+ return { stage => 'COMPLETE' } unless $e->allowed($bp);
+ }
+
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
+ return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
+
+ my $meth = 'retrieve_' . $ctypes{$class};
+ my $bkt = $e->$meth($c_id) or return $e->die_event;
+
+ unless($bkt->owner eq $e->requestor->id) {
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
+ my $owner = $e->retrieve_actor_user($bkt->owner)
+ or return $e->die_event;
+ return $e->die_event unless (
+ $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
+ );
+ }
+
+ $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
+ return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
+
+ my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
+ return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
+
+ my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
+ $max = scalar(@$fieldsets);
+
+ $client->respond({ ord => $stage, max => $max, count => 0, stage => 'APPLY_EDITS' });
+ for my $fs (@$fieldsets) {
+ my $res = $e->json_query({
+ from => ['action.apply_fieldset', $fs->id, $table{$class}, 'id', undef]
+ })->[0]->{'action.apply_fieldset'};
+
+ $client->respond({
+ ord => $stage,
+ max => $max,
+ count => ++$count,
+ stage => 'APPLY_EDITS',
+ error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
+ });
+ }
+
+ $main_fsg->rollback_time('now');
+ $e->update_action_fieldset_group($main_fsg);
+
+ $e->commit;
+
+ return { stage => 'COMPLETE' };
+}
+__PACKAGE__->register_method(
+ method => "apply_rollback",
+ max_bundle_count => 1,
+ api_name => "open-ils.actor.container.user.apply_rollback",
+ ctype => 'user',
+ signature => {
+ desc => 'Applys rollback of a fieldset group to users in a bucket',
+ params => [
+ { desc => 'Session key', type => 'string' },
+ { desc => 'User container id', type => 'number' },
+ { desc => 'Main (non-rollback) fieldset group' },
+ ],
+ return => {
+ desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
+ type => 'hash'
+ }
+ }
+);
+
+
+sub batch_edit {
+ my $self = shift;
+ my $client = shift;
+ my $ses = shift;
+ my $c_id = shift;
+ my $edit_name = shift;
+ my $edits = shift;
+
+ my $max = 0;
+ my $count = 0;
+ my $stage = 0;
+
+ my $class = $self->{ctype} or return undef;
+
+ my $e = new_editor(xact=>1, authtoken=>$ses);
+ return $e->die_event unless $e->checkauth;
+
+ for my $bp (@{$batch_perm{$class}}) {
+ return { stage => 'COMPLETE' } unless $e->allowed($bp);
+ }
+
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
+ return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
+
+ my $meth = 'retrieve_' . $ctypes{$class};
+ my $bkt = $e->$meth($c_id) or return $e->die_event;
+
+ unless($bkt->owner eq $e->requestor->id) {
+ $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
+ my $owner = $e->retrieve_actor_user($bkt->owner)
+ or return $e->die_event;
+ return $e->die_event unless (
+ $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
+ );
+ }
+
+ $meth = 'search_' . $ctypes{$class} . '_item';
+ my $contents = $e->$meth({bucket => $c_id});
+
+ $max = 0;
+ $max = scalar(@$contents) if ($self->{perms});
+ $max += scalar(@$contents) if ($self->{base_perm});
+
+ my $obj_cache = {};
+ if ($self->{base_perm}) {
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
+ for my $item (@$contents) {
+ $count++;
+ $meth = 'retrieve_' . $itypes{$class};
+ my $field = 'target_'.$ttypes{$class};
+ my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
+
+ for my $perm_field (keys %{$self->{base_perm}}) {
+ my $perm_def = $self->{base_perm}->{$perm_field};
+ my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
+ for my $p (@$pwhat) {
+ $e->allowed($p, $obj->$pwhere) or return $e->die_event;
+ if ($$edits{$pwhere}) {
+ $e->allowed($p, $$edits{$pwhere}) or do {
+ $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
+ return $e->die_event;
+ };
+ }
+ }
+ }
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
+ }
+ }
+
+ if ($self->{perms}) {
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
+ for my $item (@$contents) {
+ $count++;
+ $meth = 'retrieve_' . $itypes{$class};
+ my $field = 'target_'.$ttypes{$class};
+ my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
+
+ for my $perm_field (keys %{$self->{perms}}) {
+ my $perm_def = $self->{perms}->{$perm_field};
+ if (ref($perm_def) eq 'HASH') { # we care about specific values being set
+ for my $perm_value (keys %$perm_def) {
+ if (exists $$edits{$perm_field} && $$edits{$perm_field} eq $perm_value) { # check permission
+ while (my ($pwhat,$pwhere) = each %{$$perm_def{$perm_value}}) {
+ if ($pwhere eq '*') {
+ $pwhere = undef;
+ } else {
+ $pwhere = $obj->$pwhere;
+ }
+ $pwhat = [ split / /, $pwhat ];
+ for my $p (@$pwhat) {
+ $e->allowed($p, $pwhere) or do {
+ $pwhere ||= "everywhere";
+ $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
+ return $e->die_event;
+ };
+ }
+ }
+ }
+ }
+ } elsif (ref($perm_def) eq 'CODE') { # we need to run the code on old and new, and pass both tests
+ if (exists $$edits{$perm_field}) {
+ $perm_def->($e, $obj->$perm_field) or return $e->die_event;
+ $perm_def->($e, $$edits{$perm_field}) or return $e->die_event;
+ }
+ } else { # we're checking an ou field
+ my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
+ if ($$edits{$pwhere}) {
+ for my $p (@$pwhat) {
+ $e->allowed($p, $obj->$pwhere) or return $e->die_event;
+ $e->allowed($p, $$edits{$pwhere}) or do {
+ $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
+ return $e->die_event;
+ };
+ }
+ }
+ }
+ }
+ $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
+ }
+ $stage++;
+ }
+
+ $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
+ my $fsgroup = Fieldmapper::action::fieldset_group->new;
+ $fsgroup->isnew(1);
+ $fsgroup->name($edit_name);
+ $fsgroup->creator($e->requestor->id);
+ $fsgroup->owning_lib($e->requestor->ws_ou);
+ $fsgroup->container($c_id);
+ $fsgroup->container_type($ttypes{$class});
+ $fsgroup = $e->create_action_fieldset_group($fsgroup);
+
+ $client->respond({ ord => $stage++, stage => 'FIELDSET_CREATE' });
+ my $fieldset = Fieldmapper::action::fieldset->new;
+ $fieldset->isnew(1);
+ $fieldset->fieldset_group($fsgroup->id);
+ $fieldset->owner($e->requestor->id);
+ $fieldset->owning_lib($e->requestor->ws_ou);
+ $fieldset->status('PENDING');
+ $fieldset->classname($htypes{$class});
+ $fieldset->name($edit_name . ' batch group fieldset');
+ $fieldset->stored_query($qtypes{$class});
+ $fieldset = $e->create_action_fieldset($fieldset);
+
+ my @keys = keys %$edits;
+ $max = scalar(@keys);
+ $count = 0;
+ $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
+ for my $key (@keys) {
+ if ($self->{fields}) { # restrict edits to registered fields
+ next unless (grep { $_ eq $key } @{$self->{fields}});
+ }
+ my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
+ $fs_cv->isnew(1);
+ $fs_cv->fieldset($fieldset->id);
+ $fs_cv->col($key);
+ $fs_cv->val($$edits{$key});
+ $e->create_action_fieldset_col_val($fs_cv);
+ $count++;
+ $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
+ }
+
+ $client->respond({ ord => ++$stage, stage => 'CONSTRUCT_QUERY' });
+ my $qstore = OpenSRF::AppSession->connect('open-ils.qstore');
+ my $prep = $qstore->request('open-ils.qstore.prepare', $fieldset->stored_query)->gather(1);
+ my $token = $prep->{token};
+ $qstore->request('open-ils.qstore.bind_param', $token, {bucket => $c_id})->gather(1);
+ my $sql = $qstore->request('open-ils.qstore.sql', $token)->gather(1);
+ $sql =~ s/\n\s*/ /g; # normalize the string
+ $sql =~ s/;\s*//g; # kill trailing semicolon
+
+ $client->respond({ ord => ++$stage, stage => 'APPLY_EDITS' });
+ my $res = $e->json_query({
+ from => ['action.apply_fieldset', $fieldset->id, $table{$class}, 'id', $sql]
+ })->[0]->{'action.apply_fieldset'};
+
+ $e->commit;
+ $qstore->disconnect;
+
+ return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
+}
+
+__PACKAGE__->register_method(
+ method => "batch_edit",
+ max_bundle_count => 1,
+ api_name => "open-ils.actor.container.user.batch_edit",
+ ctype => 'user',
+ base_perm => { home_ou => 'UPDATE_USER' },
+ perms => {
+ profile => sub {
+ my ($e, $group) = @_;
+ my $g = $e->retrieve_permission_grp_tree($group);
+ if (my $p = $g->application_perm()) {
+ return $e->allowed($p);
+ }
+ return 1;
+ }, # code ref is run with params (editor,value), for both old and new value
+ # home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
+ barred => {
+ t => { BAR_PATRON => 'home_ou' },
+ f => { UNBAR_PATRON => 'home_ou' }
+ } # field -> struct means "if field getting value "key" check -> perm -> at context org, both old and new
+ },
+ fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
+ signature => {
+ desc => 'Edits allowed fields on users in a bucket',
+ params => [
+ { desc => 'Session key', type => 'string' },
+ { desc => 'User container id', type => 'number' },
+ { desc => 'Batch edit name', type => 'string' },
+ { desc => 'Edit hash, key is column, value is new value to apply', type => 'hash' },
+ ],
+ return => {
+ desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
+ type => 'hash'
+ }
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "batch_edit",
+ api_name => "open-ils.actor.container.user.batch_delete",
+ ctype => 'user',
+ perms => {
+ deleted => {
+ t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
+ f => { 'UPDATE_USER' => 'home_ou' }
+ }
+ },
+ fields => [ qw/deleted/ ],
+ signature => {
+ desc => 'Deletes users in a bucket',
+ params => [{
+ { desc => 'Session key', type => 'string' },
+ { desc => 'User container id', type => 'number' },
+ { desc => 'Batch delete name', type => 'string' },
+ { desc => 'Edit delete, key is "deleted", value is new value to apply ("t")', type => 'hash' },
+
+ }],
+ return => {
+ desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
+ type => 'hash'
+ }
+ }
+);
+
1;
DO INSTEAD
DELETE FROM query.expression WHERE id = OLD.id;
+INSERT INTO query.bind_variable (name,type,description,label)
+ SELECT 'bucket','number','ID of the bucket to pull items from','Bucket ID';
+
+-- Assumes completely empty 'query' schema
+INSERT INTO query.stored_query (type, use_distinct) VALUES ('SELECT', TRUE); -- 1
+
+INSERT INTO query.from_relation (type, table_name, class_name, table_alias) VALUES ('RELATION', 'container.user_bucket_item', 'cubi', 'cubi'); -- 1
+UPDATE query.stored_query SET from_clause = 1;
+
+INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'target_user'); -- 1
+INSERT INTO query.select_item (stored_query,seq_no,expression) VALUES (1,1,1);
+
+INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'bucket'); -- 2
+INSERT INTO query.expr_xbind (bind_variable) VALUES ('bucket'); -- 3
+
+INSERT INTO query.expr_xop (left_operand, operator, right_operand) VALUES (2, '=', 3); -- 4
+UPDATE query.stored_query SET where_clause = 4;
+
+SELECT SETVAL('query.stored_query_id_seq', 1000, TRUE) FROM query.stored_query;
+SELECT SETVAL('query.from_relation_id_seq', 1000, TRUE) FROM query.from_relation;
+SELECT SETVAL('query.expression_id_seq', 10000, TRUE) FROM query.expression;
+
+
COMMIT;
btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.copy_bucket_type (code) DEFERRABLE INITIALLY DEFERRED,
description TEXT,
pub BOOL NOT NULL DEFAULT FALSE,
+ owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT cb_name_once_per_owner UNIQUE (owner,name,btype)
);
btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.call_number_bucket_type (code) DEFERRABLE INITIALLY DEFERRED,
description TEXT,
pub BOOL NOT NULL DEFAULT FALSE,
+ owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT cnb_name_once_per_owner UNIQUE (owner,name,btype)
);
btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.biblio_record_entry_bucket_type (code) DEFERRABLE INITIALLY DEFERRED,
description TEXT,
pub BOOL NOT NULL DEFAULT FALSE,
+ owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT breb_name_once_per_owner UNIQUE (owner,name,btype)
);
btype TEXT NOT NULL DEFAULT 'misc' REFERENCES container.user_bucket_type (code) DEFERRABLE INITIALLY DEFERRED,
description TEXT,
pub BOOL NOT NULL DEFAULT FALSE,
+ owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT ub_name_once_per_owner UNIQUE (owner,name,btype)
);
FOR EACH ROW
EXECUTE PROCEDURE action.age_hold_on_delete ();
+CREATE TABLE action.fieldset_group (
+ id SERIAL PRIMARY KEY,
+ name NEXT NOT NULL,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ complete_time TIMESTAMPTZ,
+ container INT, -- Points to a container of some type ...
+ container_type TEXT, -- One of 'biblio_record_entry', 'user', 'call_number', 'copy'
+ rollback_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ rollback_time TIMESTAMPTZ,
+ creator INT NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ owning_lib INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
CREATE TABLE action.fieldset (
id SERIAL PRIMARY KEY,
+ fieldset_group INT REFERENCES action.fieldset_group (id)
+ DEFERRABLE INITIALLY DEFERRED,
owner INT NOT NULL REFERENCES actor.usr (id)
DEFERRABLE INITIALLY DEFERRED,
owning_lib INT NOT NULL REFERENCES actor.org_unit (id)
applied_time TIMESTAMPTZ,
classname TEXT NOT NULL, -- an IDL class name
name TEXT NOT NULL,
+ error_msg TEXT,
stored_query INT REFERENCES query.stored_query (id)
DEFERRABLE INITIALLY DEFERRED,
pkey_value TEXT,
$func$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION action.apply_fieldset(
- fieldset_id IN INT, -- id from action.fieldset
- table_name IN TEXT, -- table to be updated
- pkey_name IN TEXT, -- name of primary key column in that table
- query IN TEXT -- query constructed by qstore (for query-based
- -- fieldsets only; otherwise null
+ fieldset_id IN INT, -- id from action.fieldset
+ table_name IN TEXT, -- table to be updated
+ pkey_name IN TEXT, -- name of primary key column in that table
+ query IN TEXT -- query constructed by qstore (for query-based
+ -- fieldsets only; otherwise null
)
RETURNS TEXT AS $$
DECLARE
- statement TEXT;
- fs_status TEXT;
- fs_pkey_value TEXT;
- fs_query TEXT;
- sep CHAR;
- status_code TEXT;
- msg TEXT;
- update_count INT;
- cv RECORD;
+ statement TEXT;
+ where_clause TEXT;
+ fs_status TEXT;
+ fs_pkey_value TEXT;
+ fs_query TEXT;
+ sep CHAR;
+ status_code TEXT;
+ msg TEXT;
+ fs_id INT;
+ fsg_id INT;
+ update_count INT;
+ cv RECORD;
+ fs_obj action.fieldset%ROWTYPE;
+ fs_group action.fieldset_group%ROWTYPE;
+ rb_row RECORD;
BEGIN
- -- Sanity checks
- IF fieldset_id IS NULL THEN
- RETURN 'Fieldset ID parameter is NULL';
- END IF;
- IF table_name IS NULL THEN
- RETURN 'Table name parameter is NULL';
- END IF;
- IF pkey_name IS NULL THEN
- RETURN 'Primary key name parameter is NULL';
- END IF;
- --
- statement := 'UPDATE ' || table_name || ' SET';
- --
- SELECT
- status,
- quote_literal( pkey_value )
- INTO
- fs_status,
- fs_pkey_value
- FROM
- action.fieldset
- WHERE
- id = fieldset_id;
- --
- IF fs_status IS NULL THEN
- RETURN 'No fieldset found for id = ' || fieldset_id;
- ELSIF fs_status = 'APPLIED' THEN
- RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
- END IF;
- --
- sep := '';
- FOR cv IN
- SELECT col,
- val
- FROM action.fieldset_col_val
- WHERE fieldset = fieldset_id
- LOOP
- statement := statement || sep || ' ' || cv.col
- || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
- sep := ',';
- END LOOP;
- --
- IF sep = '' THEN
- RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
- END IF;
- --
- -- Add the WHERE clause. This differs according to whether it's a
- -- single-row fieldset or a query-based fieldset.
- --
- IF query IS NULL AND fs_pkey_value IS NULL THEN
- RETURN 'Incomplete fieldset: neither a primary key nor a query available';
- ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
- fs_query := rtrim( query, ';' );
- statement := statement || ' WHERE ' || pkey_name || ' IN ( '
- || fs_query || ' );';
- ELSIF query IS NULL AND fs_pkey_value IS NOT NULL THEN
- statement := statement || ' WHERE ' || pkey_name || ' = '
- || fs_pkey_value || ';';
- ELSE -- both are not null
- RETURN 'Ambiguous fieldset: both a primary key and a query provided';
- END IF;
- --
- -- Execute the update
- --
- BEGIN
- EXECUTE statement;
- GET DIAGNOSTICS update_count = ROW_COUNT;
- --
- IF UPDATE_COUNT > 0 THEN
- status_code := 'APPLIED';
- msg := NULL;
- ELSE
- status_code := 'ERROR';
- msg := 'No eligible rows found for fieldset ' || fieldset_id;
- END IF;
- EXCEPTION WHEN OTHERS THEN
- status_code := 'ERROR';
- msg := 'Unable to apply fieldset ' || fieldset_id
- || ': ' || sqlerrm;
- END;
- --
- -- Update fieldset status
- --
- UPDATE action.fieldset
- SET status = status_code,
- applied_time = now()
- WHERE id = fieldset_id;
- --
- RETURN msg;
+ -- Sanity checks
+ IF fieldset_id IS NULL THEN
+ RETURN 'Fieldset ID parameter is NULL';
+ END IF;
+ IF table_name IS NULL THEN
+ RETURN 'Table name parameter is NULL';
+ END IF;
+ IF pkey_name IS NULL THEN
+ RETURN 'Primary key name parameter is NULL';
+ END IF;
+
+ SELECT
+ status,
+ quote_literal( pkey_value )
+ INTO
+ fs_status,
+ fs_pkey_value
+ FROM
+ action.fieldset
+ WHERE
+ id = fieldset_id;
+
+ --
+ -- Build the WHERE clause. This differs according to whether it's a
+ -- single-row fieldset or a query-based fieldset.
+ --
+ IF query IS NULL AND fs_pkey_value IS NULL THEN
+ RETURN 'Incomplete fieldset: neither a primary key nor a query available';
+ ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
+ fs_query := rtrim( query, ';' );
+ where_clause := 'WHERE ' || pkey_name || ' IN ( '
+ || fs_query || ' )';
+ ELSIF query IS NULL AND fs_pkey_value IS NOT NULL THEN
+ where_clause := 'WHERE ' || pkey_name || ' = ';
+ IF pkey_name = 'id' THEN
+ where_clause := where_clause || fs_pkey_value;
+ ELSIF pkey_name = 'code' THEN
+ where_clause := where_clause || quote_literal(fs_pkey_value);
+ ELSE
+ RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
+ END IF;
+ ELSE -- both are not null
+ RETURN 'Ambiguous fieldset: both a primary key and a query provided';
+ END IF;
+
+ IF fs_status IS NULL THEN
+ RETURN 'No fieldset found for id = ' || fieldset_id;
+ ELSIF fs_status = 'APPLIED' THEN
+ RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
+ END IF;
+
+ SELECT * INTO fs_obj FROM action.fieldset WHERE id = fieldset_id;
+ SELECT * INTO fs_group FROM action.fieldset_group WHERE id = fs_obj.fieldset_group;
+
+ IF fs_group.can_rollback THEN
+ -- This is part of a non-rollback group. We need to record the current values for future rollback.
+
+ INSERT INTO action.fieldset_group (can_rollback, name, creator, owning_lib, container, container_type)
+ VALUES (FALSE, 'ROLLBACK: '|| fs_group.name, fs_group.creator, fs_group.owning_lib, fs_group.container, fs_group.container_type);
+
+ fsg_id := CURRVAL('action.fieldset_group_id_seq');
+
+ FOR rb_row IN EXECUTE 'SELECT * FROM ' || table_name || ' ' || where_clause LOOP
+ IF pkey_name = 'id' THEN
+ fs_pkey_value := rb_row.id;
+ ELSIF pkey_name = 'code' THEN
+ fs_pkey_value := rb_row.code;
+ ELSE
+ RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
+ END IF;
+ INSERT INTO action.fieldset (fieldset_group,owner,owning_lib,status,classname,name,pkey_value)
+ VALUES (fsg_id, fs_obj.owner, fs_obj.owning_lib, 'PENDING', fs_obj.classname, fs_obj.name || ' ROLLBACK FOR ' || fs_pkey_value, fs_pkey_value);
+
+ fs_id := CURRVAL('action.fieldset_id_seq');
+ sep := '';
+ FOR cv IN
+ SELECT DISTINCT col
+ FROM action.fieldset_col_val
+ WHERE fieldset = fieldset_id
+ LOOP
+ EXECUTE 'INSERT INTO action.fieldset_col_val (fieldset, col, val) ' ||
+ 'SELECT '|| fs_id || ', '||quote_literal(cv.col)||', '||cv.col||' FROM '||table_name||' WHERE '||pkey_name||' = '||fs_pkey_value;
+ END LOOP;
+ END LOOP;
+ END IF;
+
+ statement := 'UPDATE ' || table_name || ' SET';
+
+ sep := '';
+ FOR cv IN
+ SELECT col,
+ val
+ FROM action.fieldset_col_val
+ WHERE fieldset = fieldset_id
+ LOOP
+ statement := statement || sep || ' ' || cv.col
+ || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
+ sep := ',';
+ END LOOP;
+
+ IF sep = '' THEN
+ RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
+ END IF;
+ statement := statement || ' ' || where_clause;
+
+ --
+ -- Execute the update
+ --
+ BEGIN
+ EXECUTE statement;
+ GET DIAGNOSTICS update_count = ROW_COUNT;
+
+ IF update_count = 0 THEN
+ RAISE data_exception;
+ END IF;
+
+ IF fsg_id IS NOT NULL THEN
+ UPDATE action.fieldset_group SET rollback_group = fsg_id WHERE id = fs_group.id;
+ END IF;
+
+ IF fs_group.id IS NOT NULL THEN
+ UPDATE action.fieldset_group SET complete_time = now() WHERE id = fs_group.id;
+ END IF;
+
+ UPDATE action.fieldset SET status = 'APPLIED', applied_time = now() WHERE id = fieldset_id;
+
+ EXCEPTION WHEN data_exception THEN
+ msg := 'No eligible rows found for fieldset ' || fieldset_id;
+ UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
+ RETURN msg;
+
+ END;
+
+ RETURN msg;
+
+EXCEPTION WHEN OTHERS THEN
+ msg := 'Unable to apply fieldset ' || fieldset_id || ': ' || sqlerrm;
+ UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
+ RETURN msg;
+
END;
$$ LANGUAGE plpgsql;
WHERE aacirc.target_copy = ac_aacirc.id;
COMMIT;
+
( 590, 'ADMIN_COPY_TAG_TYPES', oils_i18n_gettext( 590,
'Administer copy tag types', 'ppl', 'description' )),
( 591, 'ADMIN_COPY_TAG', oils_i18n_gettext( 591,
- 'Administer copy tag', 'ppl', 'description' ))
+ 'Administer copy tag', 'ppl', 'description' )),
+ ( 592,'CONTAINER_BATCH_UPDATE', oils_i18n_gettext( 592,
+ 'Allow batch update via buckets', 'ppl', 'description' ))
;
SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
INSERT INTO container.user_bucket_type (code,label) VALUES ('folks:hold.view', oils_i18n_gettext('folks:hold.view', 'View Holds', 'cubt', 'label'));
INSERT INTO container.user_bucket_type (code,label) VALUES ('folks:hold.cancel', oils_i18n_gettext('folks:hold.cancel', 'Cancel Holds', 'cubt', 'label'));
+INSERT INTO container.user_bucket_type (code,label) SELECT code,label FROM container.copy_bucket_type where code = 'staff_client';
----------------------------------
-- MARC21 record structure data --
--- /dev/null
+BEGIN;
+
+INSERT INTO permission.perm_list (id,code,description) VALUES (592,'CONTAINER_BATCH_UPDATE','Allow batch update via buckets');
+
+INSERT INTO container.user_bucket_type (code,label) SELECT code,label FROM container.copy_bucket_type where code = 'staff_client';
+
+CREATE TABLE action.fieldset_group (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ complete_time TIMESTAMPTZ,
+ container INT, -- Points to a container of some type ...
+ container_type TEXT, -- One of 'biblio_record_entry', 'user', 'call_number', 'copy'
+ can_rollback BOOL DEFAULT TRUE,
+ rollback_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ rollback_time TIMESTAMPTZ,
+ creator INT NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ owning_lib INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+);
+
+ALTER TABLE action.fieldset ADD COLUMN fieldset_group INT REFERENCES action.fieldset_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE action.fieldset ADD COLUMN error_msg TEXT;
+ALTER TABLE container.biblio_record_entry_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE container.user_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE container.call_number_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE container.copy_bucket ADD COLUMN owning_lib INT REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
+UPDATE query.stored_query SET id = id + 1000 WHERE id < 1000;
+UPDATE query.from_relation SET id = id + 1000 WHERE id < 1000;
+UPDATE query.expression SET id = id + 1000 WHERE id < 1000;
+
+SELECT SETVAL('query.stored_query_id_seq', 1, FALSE);
+SELECT SETVAL('query.from_relation_id_seq', 1, FALSE);
+SELECT SETVAL('query.expression_id_seq', 1, FALSE);
+
+INSERT INTO query.bind_variable (name,type,description,label)
+ SELECT 'bucket','number','ID of the bucket to pull items from','Bucket ID'
+ WHERE NOT EXISTS (SELECT 1 FROM query.bind_variable WHERE name = 'bucket');
+
+-- Assumes completely empty 'query' schema
+INSERT INTO query.stored_query (type, use_distinct) VALUES ('SELECT', TRUE); -- 1
+
+INSERT INTO query.from_relation (type, table_name, class_name, table_alias) VALUES ('RELATION', 'container.user_bucket_item', 'cubi', 'cubi'); -- 1
+UPDATE query.stored_query SET from_clause = 1;
+
+INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'target_user'); -- 1
+INSERT INTO query.select_item (stored_query,seq_no,expression) VALUES (1,1,1);
+
+INSERT INTO query.expr_xcol (table_alias, column_name) VALUES ('cubi', 'bucket'); -- 2
+INSERT INTO query.expr_xbind (bind_variable) VALUES ('bucket'); -- 3
+
+INSERT INTO query.expr_xop (left_operand, operator, right_operand) VALUES (2, '=', 3); -- 4
+UPDATE query.stored_query SET where_clause = 4;
+
+SELECT SETVAL('query.stored_query_id_seq', 1000, TRUE) FROM query.stored_query;
+SELECT SETVAL('query.from_relation_id_seq', 1000, TRUE) FROM query.from_relation;
+SELECT SETVAL('query.expression_id_seq', 10000, TRUE) FROM query.expression;
+
+CREATE OR REPLACE FUNCTION action.apply_fieldset(
+ fieldset_id IN INT, -- id from action.fieldset
+ table_name IN TEXT, -- table to be updated
+ pkey_name IN TEXT, -- name of primary key column in that table
+ query IN TEXT -- query constructed by qstore (for query-based
+ -- fieldsets only; otherwise null
+)
+RETURNS TEXT AS $$
+DECLARE
+ statement TEXT;
+ where_clause TEXT;
+ fs_status TEXT;
+ fs_pkey_value TEXT;
+ fs_query TEXT;
+ sep CHAR;
+ status_code TEXT;
+ msg TEXT;
+ fs_id INT;
+ fsg_id INT;
+ update_count INT;
+ cv RECORD;
+ fs_obj action.fieldset%ROWTYPE;
+ fs_group action.fieldset_group%ROWTYPE;
+ rb_row RECORD;
+BEGIN
+ -- Sanity checks
+ IF fieldset_id IS NULL THEN
+ RETURN 'Fieldset ID parameter is NULL';
+ END IF;
+ IF table_name IS NULL THEN
+ RETURN 'Table name parameter is NULL';
+ END IF;
+ IF pkey_name IS NULL THEN
+ RETURN 'Primary key name parameter is NULL';
+ END IF;
+
+ SELECT
+ status,
+ quote_literal( pkey_value )
+ INTO
+ fs_status,
+ fs_pkey_value
+ FROM
+ action.fieldset
+ WHERE
+ id = fieldset_id;
+
+ --
+ -- Build the WHERE clause. This differs according to whether it's a
+ -- single-row fieldset or a query-based fieldset.
+ --
+ IF query IS NULL AND fs_pkey_value IS NULL THEN
+ RETURN 'Incomplete fieldset: neither a primary key nor a query available';
+ ELSIF query IS NOT NULL AND fs_pkey_value IS NULL THEN
+ fs_query := rtrim( query, ';' );
+ where_clause := 'WHERE ' || pkey_name || ' IN ( '
+ || fs_query || ' )';
+ ELSIF query IS NULL AND fs_pkey_value IS NOT NULL THEN
+ where_clause := 'WHERE ' || pkey_name || ' = ';
+ IF pkey_name = 'id' THEN
+ where_clause := where_clause || fs_pkey_value;
+ ELSIF pkey_name = 'code' THEN
+ where_clause := where_clause || quote_literal(fs_pkey_value);
+ ELSE
+ RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
+ END IF;
+ ELSE -- both are not null
+ RETURN 'Ambiguous fieldset: both a primary key and a query provided';
+ END IF;
+
+ IF fs_status IS NULL THEN
+ RETURN 'No fieldset found for id = ' || fieldset_id;
+ ELSIF fs_status = 'APPLIED' THEN
+ RETURN 'Fieldset ' || fieldset_id || ' has already been applied';
+ END IF;
+
+ SELECT * INTO fs_obj FROM action.fieldset WHERE id = fieldset_id;
+ SELECT * INTO fs_group FROM action.fieldset_group WHERE id = fs_obj.fieldset_group;
+
+ IF fs_group.can_rollback THEN
+ -- This is part of a non-rollback group. We need to record the current values for future rollback.
+
+ INSERT INTO action.fieldset_group (can_rollback, name, creator, owning_lib, container, container_type)
+ VALUES (FALSE, 'ROLLBACK: '|| fs_group.name, fs_group.creator, fs_group.owning_lib, fs_group.container, fs_group.container_type);
+
+ fsg_id := CURRVAL('action.fieldset_group_id_seq');
+
+ FOR rb_row IN EXECUTE 'SELECT * FROM ' || table_name || ' ' || where_clause LOOP
+ IF pkey_name = 'id' THEN
+ fs_pkey_value := rb_row.id;
+ ELSIF pkey_name = 'code' THEN
+ fs_pkey_value := rb_row.code;
+ ELSE
+ RETURN 'Only know how to handle "id" and "code" pkeys currently, received ' || pkey_name;
+ END IF;
+ INSERT INTO action.fieldset (fieldset_group,owner,owning_lib,status,classname,name,pkey_value)
+ VALUES (fsg_id, fs_obj.owner, fs_obj.owning_lib, 'PENDING', fs_obj.classname, fs_obj.name || ' ROLLBACK FOR ' || fs_pkey_value, fs_pkey_value);
+
+ fs_id := CURRVAL('action.fieldset_id_seq');
+ sep := '';
+ FOR cv IN
+ SELECT DISTINCT col
+ FROM action.fieldset_col_val
+ WHERE fieldset = fieldset_id
+ LOOP
+ EXECUTE 'INSERT INTO action.fieldset_col_val (fieldset, col, val) ' ||
+ 'SELECT '|| fs_id || ', '||quote_literal(cv.col)||', '||cv.col||' FROM '||table_name||' WHERE '||pkey_name||' = '||fs_pkey_value;
+ END LOOP;
+ END LOOP;
+ END IF;
+
+ statement := 'UPDATE ' || table_name || ' SET';
+
+ sep := '';
+ FOR cv IN
+ SELECT col,
+ val
+ FROM action.fieldset_col_val
+ WHERE fieldset = fieldset_id
+ LOOP
+ statement := statement || sep || ' ' || cv.col
+ || ' = ' || coalesce( quote_literal( cv.val ), 'NULL' );
+ sep := ',';
+ END LOOP;
+
+ IF sep = '' THEN
+ RETURN 'Fieldset ' || fieldset_id || ' has no column values defined';
+ END IF;
+ statement := statement || ' ' || where_clause;
+
+ --
+ -- Execute the update
+ --
+ BEGIN
+ EXECUTE statement;
+ GET DIAGNOSTICS update_count = ROW_COUNT;
+
+ IF update_count = 0 THEN
+ RAISE data_exception;
+ END IF;
+
+ IF fsg_id IS NOT NULL THEN
+ UPDATE action.fieldset_group SET rollback_group = fsg_id WHERE id = fs_group.id;
+ END IF;
+
+ IF fs_group.id IS NOT NULL THEN
+ UPDATE action.fieldset_group SET complete_time = now() WHERE id = fs_group.id;
+ END IF;
+
+ UPDATE action.fieldset SET status = 'APPLIED', applied_time = now() WHERE id = fieldset_id;
+
+ EXCEPTION WHEN data_exception THEN
+ msg := 'No eligible rows found for fieldset ' || fieldset_id;
+ UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
+ RETURN msg;
+
+ END;
+
+ RETURN msg;
+
+EXCEPTION WHEN OTHERS THEN
+ msg := 'Unable to apply fieldset ' || fieldset_id || ': ' || sqlerrm;
+ UPDATE action.fieldset SET status = 'ERROR', applied_time = now() WHERE id = fieldset_id;
+ RETURN msg;
+
+END;
+$$ LANGUAGE plpgsql;
+
+COMMIT;
+
--- /dev/null
+[%
+ WRAPPER "staff/base.tt2";
+ ctx.page_title = l("User Buckets");
+ ctx.page_app = "egCatUserBuckets";
+ ctx.page_ctrl = "UserBucketCtrl";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user-bucket.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/bucket/app.js"></script>
+
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+
+ s.CONTAINER_BATCH_UPDATE_PERM_CHECK = "[% l('Container batch update permission check') %]";
+ s.CONTAINER_PERM_CHECK = "[% l('Container permission check') %]";
+ s.ITEM_PERM_CHECK = "[% l('Item permission check') %]";
+ s.STAT_CAT_REMOVE = "[% l('Statistical category removal') %]";
+ s.STAT_CAT_APPLY = "[% l('Statistical category application') %]";
+ s.FIELDSET_GROUP_CREATE = "[% l('Fieldset group creation') %]";
+ s.FIELDSET_CREATE = "[% l('Fieldset creation') %]";
+ s.FIELDSET_EDITS_CREATE = "[% l('Fieldset change creation') %]";
+ s.CONSTRUCT_QUERY = "[% l('Query construction') %]";
+ s.APPLY_EDITS = "[% l('Applying edits') %]";
+ s.COMPLETE = "[% l('Complete') %]";
+ s.BATCH_FAILED = "[% l('Batch update failed!') %]";
+
+}]);
+</script>
+
+[% END %]
+
+<!-- using native Bootstrap taps because of limitations
+with angular-ui tabsets. it always defaults to making the
+first tab active, so it can't be driven from the route
+https://github.com/angular-ui/bootstrap/issues/910
+No JS is needed to drive the native tabs, since we're
+changing routes with each tab selection anyway.
+-->
+
+<ul class="nav nav-tabs">
+ <li ng-class="{active : tab == 'add'}">
+ <a href="./circ/patron/bucket/add/{{bucketSvc.currentBucket.id()}}">
+ [% l('Pending Users') %]
+ <span ng-cloak>({{bucketSvc.pendingList.length}})</span>
+ </a>
+ </li>
+ <li ng-class="{active : tab == 'view'}">
+ <a href="./circ/patron/bucket/view/{{bucketSvc.currentBucket.id()}}">
+ [% l('Bucket View') %]
+ <span ng-cloak>({{bucketSvc.currentBucket.items().length}})</span>
+ </a>
+ </li>
+</ul>
+<div class="tab-content">
+ <div class="tab-pane active">
+
+ <!-- bucket info header -->
+ <div class="row">
+ <div class="col-md-6">
+ [% INCLUDE 'staff/circ/patron/bucket/t_bucket_info.tt2' %]
+ </div>
+ </div>
+
+ <!-- bucket not accessible warning -->
+ <div class="col-md-10 col-md-offset-1" ng-show="forbidden">
+ <div class="alert alert-warning">
+ [% l('The selected bucket "{{bucketId}}" is not visible to this login.') %]
+ </div>
+ </div>
+
+ <div ng-view></div>
+ </div>
+</div>
+
+[% END %]
--- /dev/null
+<!-- edit bucket dialog -->
+
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(args)">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Create Bucket') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="edit-bucket-name">[% l('Name') %]</label>
+ <input type="text" class="form-control" focus-me='focusMe' required
+ id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
+ </div>
+ <div class="form-group">
+ <label for="edit-bucket-desc">[% l('Description') %]</label>
+ <input type="text" class="form-control" id="edit-bucket-desc"
+ ng-model="args.desc" placeholder="[% l('Description...') %]"/>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input ng-model="args.pub" type="checkbox"/>
+ [% l('Staff Sharable?') %]
+ </label>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" ng-disabled="form.$invalid"
+ class="btn btn-primary" value="[% l('Create Bucket') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+<div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Confirm Bucket Delete') %]</h4>
+ </div>
+ <div class="modal-body">
+ <p>[% l('Delete bucket {{bucket().name()}}?') %]</p>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-primary" ng-click="ok()">[% l('Delete Bucket') %]</button>
+ <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</div> <!-- modal-dialog -->
--- /dev/null
+<!-- edit bucket dialog -->
+<form class="form-validated" novalidate ng-submit="ok(args)" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Edit Bucket') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="edit-bucket-name">[% l('Name') %]</label>
+ <input type="text" class="form-control" focus-me='focusMe' required
+ id="edit-bucket-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
+ </div>
+ <div class="form-group">
+ <label for="edit-bucket-desc">[% l('Description') %]</label>
+ <input type="text" class="form-control" id="edit-bucket-desc"
+ ng-model="args.desc" placeholder="[% l('Description...') %]"/>
+ </div>
+ <div class="checkbox">
+ <label>
+ <input ng-model="args.pub" type="checkbox">
+ [% l('Staff Sharable?') %]
+ </label>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()"
+ ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+
+<div ng-show="bucket()">
+ <strong>[% l('Bucket: {{bucket().name()}}') %]</strong>
+ <span>
+ <ng-pluralize count="bucketSvc.currentBucket.items().length"
+ when="{'one': '[% l("1 item") %]', 'other': '[% l("{} items") %]'}">
+ </ng-pluralize>
+ </span>
+ <span> / [% l('Created {{bucket().create_time() | date}}') %]</span>
+ <span ng-show="bucket().description()"> / {{bucket().description()}}</span>
+</div>
+
+<div ng-show="!bucket()">
+ <strong>[% l('No Bucket Selected') %]</strong>
+</div>
+
--- /dev/null
+<div class="btn-group text-left" uib-dropdown>
+ <button type="button" class="btn btn-default" uib-dropdown-toggle>
+ [% l('Buckets') %]<span class="caret"></span>
+ </button>
+ <ul uib-dropdown-menu>
+ <li>
+ <a href='' ng-click="openCreateBucketDialog()">[% l('New Bucket') %]</a>
+ </li>
+ <li ng-class="{disabled : !bucket()}">
+ <a href='' ng-click="openEditBucketDialog()">[% l('Edit Bucket') %]</a>
+ </li>
+ <li ng-class="{disabled : !bucket()}">
+ <a href='' ng-click="openDeleteBucketDialog()">[% l('Delete Bucket') %]</a>
+ </li>
+ <li>
+ <a href='' ng-click="openSharedBucketDialog()">[% l('Load Shared Bucket') %]</a>
+ </li>
+ <li role="presentation" class="divider"></li>
+
+ <!-- list all of this user's buckets -->
+ <li ng-repeat="bkt in bucketSvc.allBuckets"
+ ng-class="{disabled : bkt.id() == bucket().id()}">
+ <a href='' ng-click="loadBucket(bkt.id())">{{bkt.name()}}</a>
+ </li>
+ </ul>
+</div>
+
--- /dev/null
+<!-- manage batch changes dialog -->
+<form class="form-validated" novalidate ng-submit="ok()" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('View batch changes') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-md-4">
+ <strong>[% l('Name') %]</strong>
+ </div>
+ <div class="col-md-3">
+ <strong>[% l('Completed') %]</strong>
+ </div>
+ <div class="col-md-3">
+ <strong>[% l('Rolled back') %]</strong>
+ </div>
+ </div>
+ <div class="row" ng-repeat="g in fieldset_groups track by $index">
+ <div class="col-md-4">
+ {{g.name()}}
+ </div>
+ <div class="col-md-3">
+ {{g.complete_time() | date}}
+ </div>
+ <div class="col-md-3">
+ {{g.rollback_time() | date}}
+ </div>
+ <div class="col-md-2">
+ <button class="btn btn-primary"
+ ng-click="deleteChangeset(g)">[% l('Delete') %]</button>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+<!-- edit bucket dialog -->
+<style>
+progress {
+ text-align: center;
+ height: 25px;
+ width: 500px;
+ margin-bottom: 10px;
+}
+
+progress:before {
+ content: attr(label);
+ position: relative;
+ top: 5px;
+}
+</style>
+
+<form class="form-validated" novalidate ng-submit="ok(args)" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Delete all users') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="edit-delete-name">[% l('Name for delete set') %]</label>
+ <input type="text" class="form-control" focus-me='focusMe' required
+ id="edit-delete-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()"
+ ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+ </div>
+ <div class="modal-body" ng-show='running'>
+ <div ng-repeat="progress in states">
+ <progress label="{{progress.label}}" max="{{progress.max}}" value="{{progress.count}}">{{progress.label}}</progress>
+ </div>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+
+<!-- global grid menu displayed on every Bucket page -->
+<eg-grid-menu-item label="[% l('New Bucket') %]"
+ handler="openCreateBucketDialog"></eg-grid-menu-item>
+
+<eg-grid-menu-item label="[% l('Edit Bucket') %]"
+ handler="openEditBucketDialog"></eg-grid-menu-item>
+
+<eg-grid-menu-item label="[% l('Delete Bucket') %]"
+ handler="openDeleteBucketDialog"></eg-grid-menu-item>
+
+<eg-grid-menu-item label="[% l('Shared Bucket') %]"
+ handler="openSharedBucketDialog"></eg-grid-menu-item>
+
+<eg-grid-menu-item divider="true"></eg-grid-menu-item>
+
+<eg-grid-menu-item ng-repeat="bkt in bucketSvc.allBuckets"
+ label="{{bkt.name()}}" handler-data="bkt"
+ handler="loadBucketFromMenu"></eg-grid-menu-item>
+
--- /dev/null
+<!-- load bucket by id ("shared") -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(args)">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Load Shared Bucket Bucket by ID') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="load-bucket-id">[% l('Bucket ID') %]</label>
+ <!-- NOTE: type='number' / required -->
+ <input type="number" class="form-control" focus-me='focusMe' required
+ id="load-bucket-id" ng-model="args.id" placeholder="[% l('Bucket ID...') %]"/>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" ng-disabled="form.$invalid"
+ class="btn btn-primary" value="[% l('Load Bucket') %]"/>
+ <button class="btn btn-warning"
+ ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
+
--- /dev/null
+<form ng-submit="search()">
+<div class="row">
+ <div class="col-md-6">
+ <div class="input-group">
+ <span class="input-group-addon">[% l('Scan Card') %]</span>
+ <input type="text" class="form-control" focus-me="focusMe"
+ ng-model="bucketSvc.barcodeString" placeholder="[% l('Barcode...') %]">
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="btn-pad" style="padding:4px;">
+ <div class="flex-row">
+ <div class="strong-text">[% l('OR') %]</div>
+ <div class="btn-pad">
+ <input type="file" eg-file-reader
+ container="barcodesFromFile" value="[% l('Upload from File') %]">
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+</form>
+
+<br/>
+
+<eg-grid
+ ng-hide="forbidden"
+ features="-sort,-multisort,-display"
+ id-field="id"
+ idl-class="au"
+ auto-fields="true"
+ grid-controls="gridControls"
+ items-provider="gridDataProvider"
+ menu-label="[% l('Buckets') %]"
+ persist-key="user.bucket.pending">
+
+ [% INCLUDE 'staff/circ/patron/bucket/t_grid_menu.tt2' %]
+
+ <!-- actions drop-down -->
+ <eg-grid-action label="[% l('Add To Bucket') %]"
+ handler="addToBucket"></eg-grid-action>
+
+ <eg-grid-action label="[% l('Clear List') %]"
+ handler="resetPendingList"></eg-grid-action>
+
+ <eg-grid-field path="id" required hidden></eg-grid-field>
+
+ <eg-grid-field label="[% l('Barcode') %]"
+ path="card.barcode" visible>
+ <a target="_self" href="[% ctx.base_path %]/staff/circ/patron/{{item.id}}/edit">
+ {{item['card.barcode']}}
+ </a>
+ </eg-grid-field>
+
+ <eg-grid-field label="[% l('First Name') %]" path="first_given_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Middle Name') %]" path="second_given_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Last Name') %]" path="family_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Home Library') %]" path="home_ou.name" visible></eg-grid-field>
+
+</eg-grid>
--- /dev/null
+<!-- edit bucket dialog -->
+<style>
+progress {
+ text-align: center;
+ height: 25px;
+ width: 500px;
+ margin-bottom: 10px;
+}
+
+progress:before {
+ content: attr(label);
+ position: relative;
+ top: 5px;
+}
+</style>
+
+<form class="form-validated" novalidate ng-submit="ok(args)" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Roll back batch edit') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <select
+ class="form-control"
+ ng-model="revert_me"
+ required
+ ng-options="g.name() for g in revertable_fieldset_groups track by g.id()"
+ >
+ <option value="">[% l('--- Select edit to roll back ---') %]</option>
+ </select>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="form.$invalid" value="[% l('Roll Back Changes') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()"
+ ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+ </div>
+ <div class="modal-body" ng-show='running'>
+ <div ng-repeat="progress in states">
+ <progress label="{{progress.label}}" max="{{progress.max}}" value="{{progress.count}}">{{progress.label}}</progress>
+ </div>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+<!-- edit bucket dialog -->
+<style>
+progress {
+ text-align: center;
+ height: 25px;
+ width: 500px;
+ margin-bottom: 10px;
+}
+
+progress:before {
+ content: attr(label);
+ position: relative;
+ top: 5px;
+}
+</style>
+
+<form class="form-validated" novalidate ng-submit="ok(args)" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Update all users') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="edit-name">[% l('Name for edit set') %]</label>
+ <input type="text" class="form-control" focus-me='focusMe' required
+ id="edit-name" ng-model="args.name" placeholder="[% l('Name...') %]"/>
+ </div>
+ <hr/>
+ <div>
+ <div class="row">
+ <div class="col-md-3">
+ <eg-org-selector
+ selected="args.home_ou"
+ nodefault
+ label="[% l('Home Library') %]"
+ disable-test="disable_home_org">
+ </eg-org-selector>
+ <br/>
+ <button class="btn btn-default" ng-click="unset_field($event,'home_ou')">[% l('Unset') %]</button>
+ </div>
+ <div class="col-md-3">
+ <div class="btn-group patron-search-selector" uib-dropdown>
+ <button type="button" class="btn btn-default" uib-dropdown-toggle>
+ <span style="padding-right: 5px;">{{args.profile.name() || "[% l('Main Profile') %]"}}</span>
+ <span class="caret"></span>
+ </button>
+ <ul uib-dropdown-menu>
+ <li ng-repeat="grp in profiles">
+ <a href a-disabled="grp.cannot_use"
+ style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
+ ng-click="args.profile = grp">{{grp.name()}}</a>
+ </li>
+ </ul>
+ <br/>
+ <button class="btn btn-default" ng-click="unset_field($event,'profile')">[% l('Unset') %]</button>
+ </div>
+ </div>
+ <div class="col-md-3">
+ <div class="btn-group patron-search-selector" uib-dropdown>
+ <button type="button" class="btn btn-default" uib-dropdown-toggle>
+ <span style="padding-right: 5px;">{{args.net_access_level.name() || "[% l('Internet Access Level') %]"}}</span>
+ <span class="caret"></span>
+ </button>
+ <ul uib-dropdown-menu>
+ <li ng-repeat="l in net_access_levels">
+ <a href
+ ng-click="args.net_access_level = l">{{l.name()}}</a>
+ </li>
+ </ul>
+ <br/>
+ <button class="btn btn-default" ng-click="unset_field($event,'net_access_level')">[% l('Unset') %]</button>
+ </div>
+ </div>
+ </div>
+ <br/>
+ <br/>
+ </div>
+ <div class="form-group">
+ <div class="row">
+ <div class="col-md-6">
+ <label for="edit-active">[% l('Barred flag') %]</label>
+ </div>
+ <div class="col-md-6">
+ <select class="form-control" id="edit-active" ng-model="args.barred">
+ <option value="">[% l('Unchanged') %]</option>
+ <option value="t">[% l('True') %]</option>
+ <option value="f">[% l('False') %]</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="row">
+ <div class="col-md-6">
+ <label for="edit-active">[% l('Active flag') %]</label>
+ </div>
+ <div class="col-md-6">
+ <select class="form-control" id="edit-active" ng-model="args.active">
+ <option value="">[% l('Unchanged') %]</option>
+ <option value="t">[% l('True') %]</option>
+ <option value="f">[% l('False') %]</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="row">
+ <div class="col-md-6">
+ <label for="edit-juvenile">[% l('Juvenile flag') %]</label>
+ </div>
+ <div class="col-md-6">
+ <select class="form-control" id="edit-juvenile" ng-model="args.juvenile">
+ <option value="">[% l('Unchanged') %]</option>
+ <option value="t">[% l('True') %]</option>
+ <option value="f">[% l('False') %]</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="row">
+ <div class="col-md-6">
+ <label for="edit-expire_date">[% l('Privilege Expiration Date') %]</label>
+ </div>
+ <div class="col-md-6">
+ <input type="date" class="form-control" id="edit-expire_date" ng-model="args.expire_date"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()"
+ ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+ </div>
+ <div class="modal-body" ng-show='running'>
+ <div ng-repeat="progress in states">
+ <progress label="{{progress.label}}" max="{{progress.max}}" value="{{progress.count}}">{{progress.label}}</progress>
+ </div>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+<!-- edit statcats dialog -->
+<style>
+progress {
+ text-align: center;
+ height: 25px;
+ width: 500px;
+ margin-bottom: 10px;
+}
+
+progress:before {
+ content: attr(label);
+ position: relative;
+ top: 5px;
+}
+</style>
+
+<form class="form-validated" novalidate ng-submit="ok(args)" name="form">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Update statistical categories') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div>
+ <div class="row" ng-repeat="sc in stat_cats track by $index">
+ <div class="col-md-4">
+ {{sc.name()}}
+ </div>
+ <div class="col-md-4">
+ <select class="form-control" ng-model="sc.new_value">
+ <option value="">[% l('Unchanged') %]</option>
+ <option ng-repeat="e in sc.entries()" value="{{e.value()}}">{{e.value()}}</option>
+ </select>
+ <input type="text" ng-disabled="!sc.allow_freetext()" class="form-control" ng-model="sc.new_value"/>
+ </div>
+ <div class="col-md-4">
+ <strong>[% l('Remove:') %]</strong> <input type="checkbox" ng-model="sc.delete_me"/>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary"
+ ng-disabled="form.$invalid" value="[% l('Apply Changes') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()"
+ ng-class="{disabled : actionPending}">[% l('Cancel') %]</button>
+ </div>
+ <div class="modal-body" ng-show='running'>
+ <div ng-repeat="progress in states">
+ <progress label="{{progress.label}}" max="{{progress.max}}" value="{{progress.count}}">{{progress.label}}</progress>
+ </div>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+<eg-grid
+ ng-hide="forbidden"
+ features="allowAll,-display"
+ id-field="id"
+ idl-class="au"
+ auto-fields="true"
+ grid-controls="gridControls"
+ menu-label="[% l('Buckets') %]"
+ persist-key="user.bucket.view">
+
+ [% INCLUDE 'staff/circ/patron/bucket/t_grid_menu.tt2' %]
+
+ <eg-grid-menu-item disabled="noDeletePerms" label="[% l('Delete all users') %]"
+ handler="deleteAllUsers" standalone="true"></eg-grid-menu-item>
+
+ <eg-grid-menu-item disabled="noUpdatePerms" label="[% l('Batch edit all users') %]"
+ handler="updateAllUsers" standalone="true"></eg-grid-menu-item>
+
+ <eg-grid-menu-item label="[% l('View batch edits') %]"
+ handler="viewChangesets" standalone="true"></eg-grid-menu-item>
+
+ <eg-grid-menu-item label="[% l('Roll back batch edit') %]"
+ handler="applyRollback" standalone="true"></eg-grid-menu-item>
+
+ <eg-grid-menu-item label="[% l('Batch modify statistical categories') %]"
+ handler="modifyStatcats" standalone="true"></eg-grid-menu-item>
+
+ <eg-grid-action label="[% l('Individually Edit Selected Users') %]"
+ handler="spawnUserEdit"></eg-grid-action>
+ <eg-grid-action label="[% l('Remove Selected Users from Bucket') %]"
+ handler="detachUsers"></eg-grid-action>
+
+ <eg-grid-field path="id" required hidden></eg-grid-field>
+
+ <eg-grid-field label="[% l('Barcode') %]"
+ path="card.barcode" visible>
+ <a target="_self" href="[% ctx.base_path %]/staff/circ/patron/{{item.id}}/edit">
+ {{item['card.barcode']}}
+ </a>
+ </eg-grid-field>
+
+ <eg-grid-field label="[% l('First Name') %]" path="first_given_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Middle Name') %]" path="second_given_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Last Name') %]" path="family_name" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Home Library') %]" path="home_ou.name" visible></eg-grid-field>
+ <eg-grid-field path="mailing_address.*" hidden></eg-grid-field>
+ <eg-grid-field path="billing_address.*" hidden></eg-grid-field>
+
+</eg-grid>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user-bucket.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/patrons.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/billing.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/circ.js"></script>
s.PATRON_PURGE_STAFF_BAD_BARCODE = "[% l('Could not retrieve a destination account with the barcode provided. Aborting the purge...') %]";
s.PATRON_PURGE_OVERRIDE_PROMPT = "[% l('The account has open transactions (circulations and/or unpaid bills). Purge anyway?') %]";
s.PATRON_EDIT_COLLISION = "[% l('Patron record was modified by another user while you were editing it. Your changes were not saved; please reapply them.') %]";
+ s.OPT_IN_DIALOG_TITLE = "[% l('Verify Permission to Share Personal Information') %]";
+ s.OPT_IN_DIALOG = "[% l('Does patron [_1], [_2] from [_3] ([_4]) consent to having their personal information shared with your library?', '{{family_name}}', '{{first_given_name}}', '{{org_name}}', '{{org_shortname}}') %]";
+ s.BUCKET_ADD_SUCCESS = "[% l('Successfully added [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]";
+ s.BUCKET_ADD_FAIL = "[% l('Failed to add [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]";
}]);
</script>
idl-class="au" id-field="id"
features="-sort,-display,-multisort"
main-label="[% l('Patron Search Results') %]"
+ menu-label="[% l('Add To Bucket') %]"
grid-controls="gridControls"
items-provider="patronSearchGridProvider"
persist-key="circ.patron.search"
dateformat="{{$root.egDateAndTimeFormat}}">
<eg-grid-menu-item handler="merge_patrons"
- disabled="need_two_selected"
+ disabled="need_two_selected" standalone="true"
label="[% l('Merge Patrons') %]"></eg-grid-menu-item>
+ <eg-grid-menu-item label="[% l('New Bucket') %]"
+ handler="openCreateBucketDialog"></eg-grid-menu-item>
+
+ <eg-grid-menu-item divider="true"></eg-grid-menu-item>
+
+ <eg-grid-menu-item ng-repeat="bkt in bucketSvc.allBuckets"
+ label="{{bkt.name()}}" handler-data="bkt"
+ handler="addToBucket" disabled="need_one_selected"></eg-grid-menu-item>
+
<eg-grid-field label="[% ('ID') %]" path='id' visible></eg-grid-field>
<eg-grid-field label="[% ('Card') %]" path='card.barcode' visible></eg-grid-field>
<eg-grid-field label="[% ('Profile') %]" path='profile.name' visible></eg-grid-field>
[% l('Pending Patrons') %]
</a>
</li>
+ <li>
+ <a href="./circ/patron/bucket/view" target="_self">
+ <span class="glyphicon glyphicon-list-alt"></span>
+ [% l('User Buckets') %]
+ </a>
+ </li>
<li class="divider"></li>
<li>
<a href="./circ/patron/credentials" target="_self">
* Search, checkout, items out, holds, bills, edit, etc.
*/
-angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
+angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast',
'egPatronSearchMod'])
* Manages patron search
*/
.controller('PatronSearchCtrl',
- ['$scope','$q','$routeParams','$timeout','$window','$location','egCore',
- '$filter','egUser', 'patronSvc','egGridDataProvider','$document',
- 'egPatronMerge','egProgressDialog','$controller',
-function($scope, $q, $routeParams, $timeout, $window, $location, egCore,
- $filter, egUser, patronSvc , egGridDataProvider , $document,
- egPatronMerge , egProgressDialog, $controller) {
+ ['$scope','$q','$routeParams','$timeout','$window','$location','egCore','ngToast',
+ '$filter','egUser', 'patronSvc','egGridDataProvider','$document','bucketSvc',
+ 'egPatronMerge','egProgressDialog','$controller','$interpolate','$uibModal',
+function($scope, $q, $routeParams, $timeout, $window, $location, egCore , ngToast,
+ $filter, egUser, patronSvc , egGridDataProvider , $document , bucketSvc,
+ egPatronMerge , egProgressDialog , $controller , $interpolate , $uibModal) {
angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
$scope.initTab('search');
activateItem : function(item) {
$location.path('/circ/patron/' + item.id() + '/checkout');
},
- selectedItems : function() {return []}
+ selectedItems : function() { return [] }
+ }
+
+ $scope.bucketSvc = bucketSvc;
+ $scope.bucketSvc.fetchUserBuckets();
+ $scope.addToBucket = function(item, data, recs) {
+ if (recs.length == 0) return;
+ var added_count = 0;
+ var failed_count = 0;
+ var p = [];
+ angular.forEach(recs,
+ function(rec) {
+ var item = new egCore.idl.cubi();
+ item.bucket(data.id());
+ item.target_user(rec.id());
+ p.push(egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.item.create',
+ egCore.auth.token(), 'user', item
+ ).then(
+ function(){ added_count++ },
+ function(){ failed_count++ }
+ ));
+ }
+ );
+
+ $q.all(p).then( function () {
+ if (added_count) ngToast.create($interpolate(egCore.strings.BUCKET_ADD_SUCCESS)({ count: ''+added_count, name: data.name()} ));
+ if (failed_count) ngToast.warning($interpolate(egCore.strings.BUCKET_ADD_FAIL)({ count: ''+failed_count, name: data.name() } ));
+ });
+ }
+
+ var temp_scope = $scope;
+ $scope.openCreateBucketDialog = function() {
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_bucket_create',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.focusMe = true;
+ $scope.ok = function(args) { $uibModalInstance.close(args) }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+ }]
+ }).result.then(function (args) {
+ if (!args || !args.name) return;
+ bucketSvc.createBucket(args.name, args.desc).then(
+ function(id) {
+ if (id) {
+ $scope.bucketSvc.fetchBucket(id).then(function (b) {
+ $scope.addToBucket(
+ null,
+ b,
+ $scope.gridControls.selectedItems()
+ );
+ $scope.bucketSvc.fetchUserBuckets(true);
+ });
+ }
+ }
+ );
+ });
}
$scope.$watch(
true
);
+ $scope.need_one_selected = function() {
+ var items = $scope.gridControls.selectedItems();
+ return (items.length > 0) ? false : true;
+ }
$scope.need_two_selected = function() {
var items = $scope.gridControls.selectedItems();
return (items.length == 2) ? false : true;
--- /dev/null
+/**
+ * User Buckets
+ *
+ * Known Issues
+ *
+ * add-all actions only add visible/fetched items.
+ * remove all from bucket UI leaves busted pagination
+ * -- apply a refresh after item removal?
+ * problems with bucket view fetching by record ID instead of bucket item:
+ * -- dupe bibs always sort to the bottom
+ * -- dupe bibs result in more records displayed per page than requested
+ * -- item 'pos' ordering is not honored on initial load.
+ */
+
+angular.module('egCatUserBuckets',
+ ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'egUserBucketMod', 'ngToast'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+ $locationProvider.html5Mode(true);
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+ var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+ $routeProvider.when('/circ/patron/bucket/add/:id', {
+ templateUrl: './circ/patron/bucket/t_pending',
+ controller: 'PendingCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.when('/circ/patron/bucket/add', {
+ templateUrl: './circ/patron/bucket/t_pending',
+ controller: 'PendingCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.when('/circ/patron/bucket/view/:id', {
+ templateUrl: './circ/patron/bucket/t_view',
+ controller: 'ViewCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.when('/circ/patron/bucket/view', {
+ templateUrl: './circ/patron/bucket/t_view',
+ controller: 'ViewCtrl',
+ resolve : resolver
+ });
+
+ // default page / bucket view
+ $routeProvider.otherwise({redirectTo : '/circ/patron/bucket/view'});
+})
+
+/**
+ * Top-level controller.
+ * Hosts functions needed by all controllers.
+ */
+.controller('UserBucketCtrl',
+ ['$scope','$location','$q','$timeout','$uibModal',
+ '$window','egCore','bucketSvc','ngToast',
+function($scope, $location, $q, $timeout, $uibModal,
+ $window, egCore, bucketSvc , ngToast) {
+
+ $scope.bucketSvc = bucketSvc;
+ $scope.bucket = function() { return bucketSvc.currentBucket }
+
+ // tabs: add, view
+ $scope.setTab = function(tab) {
+ $scope.tab = tab;
+
+ // for bucket selector; must be called after route resolve
+ bucketSvc.fetchUserBuckets();
+ };
+
+ $scope.loadBucketFromMenu = function(item, bucket) {
+ if (bucket) return $scope.loadBucket(bucket.id());
+ }
+
+ $scope.loadBucket = function(id) {
+ $location.path(
+ '/circ/patron/bucket/' +
+ $scope.tab + '/' + encodeURIComponent(id));
+ }
+
+ $scope.addToBucket = function(recs) {
+ if (recs.length == 0) return;
+ bucketSvc.bucketNeedsRefresh = true;
+
+ angular.forEach(recs,
+ function(rec) {
+ var item = new egCore.idl.cubi();
+ item.bucket(bucketSvc.currentBucket.id());
+ item.target_user(rec.id);
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.item.create',
+ egCore.auth.token(), 'user', item
+ ).then(function(resp) {
+
+ // HACK: add the IDs of the added items so that the size
+ // of the view list will grow (and update any UI looking at
+ // the list size). The data stored is inconsistent, but since
+ // we are forcing a bucket refresh on the next rendering of
+ // the view pane, the list will be repaired.
+ bucketSvc.currentBucket.items().push(resp);
+ });
+ }
+ );
+ $scope.resetPendingList();
+ }
+
+ $scope.openCreateBucketDialog = function() {
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_bucket_create',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.focusMe = true;
+ $scope.ok = function(args) { $uibModalInstance.close(args) }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+ }]
+ }).result.then(function (args) {
+ if (!args || !args.name) return;
+ bucketSvc.createBucket(args.name, args.desc).then(
+ function(id) {
+ if (!id) return;
+ bucketSvc.viewList = [];
+ bucketSvc.allBuckets = []; // reset
+ bucketSvc.currentBucket = null;
+ $location.path(
+ '/circ/patron/bucket/' + $scope.tab + '/' + id);
+ }
+ );
+ });
+ }
+
+ $scope.openEditBucketDialog = function() {
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_bucket_edit',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.focusMe = true;
+ $scope.args = {
+ name : bucketSvc.currentBucket.name(),
+ desc : bucketSvc.currentBucket.description(),
+ pub : bucketSvc.currentBucket.pub() == 't'
+ };
+ $scope.ok = function(args) {
+ if (!args) return;
+ $scope.actionPending = true;
+ args.pub = args.pub ? 't' : 'f';
+ // close the dialog after edit has completed
+ bucketSvc.editBucket(args).then(
+ function() { $uibModalInstance.close() });
+ }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+ }]
+ })
+ }
+
+ // opens the delete confirmation and deletes the current
+ // bucket if the user confirms.
+ $scope.openDeleteBucketDialog = function() {
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_bucket_delete',
+ controller :
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.bucket = function() { return bucketSvc.currentBucket }
+ $scope.ok = function() { $uibModalInstance.close() }
+ $scope.cancel = function() { $uibModalInstance.dismiss() }
+ }]
+ }).result.then(function () {
+ bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
+ .then(function() {
+ bucketSvc.allBuckets = [];
+ $location.path('/circ/patron/bucket/view');
+ });
+ });
+ }
+
+ // retrieves the requested bucket by ID
+ $scope.openSharedBucketDialog = function() {
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_load_shared',
+ controller :
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.focusMe = true;
+ $scope.ok = function(args) {
+ if (args && args.id) {
+ $uibModalInstance.close(args.id)
+ }
+ }
+ $scope.cancel = function() { $uibModalInstance.dismiss() }
+ }]
+ }).result.then(function(id) {
+ // RecordBucketCtrl $scope is not inherited by the
+ // modal, so we need to call loadBucket from the
+ // promise resolver.
+ $scope.loadBucket(id);
+ });
+ }
+
+}])
+
+.controller('PendingCtrl',
+ ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
+function($scope, $routeParams, bucketSvc , egGridDataProvider, egCore , ngToast , $q) {
+ $scope.setTab('add');
+
+ var query;
+ $scope.gridControls = {
+ setQuery : function(q) {
+ if (bucketSvc.pendingList.length)
+ return {id : bucketSvc.pendingList};
+ else
+ return null;
+ }
+ }
+
+ $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
+ if (newVal && newVal != oldVal) {
+ var promises = [];
+ // $scope.resetPendingList(); // ??? Add instead of replace
+ angular.forEach(newVal.split(/\n/), function(line) {
+ if (!line) return;
+ // scrub any trailing spaces or commas from the barcode
+ line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
+ promises.push(egCore.pcrud.search(
+ 'ac',
+ {barcode : line},
+ {}
+ ).then(null, null, function(card) {
+ bucketSvc.pendingList.push(card.usr());
+ }));
+ });
+
+ $q.all(promises).then(function () {
+ $scope.gridControls.setQuery({id : bucketSvc.pendingList});
+ });
+ }
+ });
+
+ $scope.search = function() {
+ bucketSvc.barcodeRecords = [];
+
+ egCore.pcrud.search(
+ 'ac',
+ {barcode : bucketSvc.barcodeString},
+ {}
+ ).then(null, null, function(card) {
+ bucketSvc.pendingList.push(card.usr());
+ $scope.gridControls.setQuery({id : bucketSvc.pendingList});
+ });
+ bucketSvc.barcodeString = '';
+ }
+
+ $scope.resetPendingList = function() {
+ bucketSvc.pendingList = [];
+ $scope.gridControls.setQuery({});
+ }
+
+ $scope.$parent.resetPendingList = $scope.resetPendingList;
+
+ if ($routeParams.id &&
+ (!bucketSvc.currentBucket ||
+ bucketSvc.currentBucket.id() != $routeParams.id)) {
+ // user has accessed this page cold with a bucket ID.
+ // fetch the bucket for display, then set the totalCount
+ // (also for display), but avoid fully fetching the bucket,
+ // since it's premature, in this UI.
+ bucketSvc.fetchBucket($routeParams.id);
+ }
+ $scope.gridControls.setQuery();
+}])
+
+.controller('ViewCtrl',
+ ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egUser',
+ 'egConfirmDialog','egPerm','ngToast','$filter',
+function($scope, $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egUser ,
+ egConfirmDialog , egPerm , ngToast , $filter) {
+
+ $scope.setTab('view');
+ $scope.bucketId = $routeParams.id;
+
+ var query;
+ $scope.gridControls = {
+ setQuery : function(q) {
+ if (q) query = q;
+ return query;
+ }
+ };
+
+ $scope.modifyStatcats = function() {
+ bucketSvc.bucketNeedsRefresh = true;
+
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_update_statcats',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.running = false;
+ $scope.complete = false;
+ $scope.states = [];
+
+ $scope.modal = $uibModalInstance;
+ $scope.ok = function(args) { $uibModalInstance.close() }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ $scope.current_bucket = bucketSvc.currentBucket;
+
+ egCore.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.stat_cat.actor.retrieve.all',
+ egCore.auth.token(), egCore.auth.user().ws_ou()
+ ).then(function(cats) {
+ cats = cats.sort(function(a, b) {
+ return a.name() < b.name() ? -1 : 1});
+ angular.forEach(cats, function(cat) {
+ cat.new_value = '';
+ cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
+ cat.entries(
+ cat.entries().sort(function(a,b) {
+ return a.value() < b.value() ? -1 : 1
+ })
+ );
+ });
+ $scope.stat_cats = cats;
+ });
+
+ // This handels the progress magic instead of a normal close handler
+ $scope.$on('modal.closing', function(event, reason, closed) {
+ if (!closed) return; // dismissed
+ if ($scope.complete) return; // already done
+
+ $scope.running = true;
+
+ var changes = {remove:[], apply:{}};
+ angular.forEach($scope.stat_cats, function (sc) {
+ if (sc.delete_me) {
+ changes.remove.push(sc.id());
+ } else if (sc.new_value) {
+ changes.apply[sc.id()] = sc.new_value;
+ }
+ });
+
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.user.batch_statcat_apply',
+ egCore.auth.token(), bucketSvc.currentBucket.id(), changes
+ ).then(
+ function () {
+ $scope.complete = true;
+ $scope.modal.close();
+ drawBucket();
+ },
+ function (err) { console.log('User edit error: ' + err); },
+ function (p) {
+ if (p.error) {
+ ngToast.warning(p.error);
+ }
+ if (p.stage == 'COMPLETE') return;
+
+ p.label = egCore.strings[p.stage];
+ if (!p.max) {
+ p.max = 1;
+ p.count = 1;
+ }
+ $scope.states[p.ord] = p;
+ }
+ );
+
+ return event.preventDefault();
+ });
+ }]
+ });
+ }
+
+
+ function drawBucket() {
+ return bucketSvc.fetchBucket($scope.bucketId).then(
+ function(bucket) {
+ var ids = bucket.items().map(
+ function(i){return i.target_user()}
+ );
+ if (ids.length) {
+ $scope.gridControls.setQuery({id : ids});
+ } else {
+ $scope.gridControls.setQuery({});
+ }
+ }
+ );
+ }
+
+ $scope.no_update_perms = true;
+ $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
+
+ egPerm.hasPermHere(['UPDATE_USER']).then(
+ function (hash) {
+ if (Object.keys(hash).length == 0) return;
+
+ var one_false = false;
+ angular.forEach(hash, function(has) {
+ if (!has) one_false = true;
+ });
+
+ if (!one_false) $scope.no_update_perms = false;
+ }
+ );
+
+ function annotate_groups(grps) {
+ angular.forEach(grps, function (g) {
+ if (!g.hasOwnProperty('cannot_use')) {
+ if (g.usergroup() == 'f') {
+ g.cannot_use = true;
+ } else if (g.application_perm) {
+ egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
+ function (hash) {
+ if (Object.keys(hash).length == 0) {
+ g.cannot_use = true;
+ return;
+ }
+
+ var one_false = false;
+ angular.forEach(hash, function(has) {
+ if (has) g.cannot_use = false;
+ });
+ }
+ );
+ } else {
+ g.cannot_use = false;
+ }
+ }
+ });
+ }
+
+ $scope.viewChangesets = function() {
+ bucketSvc.bucketNeedsRefresh = true;
+
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_changesets',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.running = false;
+ $scope.complete = false;
+ $scope.states = [];
+
+ $scope.focusMe = true;
+ $scope.modal = $uibModalInstance;
+ $scope.ok = function() { $uibModalInstance.close() }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ $scope.current_bucket = bucketSvc.currentBucket;
+ $scope.fieldset_groups = [];
+
+ $scope.deleteChangeset = function (grp) {
+ egCore.pcrud.remove(grp).then(
+ function () {
+ if (grp.rollback_group()) {
+ egCore.pcrud
+ .retrieve('afsg',grp.rollback_group())
+ .then(function(g) {
+ egCore.pcrud.remove(g)
+ .then( function () { refresh_groups() } );
+ });
+ }
+ }
+ );
+ return event.preventDefault();
+ }
+
+ function refresh_groups () {
+ $scope.fieldset_groups = [];
+ egCore.pcrud.search('afsg',{
+ rollback_group : { '>' : 0 },
+ container : bucketSvc.currentBucket.id(),
+ container_type : 'user'
+ } ).then( null,null,function(g) {
+ $scope.fieldset_groups.push(g);
+ });
+ }
+ refresh_groups();
+
+ }]
+ });
+ }
+
+ $scope.applyRollback = function() {
+ bucketSvc.bucketNeedsRefresh = true;
+
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_rollback',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.running = false;
+ $scope.complete = false;
+ $scope.states = [];
+ $scope.revert_me = null;
+
+ $scope.focusMe = true;
+ $scope.modal = $uibModalInstance;
+ $scope.ok = function(args) { $uibModalInstance.close() }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ $scope.current_bucket = bucketSvc.currentBucket;
+ $scope.revertable_fieldset_groups = [];
+
+ egCore.pcrud.search('afsg',{
+ rollback_group : { '>' : 0},
+ rollback_time : null,
+ container : bucketSvc.currentBucket.id(),
+ container_type : 'user'
+ } ).then( null,null,function(g) {
+ $scope.revertable_fieldset_groups.push(g);
+ });
+
+ // This handels the progress magic instead of a normal close handler
+ $scope.$on('modal.closing', function(event, reason, closed) {
+ if (!$scope.revert_me) return;
+ if (!closed) return; // dismissed
+ if ($scope.complete) return; // already done
+
+ $scope.running = true;
+
+ var last_stage = '';
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.user.apply_rollback',
+ egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
+ ).then(
+ function () {
+ $scope.complete = true;
+ $scope.modal.close();
+ drawBucket();
+ },
+ function (err) { console.log('User edit error: ' + err); },
+ function (p) {
+ last_stage = p.stage;
+ if (p.error) {
+ ngToast.warning(p.error);
+ }
+ if (p.stage == 'COMPLETE') return;
+
+ p.label = egCore.strings[p.stage];
+ if (!p.max) {
+ p.max = 1;
+ p.count = 1;
+ }
+ $scope.states[p.ord] = p;
+ }
+ ).then(function() {
+ if (last_stage != 'COMPLETE')
+ ngToast.warning(egCore.strings.BATCH_FAILED);
+ });
+
+ return event.preventDefault();
+ });
+ }]
+ });
+ }
+
+ $scope.updateAllUsers = function() {
+ bucketSvc.bucketNeedsRefresh = true;
+
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_update_all',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.running = false;
+ $scope.complete = false;
+ $scope.states = [];
+ $scope.home_ou_name = '';
+ $scope.args = {home_ou:null};
+ $scope.focusMe = true;
+ $scope.modal = $uibModalInstance;
+ $scope.ok = function(args) { $uibModalInstance.close() }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ $scope.disable_home_org = function(org_id) {
+ if (!org_id) return;
+ var org = egCore.org.get(org_id);
+ return (
+ org &&
+ org.ou_type() &&
+ org.ou_type().can_have_users() == 'f'
+ );
+ }
+
+ $scope.pgt_depth = function(grp) {
+ var d = 0;
+ while (grp = egCore.env.pgt.map[grp.parent()]) d++;
+ return d;
+ }
+
+ if (egCore.env.cnal) {
+ $scope.net_access_levels = egCore.env.cnal.list;
+ } else {
+ egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
+ .then(function(types) {
+ egCore.env.absorbList(types, 'cnal')
+ $scope.net_access_levels = egCore.env.cnal.list;
+ });
+ }
+
+ if (egCore.env.pgt) {
+ $scope.profiles = egCore.env.pgt.list;
+ annotate_groups($scope.profiles);
+ } else {
+ egCore.pcrud.search('pgt', {parent : null},
+ {flesh : -1, flesh_fields : {pgt : ['children']}}
+ ).then(
+ function(tree) {
+ egCore.env.absorbTree(tree, 'pgt')
+ $scope.profiles = egCore.env.pgt.list;
+ annotate_groups($scope.profiles);
+ }
+ );
+ }
+
+ $scope.unset_field = function (event,field) {
+ $scope.args[field] = null;
+ return event.preventDefault();
+ }
+
+ // This handels the progress magic instead of a normal close handler
+ $scope.$on('modal.closing', function(event, reason, closed) {
+ if (!$scope.args || !$scope.args.name) return;
+ if (!closed) return; // dismissed
+ if ($scope.complete) return; // already done
+
+ $scope.running = true;
+
+ // XXX fix up $scope.args values here
+ if ($scope.args.home_ou) {
+ $scope.args.home_ou = $scope.args.home_ou.id();
+ }
+ if ($scope.args.net_access_level) {
+ $scope.args.net_access_level = $scope.args.net_access_level.id();
+ }
+ if ($scope.args.profile) {
+ $scope.args.profile = $scope.args.profile.id();
+ }
+ if ($scope.args.expire_date) {
+ $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
+ }
+
+ for (var key in $scope.args) {
+ if (!$scope.args[key] && $scope.args[key] !== 0) {
+ delete $scope.args[key];
+ }
+ }
+
+ var last_stage = '';
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.user.batch_edit',
+ egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
+ ).then(
+ function () {
+ $scope.complete = true;
+ $scope.modal.close();
+ drawBucket();
+ },
+ function (err) { console.log('User edit error: ' + err); },
+ function (p) {
+ last_stage = p.stage;
+ if (p.error) {
+ ngToast.warning(p.error);
+ }
+ if (p.stage == 'COMPLETE') return;
+
+ p.label = egCore.strings[p.stage];
+ if (!p.max) {
+ p.max = 1;
+ p.count = 1;
+ }
+ $scope.states[p.ord] = p;
+ }
+ ).then(function() {
+ if (last_stage != 'COMPLETE')
+ ngToast.warning(egCore.strings.BATCH_FAILED);
+ });
+
+ return event.preventDefault();
+ });
+ }]
+ });
+ }
+
+ $scope.no_delete_perms = true;
+ $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
+
+ egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
+ function (hash) {
+ if (Object.keys(hash).length == 0) return;
+
+ var one_false = false;
+ angular.forEach(hash, function(has) {
+ if (!has) one_false = true;
+ });
+
+ if (!one_false) $scope.no_delete_perms = false;
+ }
+ );
+
+ $scope.deleteAllUsers = function() {
+ bucketSvc.bucketNeedsRefresh = true;
+
+ $uibModal.open({
+ templateUrl: './circ/patron/bucket/t_delete_all',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.running = false;
+ $scope.complete = false;
+ $scope.states = [];
+ $scope.args = {};
+ $scope.focusMe = true;
+ $scope.modal = $uibModalInstance;
+ $scope.ok = function(args) { $uibModalInstance.close() }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ // This handels the progress magic instead of a normal close handler
+ $scope.$on('modal.closing', function(event, reason, closed) {
+ if (!$scope.args || !$scope.args.name) return;
+ if (!closed) return; // dismissed
+ if ($scope.complete) return; // already done
+
+ $scope.running = true;
+
+ var last_stage = '';
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.user.batch_delete',
+ egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
+ ).then(
+ function () {
+ $scope.complete = true;
+ $scope.modal.close();
+ drawBucket();
+ },
+ function (err) { console.log('User deletion error: ' + err); },
+ function (p) {
+ last_stage = p.stage;
+ if (p.error) {
+ ngToast.warning(p.error);
+ }
+ if (p.stage == 'COMPLETE') return;
+
+ p.label = egCore.strings[p.stage];
+ if (!p.max) {
+ p.max = 1;
+ p.count = 1;
+ }
+ $scope.states[p.ord] = p;
+ }
+ ).then(function() {
+ if (last_stage != 'COMPLETE')
+ ngToast.warning(egCore.strings.BATCH_FAILED);
+ });
+
+ return event.preventDefault();
+ });
+ }]
+ });
+
+ }
+
+ $scope.detachUsers = function(users) {
+ var promises = [];
+ angular.forEach(users, function(rec) {
+ var item = bucketSvc.currentBucket.items().filter(
+ function(i) {
+ return (i.target_user() == rec.id)
+ }
+ );
+ if (item.length)
+ promises.push(bucketSvc.detachUser(item[0].id()));
+ });
+
+ bucketSvc.bucketNeedsRefresh = true;
+ return $q.all(promises).then(drawBucket);
+ }
+
+ $scope.spawnUserEdit = function (users) {
+ angular.forEach($scope.gridControls.selectedItems(), function (i) {
+ var url = egCore.env.basePath + 'circ/patron/' + i.id + '/edit';
+ $timeout(function() { $window.open(url, '_blank') });
+ })
+ }
+
+ // fetch the bucket; on error show the not-allowed message
+ if ($scope.bucketId)
+ drawBucket()['catch'](function() { $scope.forbidden = true });
+}])
--- /dev/null
+/**
+ * User Buckets
+ *
+ */
+
+angular.module('egUserBucketMod', ['egCoreMod'])
+.factory('bucketSvc', ['$q','egCore', function($q, egCore) {
+
+ var service = {
+ allBuckets : [], // un-fleshed user buckets
+ barcodeString : '', // last scanned barcode
+ barcodeRecords : [], // last scanned barcode results
+ currentBucket : null, // currently viewed bucket
+
+ // per-page list collections
+ pendingList : [],
+ viewList : [],
+
+ // fetches all staff/user buckets for the authenticated user
+ // this function may only be called after startup.
+ fetchUserBuckets : function(force) {
+ if (this.allBuckets.length && !force) return;
+ var self = this;
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.retrieve_by_class.authoritative',
+ egCore.auth.token(), egCore.auth.user().id(),
+ 'user', 'staff_client'
+ ).then(function(buckets) { self.allBuckets = buckets });
+ },
+
+ createBucket : function(name, desc) {
+ var deferred = $q.defer();
+ var bucket = new egCore.idl.cub();
+ bucket.owner(egCore.auth.user().id());
+ bucket.name(name);
+ bucket.description(desc || '');
+ bucket.btype('staff_client');
+
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.create',
+ egCore.auth.token(), 'user', bucket
+ ).then(function(resp) {
+ if (resp) {
+ if (typeof resp == 'object') {
+ console.error('bucket create error: ' + js2JSON(resp));
+ deferred.reject();
+ } else {
+ deferred.resolve(resp);
+ }
+ }
+ });
+
+ return deferred.promise;
+ },
+
+ // edit the current bucket. since we edit the
+ // local object, there's no need to re-fetch.
+ editBucket : function(args) {
+ var bucket = service.currentBucket;
+ bucket.name(args.name);
+ bucket.description(args.desc);
+ bucket.pub(args.pub);
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.update',
+ egCore.auth.token(), 'user', bucket
+ );
+ }
+ }
+
+ // returns 1 if full refresh is needed
+ // returns 2 if list refresh only is needed
+ service.bucketRefreshLevel = function(id) {
+ if (!service.currentBucket) return 1;
+ if (service.bucketNeedsRefresh) {
+ service.bucketNeedsRefresh = false;
+ service.currentBucket = null;
+ return 1;
+ }
+ if (service.currentBucket.id() != id) return 1;
+ return 2;
+ }
+
+ // returns a promise, resolved with bucket, rejected if bucket is
+ // not fetch-able
+ service.fetchBucket = function(id) {
+ var refresh = service.bucketRefreshLevel(id);
+ if (refresh == 2) return $q.when(service.currentBucket);
+
+ var deferred = $q.defer();
+
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.flesh.authoritative',
+ egCore.auth.token(), 'user', id
+ ).then(function(bucket) {
+ var evt = egCore.evt.parse(bucket);
+ if (evt) {
+ console.debug(evt);
+ deferred.reject(evt);
+ return;
+ }
+ service.currentBucket = bucket;
+ deferred.resolve(bucket);
+ });
+
+ return deferred.promise;
+ }
+
+ // deletes a single container item from a bucket by container item ID.
+ // promise is rejected on failure
+ service.detachUser = function(itemId) {
+ var deferred = $q.defer();
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.item.delete',
+ egCore.auth.token(), 'user', itemId
+ ).then(function(resp) {
+ var evt = egCore.evt.parse(resp);
+ if (evt) {
+ console.error(evt);
+ deferred.reject(evt);
+ return;
+ }
+ console.log('detached bucket item ' + itemId);
+ deferred.resolve(resp);
+ });
+
+ return deferred.promise;
+ }
+
+ // delete bucket by ID.
+ // resolved w/ response on successful delete,
+ // rejected otherwise.
+ service.deleteBucket = function(id) {
+ var deferred = $q.defer();
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.full_delete',
+ egCore.auth.token(), 'user', id
+ ).then(function(resp) {
+ var evt = egCore.evt.parse(resp);
+ if (evt) {
+ console.error(evt);
+ deferred.reject(evt);
+ return;
+ }
+ deferred.resolve(resp);
+ });
+ return deferred.promise;
+ }
+
+ return service;
+}])