View | Details | Raw Unified | Return to bug 10190
Collapse All | Expand All

(-)a/Koha/REST/V1/CirculationRules.pm (-1 / +15 lines)
Lines 19-24 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious::Controller';
20
use Mojo::Base 'Mojolicious::Controller';
21
21
22
use C4::Auth qw( haspermission );
22
use Koha::CirculationRules;
23
use Koha::CirculationRules;
23
24
24
=head1 API
25
=head1 API
Lines 240-245 sub set_rules { Link Here
240
            ) unless $library;
241
            ) unless $library;
241
        }
242
        }
242
243
244
        my $logged_in_user = $c->stash('koha.user');
245
        unless ( haspermission( $logged_in_user->userid, { parameters => 'manage_circ_rules_from_any_libraries' } ) ) {
246
            if ( !defined $branchcode || $branchcode ne $logged_in_user->branchcode ) {
247
                return $c->render(
248
                    status  => 403,
249
                    openapi => {
250
                        error =>
251
                            'manage_circ_rules_from_any_libraries permission is required to manage circulation rules for libraries other than your own'
252
                    }
253
                );
254
            }
255
        }
256
243
        if ( $patron_category eq '*' ) {
257
        if ( $patron_category eq '*' ) {
244
            $patron_category = undef;
258
            $patron_category = undef;
245
        } else {
259
        } else {
Lines 326-332 sub config { Link Here
326
    my $c = shift->openapi->valid_input or return;
340
    my $c = shift->openapi->valid_input or return;
327
341
328
    my $patron      = $c->stash('koha.user');
342
    my $patron      = $c->stash('koha.user');
329
    my $userflags   = C4::Auth::haspermission( $patron->userid );
343
    my $userflags   = haspermission( $patron->userid );
330
    my $permissions = Koha::Auth::Permissions->get_authz_from_flags( { flags => $userflags } );
344
    my $permissions = Koha::Auth::Permissions->get_authz_from_flags( { flags => $userflags } );
331
345
332
    return $c->render(
346
    return $c->render(
(-)a/api/v1/swagger/paths/circulation_rules.yaml (+3 lines)
Lines 77-82 Link Here
77
    x-koha-authorization:
77
    x-koha-authorization:
78
      permissions:
78
      permissions:
79
        - circulate: circulate_remaining_permissions
79
        - circulate: circulate_remaining_permissions
80
        - parameters: manage_circ_rules
80
  put:
81
  put:
81
    x-mojo-to: CirculationRules#set_rules
82
    x-mojo-to: CirculationRules#set_rules
82
    operationId: setCirculationRules
83
    operationId: setCirculationRules
Lines 131-136 Link Here
131
    x-koha-authorization:
132
    x-koha-authorization:
132
      permissions:
133
      permissions:
133
        - circulate: circulate_remaining_permissions
134
        - circulate: circulate_remaining_permissions
135
        - parameters: manage_circ_rules
134
/circulation_rules/kinds:
136
/circulation_rules/kinds:
135
  get:
137
  get:
136
    x-mojo-to: CirculationRules#get_kinds
138
    x-mojo-to: CirculationRules#get_kinds
Lines 169-171 Link Here
169
    x-koha-authorization:
171
    x-koha-authorization:
170
      permissions:
172
      permissions:
171
        - circulate: circulate_remaining_permissions
173
        - circulate: circulate_remaining_permissions
174
        - parameters: manage_circ_rules
(-)a/api/v1/swagger/paths/circulation_rules_config.yaml (+1 lines)
Lines 36-38 Link Here
36
    x-koha-authorization:
36
    x-koha-authorization:
37
      permissions:
37
      permissions:
38
        - circulate: circulate_remaining_permissions
38
        - circulate: circulate_remaining_permissions
39
        - parameters: manage_circ_rules
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/circulation_triggers.tt (+1 lines)
Lines 34-39 Link Here
34
            const letters = [% To.json(letters) | $raw %];
34
            const letters = [% To.json(letters) | $raw %];
35
            const default_view = "[%- IF Koha.Preference('DefaultToLoggedInLibraryOverdueTriggers') -%][% Branches.GetLoggedInBranchcode | html %][%- ELSE -%]*[%- END -%]";
35
            const default_view = "[%- IF Koha.Preference('DefaultToLoggedInLibraryOverdueTriggers') -%][% Branches.GetLoggedInBranchcode | html %][%- ELSE -%]*[%- END -%]";
36
            const from_branch = "[% Koha.Preference('OverdueNoticeFrom') | html %]";
36
            const from_branch = "[% Koha.Preference('OverdueNoticeFrom') | html %]";
37
            const logged_in_library_id = "[% Branches.GetLoggedInBranchcode | html %]";
37
        </script>
38
        </script>
38
        [% Asset.js("js/vue/dist/admin/circulation_triggers.js") | $raw %]
39
        [% Asset.js("js/vue/dist/admin/circulation_triggers.js") | $raw %]
39
    [% END %]
40
    [% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue (-3 / +22 lines)
Lines 36-43 Link Here
36
                            v-model="context.library_id"
36
                            v-model="context.library_id"
37
                            label="name"
37
                            label="name"
38
                            :reduce="lib => lib.library_id"
38
                            :reduce="lib => lib.library_id"
39
                            :options="libraries"
39
                            :options="
40
                            :disabled="editMode !== 'confirmContext'"
40
                                canManageAnyLibrary
41
                                    ? libraries
42
                                    : libraries.filter(
43
                                          lib =>
44
                                              lib.library_id ===
45
                                              logged_in_library_id
46
                                      )
47
                            "
48
                            :disabled="
49
                                editMode !== 'confirmContext' ||
50
                                !canManageAnyLibrary
51
                            "
41
                        >
52
                        >
42
                            <template #search="{ attributes, events }">
53
                            <template #search="{ attributes, events }">
43
                                <input
54
                                <input
Lines 442-447 export default { Link Here
442
            patronCategories,
453
            patronCategories,
443
            triggerCounts,
454
            triggerCounts,
444
            storeInitialized,
455
            storeInitialized,
456
            canManageAnyLibrary,
457
            logged_in_library_id,
445
        } = storeToRefs(circRulesStore);
458
        } = storeToRefs(circRulesStore);
446
459
447
        return {
460
        return {
Lines 458-463 export default { Link Here
458
            updateCircRuleSets,
471
            updateCircRuleSets,
459
            hasConflict,
472
            hasConflict,
460
            storeInitialized,
473
            storeInitialized,
474
            canManageAnyLibrary,
475
            logged_in_library_id,
461
        };
476
        };
462
    },
477
    },
463
    data() {
478
    data() {
Lines 659-665 export default { Link Here
659
            this.ruleSetInitialized = true;
674
            this.ruleSetInitialized = true;
660
        },
675
        },
661
        setContext(query) {
676
        setContext(query) {
662
            this.context.library_id = query.library_id ?? "*";
677
            if (!this.canManageAnyLibrary && this.logged_in_library_id) {
678
                this.context.library_id = this.logged_in_library_id;
679
            } else {
680
                this.context.library_id = query.library_id ?? "*";
681
            }
663
            this.context.item_type_id = query.item_type_id ?? "*";
682
            this.context.item_type_id = query.item_type_id ?? "*";
664
            this.context.patron_category_id = query.patron_category_id ?? "*";
683
            this.context.patron_category_id = query.patron_category_id ?? "*";
665
        },
684
        },
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue (-9 / +26 lines)
Lines 138-143 Link Here
138
                Filter by
138
                Filter by
139
                <span style="color: blue; font-weight: bold">context</span>
139
                <span style="color: blue; font-weight: bold">context</span>
140
            </legend>
140
            </legend>
141
            <p v-if="!canManageAnyLibrary" class="alert alert-info">
142
                {{
143
                    $__(
144
                        "You can only manage circulation rules for your own library."
145
                    )
146
                }}
147
            </p>
141
            <table>
148
            <table>
142
                <thead>
149
                <thead>
143
                    <tr>
150
                    <tr>
Lines 154-164 Link Here
154
                                v-model="currentLibraryId"
161
                                v-model="currentLibraryId"
155
                                label="name"
162
                                label="name"
156
                                :reduce="lib => lib.library_id"
163
                                :reduce="lib => lib.library_id"
157
                                :options="libraries"
164
                                :options="
165
                                    canManageAnyLibrary
166
                                        ? libraries
167
                                        : libraries.filter(
168
                                              lib =>
169
                                                  lib.library_id ===
170
                                                  logged_in_library_id
171
                                          )
172
                                "
158
                                @update:modelValue="
173
                                @update:modelValue="
159
                                    filterRuleSetsbySearchParam()
174
                                    filterRuleSetsbySearchParam()
160
                                "
175
                                "
161
                                :clearable="false"
176
                                :clearable="false"
177
                                :disabled="!canManageAnyLibrary"
162
                                placeholder="Default rules for all libraries"
178
                                placeholder="Default rules for all libraries"
163
                            >
179
                            >
164
                                <template #search="{ attributes, events }">
180
                                <template #search="{ attributes, events }">
Lines 353-358 export default { Link Here
353
            librariesWithRules,
369
            librariesWithRules,
354
            storeInitialized,
370
            storeInitialized,
355
            metaInitialized,
371
            metaInitialized,
372
            canManageAnyLibrary,
373
            logged_in_library_id,
356
        } = storeToRefs(circRulesStore);
374
        } = storeToRefs(circRulesStore);
357
375
358
        return {
376
        return {
Lines 375-380 export default { Link Here
375
            storeInitialized,
393
            storeInitialized,
376
            metaInitialized,
394
            metaInitialized,
377
            from_branch,
395
            from_branch,
396
            canManageAnyLibrary,
397
            logged_in_library_id,
378
        };
398
        };
379
    },
399
    },
380
    data() {
400
    data() {
Lines 487-501 export default { Link Here
487
                ) {
507
                ) {
488
                    if (!this.metaInitialized) {
508
                    if (!this.metaInitialized) {
489
                        await new Promise(resolve => {
509
                        await new Promise(resolve => {
490
                            const stop = this.$watch(
510
                            const stop = this.$watch("metaInitialized", val => {
491
                                "metaInitialized",
511
                                if (val) {
492
                                val => {
512
                                    stop();
493
                                    if (val) {
513
                                    resolve();
494
                                        stop();
495
                                        resolve();
496
                                    }
497
                                }
514
                                }
498
                            );
515
                            });
499
                        });
516
                        });
500
                    }
517
                    }
501
                    await this.$nextTick();
518
                    await this.$nextTick();
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/Main.vue (-2 / +2 lines)
Lines 29-35 import { inject } from "vue"; Link Here
29
export default {
29
export default {
30
    setup() {
30
    setup() {
31
        const circRulesStore = inject("circRulesStore");
31
        const circRulesStore = inject("circRulesStore");
32
        circRulesStore.init(default_view);
32
        circRulesStore.init(default_view, logged_in_library_id);
33
        letters.unshift({
33
        letters.unshift({
34
            name: "No letter",
34
            name: "No letter",
35
            code: "",
35
            code: "",
Lines 52-58 export default { Link Here
52
52
53
<style>
53
<style>
54
.page-section.bg-info {
54
.page-section.bg-info {
55
    background-color: #DAECFB !important;
55
    background-color: #daecfb !important;
56
}
56
}
57
57
58
form .v-select {
58
form .v-select {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js (-3 / +19 lines)
Lines 3-9 import { $__ } from "../i18n"; Link Here
3
import { APIClient } from "../fetch/api-client.js";
3
import { APIClient } from "../fetch/api-client.js";
4
import { isEqual } from "lodash";
4
import { isEqual } from "lodash";
5
import { permissionsActions } from "../composables/permissions";
5
import { permissionsActions } from "../composables/permissions";
6
import { reactive, toRefs } from "vue";
6
import { reactive, toRefs, computed } from "vue";
7
7
8
// NOTES ON RULE SETS TYPES
8
// NOTES ON RULE SETS TYPES
9
// exhaustive:  includes 'pure fallback' rules sets for contexts that no rules match.
9
// exhaustive:  includes 'pure fallback' rules sets for contexts that no rules match.
Lines 26-31 export const useCircRulesStore = defineStore("circRules", () => { Link Here
26
        libraries: [],
26
        libraries: [],
27
        patronCategories: [],
27
        patronCategories: [],
28
        userPermissions: null,
28
        userPermissions: null,
29
        logged_in_library_id: null,
29
        letters: [],
30
        letters: [],
30
        ruleSuffixes: ["delay", "notice", "mtt", "restrict", "has_rules"],
31
        ruleSuffixes: ["delay", "notice", "mtt", "restrict", "has_rules"],
31
        transportTypes: [
32
        transportTypes: [
Lines 43-55 export const useCircRulesStore = defineStore("circRules", () => { Link Here
43
        storeInitialized: false,
44
        storeInitialized: false,
44
    });
45
    });
45
46
47
    const canManageAnyLibrary = computed(
48
        () =>
49
            !store.userPermissions ||
50
            !!store.userPermissions
51
                .CAN_user_parameters_manage_circ_rules_from_any_libraries
52
    );
53
46
    const actions = {
54
    const actions = {
47
        // controllers
55
        // controllers
48
        async init(defaultLibraryId = "*") {
56
        async init(defaultLibraryId = "*", loggedInLibraryId = null) {
57
            store.logged_in_library_id = loggedInLibraryId;
58
            await this.loadUserPermissions();
49
            await this.getItemTypes();
59
            await this.getItemTypes();
50
            await this.getLibraries();
60
            await this.getLibraries();
51
            await this.getPatronCategories();
61
            await this.getPatronCategories();
52
            this.currentLibraryId = defaultLibraryId;
62
            // If user can only manage their own library, override the default view
63
            if (!canManageAnyLibrary.value && store.logged_in_library_id) {
64
                this.currentLibraryId = store.logged_in_library_id;
65
            } else {
66
                this.currentLibraryId = defaultLibraryId;
67
            }
53
            this.metaInitialized = true;
68
            this.metaInitialized = true;
54
        },
69
        },
55
        async loadUserPermissions() {
70
        async loadUserPermissions() {
Lines 515-519 export const useCircRulesStore = defineStore("circRules", () => { Link Here
515
    return {
530
    return {
516
        ...toRefs(store),
531
        ...toRefs(store),
517
        ...actions,
532
        ...actions,
533
        canManageAnyLibrary,
518
    };
534
    };
519
});
535
});
(-)a/t/db_dependent/api/v1/circulation_rules.t (-2 / +58 lines)
Lines 299-305 subtest 'list_rules() tests' => sub { Link Here
299
};
299
};
300
300
301
subtest 'set_rules() tests' => sub {
301
subtest 'set_rules() tests' => sub {
302
    plan tests => 28;
302
    plan tests => 34;
303
303
304
    $schema->storage->txn_begin;
304
    $schema->storage->txn_begin;
305
305
Lines 318-323 subtest 'set_rules() tests' => sub { Link Here
318
    $librarian->set_password( { password => $password, skip_validation => 1 } );
318
    $librarian->set_password( { password => $password, skip_validation => 1 } );
319
    my $userid = $librarian->userid;
319
    my $userid = $librarian->userid;
320
320
321
    # Grant manage_circ_rules_from_any_libraries so the librarian can set rules for any library
322
    $builder->build(
323
        {
324
            source => 'UserPermission',
325
            value  => {
326
                borrowernumber => $librarian->borrowernumber,
327
                module_bit     => 3,
328
                code           => 'manage_circ_rules_from_any_libraries',
329
            }
330
        }
331
    );
332
321
    my $patron = $builder->build_object(
333
    my $patron = $builder->build_object(
322
        {
334
        {
323
            class => 'Koha::Patrons',
335
            class => 'Koha::Patrons',
Lines 328-333 subtest 'set_rules() tests' => sub { Link Here
328
    $patron->set_password( { password => $password, skip_validation => 1 } );
340
    $patron->set_password( { password => $password, skip_validation => 1 } );
329
    my $unauth_userid = $patron->userid;
341
    my $unauth_userid = $patron->userid;
330
342
343
    # Restricted librarian: has manage_circ_rules but NOT manage_circ_rules_from_any_libraries
344
    my $own_branch        = $builder->build( { source => 'Branch' } )->{'branchcode'};
345
    my $other_branch      = $builder->build( { source => 'Branch' } )->{'branchcode'};
346
    my $restricted_patron = $builder->build_object(
347
        {
348
            class => 'Koha::Patrons',
349
            value => { flags => 0, branchcode => $own_branch }
350
        }
351
    );
352
    $restricted_patron->set_password( { password => $password, skip_validation => 1 } );
353
    my $restricted_userid = $restricted_patron->userid;
354
    $builder->build(
355
        {
356
            source => 'UserPermission',
357
            value  => {
358
                borrowernumber => $restricted_patron->borrowernumber,
359
                module_bit     => 3,
360
                code           => 'manage_circ_rules',
361
            }
362
        }
363
    );
364
331
    ## Authorized user tests
365
    ## Authorized user tests
332
    note("Authorized user setting rules");
366
    note("Authorized user setting rules");
333
367
Lines 416-420 subtest 'set_rules() tests' => sub { Link Here
416
        { categorycode => undef, branchcode => undef, itemtype => undef, rule_name => 'finedays' } );
450
        { categorycode => undef, branchcode => undef, itemtype => undef, rule_name => 'finedays' } );
417
    is( $rules->count, 0, "Finedays rule deleted from database" );
451
    is( $rules->count, 0, "Finedays rule deleted from database" );
418
452
453
    # manage_circ_rules_from_any_libraries restriction tests
454
    note("Testing manage_circ_rules_from_any_libraries restrictions");
455
456
    my $restricted_rules = {
457
        context => {
458
            library_id         => '*',
459
            patron_category_id => '*',
460
            item_type_id       => '*',
461
        },
462
        fine => 3,
463
    };
464
465
    $t->put_ok( "//$restricted_userid:$password@/api/v1/circulation_rules" => json => $restricted_rules )
466
        ->status_is( 403, "Restricted user cannot set rules for default (*) library" );
467
468
    $restricted_rules->{context}->{library_id} = $other_branch;
469
    $t->put_ok( "//$restricted_userid:$password@/api/v1/circulation_rules" => json => $restricted_rules )
470
        ->status_is( 403, "Restricted user cannot set rules for another library" );
471
472
    $restricted_rules->{context}->{library_id} = $own_branch;
473
    $t->put_ok( "//$restricted_userid:$password@/api/v1/circulation_rules" => json => $restricted_rules )
474
        ->status_is( 200, "Restricted user can set rules for their own library" );
475
419
    $schema->storage->txn_rollback;
476
    $schema->storage->txn_rollback;
420
};
477
};
421
- 

Return to bug 10190