Radicale 3.8.0__tar.gz → 3.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {radicale-3.8.0 → radicale-3.8.2}/CHANGELOG.md +32 -0
- {radicale-3.8.0 → radicale-3.8.2}/DOCUMENTATION.md +36 -4
- {radicale-3.8.0 → radicale-3.8.2}/PKG-INFO +5 -1
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/PKG-INFO +5 -1
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/SOURCES.txt +0 -1
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/requires.txt +7 -0
- {radicale-3.8.0 → radicale-3.8.2}/config +8 -2
- {radicale-3.8.0 → radicale-3.8.2}/pyproject.toml +37 -5
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/__init__.py +16 -8
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/base.py +23 -6
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/delete.py +11 -2
- radicale-3.8.2/radicale/app/get.py +343 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/mkcalendar.py +1 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/mkcol.py +1 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/move.py +13 -22
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/propfind.py +120 -26
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/proppatch.py +7 -4
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/put.py +9 -6
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/report.py +205 -65
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/htpasswd.py +9 -3
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/imap.py +4 -2
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/ldap.py +15 -15
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/oauth2.py +5 -3
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/pam.py +16 -10
- {radicale-3.8.0 → radicale-3.8.2}/radicale/config.py +8 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/__init__.py +3 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/httputils.py +2 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/item/__init__.py +43 -9
- {radicale-3.8.0 → radicale-3.8.2}/radicale/item/filter.py +9 -3
- {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/__init__.py +3 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/from_file.py +19 -2
- {radicale-3.8.0 → radicale-3.8.2}/radicale/server.py +13 -5
- {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/__init__.py +19 -8
- {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/common.py +6 -4
- {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/files.py +32 -31
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/__init__.py +13 -5
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/cache.py +4 -3
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/create_collection.py +1 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/discover.py +15 -14
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/get.py +10 -9
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/history.py +3 -2
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/lock.py +31 -16
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/move.py +2 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/upload.py +1 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/utils.py +22 -2
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/main.css +21 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/index.html +11 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/models/collection.js +4 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CollectionsScene.js +92 -33
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +2 -2
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +1 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +4 -4
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/misc.js +37 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/permissions.js +40 -16
- {radicale-3.8.0 → radicale-3.8.2}/rights +7 -0
- radicale-3.8.2/setup.cfg +4 -0
- radicale-3.8.0/radicale/app/get.py +0 -151
- radicale-3.8.0/setup.cfg +0 -27
- {radicale-3.8.0 → radicale-3.8.2}/COPYING.md +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/MANIFEST.in +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/README.md +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/dependency_links.txt +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/entry_points.txt +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/top_level.txt +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/__main__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/head.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/options.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/app/post.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/denyall.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/dovecot.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/http_remote_user.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/http_x_remote_user.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/none.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/remote_user.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/group/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/group/from_auth.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/group/htgroup.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/group/none.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/email/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/none.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/rabbitmq/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/log.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/pathutils.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/py.typed +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/authenticated.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/owner_only.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/owner_write.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/csv.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/none.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/base.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/delete.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/meta.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/sync.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/verify.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem_nolock.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/auth.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/rights.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/storage_simple_sync.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/web.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/types.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/__init__.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icon.png +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/delete.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/download.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/edit.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/eye.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/key.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/new.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/repeat.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/share.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/upload.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/loading.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/logo.svg +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/api.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/common.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/sharing.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/config.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/constants.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/main.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +1 -1
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoadingScene.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoginScene.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/scene_manager.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collection_utils.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collections_cache.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/error.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/form_validator.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/url_text.js +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/web/none.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale/xmlutils.py +0 -0
- {radicale-3.8.0 → radicale-3.8.2}/radicale.wsgi +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.8.2
|
|
4
|
+
* Add: tests: sharing API plus CalDAV/CardDAV workflow and corrupt CSV handling (covers #2259)
|
|
5
|
+
* Add: tests: mocked LDAP, IMAP, OAuth2 and PAM authentication servers (covers #2259)
|
|
6
|
+
* Improve: storage hook: avoid using preexc_fn, use process_group
|
|
7
|
+
* Add: sharing/token: plain GET and HEAD serve free/busy when Actions config `view` is `freebusy`
|
|
8
|
+
* Add: WebUI read-only free/busy calendar URL under each calendar
|
|
9
|
+
* Add: read-only free/busy view via GET ?view=freebusy
|
|
10
|
+
* Fix: free-busy REPORT returns one VFREEBUSY with FREEBUSY periods and no event details (RFC 4791)
|
|
11
|
+
* Add: rights permission `f` to read free-busy time without event details
|
|
12
|
+
* Fix: free-busy REPORT does not expand transparent recurrences
|
|
13
|
+
* Fix: sharing/bday-conversion: add proper RRULE and RECURRENCE-ID in case of "age" support is triggered
|
|
14
|
+
* Fix: sharing/bday-conversion: do not serve item.vcf in parallel of item.ics
|
|
15
|
+
* Fix: sharing/bday-conversion: adjust etag depending on template (item) or content (collection)
|
|
16
|
+
* Fix: sharing/propfind: remove unexpected additional shares listed in case of request on collection with HTTP_DEPTH=1
|
|
17
|
+
* Add: sharing/bday-conversion: X-RADICALE-NAME to generated bday items
|
|
18
|
+
* Fix: sharing/propfind: suppress (resolved) shares having same URI as native ones
|
|
19
|
+
* Fix: sharing/mkcol+mkcalendar: detect conflict with existing resolved shares
|
|
20
|
+
* Fix: WebUI/sharing: display permissions and share-by-group
|
|
21
|
+
* Performance: guard expensive logging with isEnabledFor (where missed)
|
|
22
|
+
* Performance: use lazy formatting for logger calls (where missed)
|
|
23
|
+
* Fix: delete/sharing: neither allow resolved share nor still mapped collections to be deleted
|
|
24
|
+
* Fix: sharing: resolved share resulting in same path as original requested will not have precedence
|
|
25
|
+
|
|
26
|
+
## 3.8.1
|
|
27
|
+
* Add: tests: optional caldav-server-tester based CalDAV compatibility test suite (caldav_compat_tests)
|
|
28
|
+
* Fix: sharing/bday-conversion: add missing support for comma separated text components in vcard field N (name)
|
|
29
|
+
* Improvement: rights/from_file: extend detection of conflicting/unsupported permissions on startup
|
|
30
|
+
* Fix: sharing: fix logic of permit/deny permissions for map (mM) and token (tT)
|
|
31
|
+
* Add: support for /.mobileconfig for authenticated user
|
|
32
|
+
* Fix: WebUI sharing/propatch issue
|
|
33
|
+
* Fix: storage/sharing close unguarded os.scandir() iterators
|
|
34
|
+
|
|
3
35
|
## 3.8.0
|
|
4
36
|
* Fix: storage/multifilesystem: depth:1 PROPFIND no longer re-runs the filesystem collision check (path_to_filesystem) for every item in a collection; this made listing large collections O(n^2) on file systems not detected as collision-free
|
|
5
37
|
* Improve: storage/multifilesystem: avoid redundant stat() calls per item in get/upload when use_mtime_and_size_for_item_cache is enabled
|
|
@@ -2269,6 +2269,35 @@ is thrown instead of returning the results.
|
|
|
2269
2269
|
|
|
2270
2270
|
Default: 10000
|
|
2271
2271
|
|
|
2272
|
+
##### freebusy_view_past_days
|
|
2273
|
+
|
|
2274
|
+
_(>= 3.8.2)_
|
|
2275
|
+
|
|
2276
|
+
Number of days before today included in the default window of the
|
|
2277
|
+
read-only free/busy view (`GET` or `HEAD` with `?view=freebusy`). The
|
|
2278
|
+
window starts at midnight UTC. `0` starts the window at midnight UTC
|
|
2279
|
+
today. A request that provides both `start` and `end`
|
|
2280
|
+
(`YYYYMMDDTHHMMSSZ`) overrides this default and
|
|
2281
|
+
`freebusy_view_future_days`.
|
|
2282
|
+
|
|
2283
|
+
Default: 365
|
|
2284
|
+
|
|
2285
|
+
##### freebusy_view_future_days
|
|
2286
|
+
|
|
2287
|
+
_(>= 3.8.2)_
|
|
2288
|
+
|
|
2289
|
+
Number of days after today included in the default window of the
|
|
2290
|
+
read-only free/busy view. The window ends at midnight UTC that many
|
|
2291
|
+
days ahead. `0` ends the window at midnight UTC today.
|
|
2292
|
+
|
|
2293
|
+
Default: 365
|
|
2294
|
+
|
|
2295
|
+
A share-by-token can omit `?view=freebusy`. Set that token's Actions
|
|
2296
|
+
config `view` to `freebusy`. Plain `GET` and `HEAD` of the token then
|
|
2297
|
+
use this view, including an optional `start` and `end`. Any other
|
|
2298
|
+
`view` value is rejected. Other methods keep the token permissions.
|
|
2299
|
+
See [Collection Sharing](https://github.com/Kozea/Radicale/blob/master/SHARING.md).
|
|
2300
|
+
|
|
2272
2301
|
#### [sharing]
|
|
2273
2302
|
|
|
2274
2303
|
_(>= 3.7.0)_
|
|
@@ -2370,7 +2399,7 @@ Default permissions for create token-based sharing
|
|
|
2370
2399
|
|
|
2371
2400
|
Default: `r`
|
|
2372
2401
|
|
|
2373
|
-
Supported: `
|
|
2402
|
+
Supported: `rfwEePp`
|
|
2374
2403
|
|
|
2375
2404
|
##### default_permissions_create_map
|
|
2376
2405
|
|
|
@@ -2380,7 +2409,7 @@ Default permissions for map-based sharing
|
|
|
2380
2409
|
|
|
2381
2410
|
Default: `r`
|
|
2382
2411
|
|
|
2383
|
-
Supported: `
|
|
2412
|
+
Supported: `rfwEePp`
|
|
2384
2413
|
|
|
2385
2414
|
##### conversion_bday_summary_template
|
|
2386
2415
|
|
|
@@ -2388,7 +2417,7 @@ _(>= 3.7.5)_
|
|
|
2388
2417
|
|
|
2389
2418
|
Global template for summary of conversion "bday"
|
|
2390
2419
|
|
|
2391
|
-
Default: `{
|
|
2420
|
+
Default: `[{n:f} {n:g}|{fn}|{nickname}] ({year}) (BDAY)`
|
|
2392
2421
|
|
|
2393
2422
|
Supported placeholders (data used from VCARD)
|
|
2394
2423
|
* `{year}`: year of birthday (RFC6350#6.2.5)
|
|
@@ -2450,7 +2479,8 @@ Default: `99`
|
|
|
2450
2479
|
Radicale has been tested with:
|
|
2451
2480
|
|
|
2452
2481
|
* [Android](https://android.com/) with
|
|
2453
|
-
[DAVx⁵](https://www.davx5.com/) (formerly DAVdroid)
|
|
2482
|
+
[DAVx⁵](https://www.davx5.com/) (formerly DAVdroid) or
|
|
2483
|
+
[KashCal](https://f-droid.org/packages/org.onekash.kashcal),
|
|
2454
2484
|
* [OneCalendar](https://www.onecalendar.nl/)
|
|
2455
2485
|
* [GNOME Calendar](https://wiki.gnome.org/Apps/Calendar),
|
|
2456
2486
|
[Contacts](https://wiki.gnome.org/Apps/Contacts) and
|
|
@@ -2676,6 +2706,8 @@ The following `permissions` are recognized:
|
|
|
2676
2706
|
* **r:** read address book and calendar collections
|
|
2677
2707
|
* **i:** subset of **r** that only allows direct access via HTTP method GET
|
|
2678
2708
|
(CalDAV/CardDAV is susceptible to expensive search requests)
|
|
2709
|
+
* **f:** read the free-busy time of a calendar, without event details
|
|
2710
|
+
(**r** includes this)
|
|
2679
2711
|
* **W:** write collections (excluding address books and calendars)
|
|
2680
2712
|
* **w:** write address book and calendar collections
|
|
2681
2713
|
* **D:** allow deleting a collection in case `permit_delete_collection=False` _(>= 3.3.0)_
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Radicale
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.2
|
|
4
4
|
Summary: CalDAV and CardDAV Server
|
|
5
5
|
Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
|
|
6
6
|
License: GNU GPL v3
|
|
@@ -54,6 +54,10 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
54
54
|
Requires-Dist: pytest; extra == "dev"
|
|
55
55
|
Requires-Dist: pytest-playwright; extra == "dev"
|
|
56
56
|
Requires-Dist: html5validator; extra == "dev"
|
|
57
|
+
Provides-Extra: caldav-test
|
|
58
|
+
Requires-Dist: pytest>=7; extra == "caldav-test"
|
|
59
|
+
Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
|
|
60
|
+
Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
|
|
57
61
|
Dynamic: license-file
|
|
58
62
|
|
|
59
63
|
# Radicale
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Radicale
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.2
|
|
4
4
|
Summary: CalDAV and CardDAV Server
|
|
5
5
|
Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
|
|
6
6
|
License: GNU GPL v3
|
|
@@ -54,6 +54,10 @@ Requires-Dist: mypy; extra == "dev"
|
|
|
54
54
|
Requires-Dist: pytest; extra == "dev"
|
|
55
55
|
Requires-Dist: pytest-playwright; extra == "dev"
|
|
56
56
|
Requires-Dist: html5validator; extra == "dev"
|
|
57
|
+
Provides-Extra: caldav-test
|
|
58
|
+
Requires-Dist: pytest>=7; extra == "caldav-test"
|
|
59
|
+
Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
|
|
60
|
+
Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
|
|
57
61
|
Dynamic: license-file
|
|
58
62
|
|
|
59
63
|
# Radicale
|
|
@@ -372,11 +372,11 @@ type = none
|
|
|
372
372
|
#enforce_properties_overlay = true
|
|
373
373
|
|
|
374
374
|
# Default permissions for token-based sharing
|
|
375
|
-
# Supported:
|
|
375
|
+
# Supported: rfwEePp
|
|
376
376
|
#default_permissions_create_token = rp
|
|
377
377
|
|
|
378
378
|
# Default permissions for map-based sharing
|
|
379
|
-
# Supported:
|
|
379
|
+
# Supported: rfwEePp
|
|
380
380
|
#default_permissions_create_map = r
|
|
381
381
|
|
|
382
382
|
# Global template for summary of conversion "bday"
|
|
@@ -526,3 +526,9 @@ Content-Security-Policy = default-src 'self'; object-src 'none'
|
|
|
526
526
|
# When returning a free-busy report, limit the number of returned
|
|
527
527
|
# occurences per event to prevent DoS attacks.
|
|
528
528
|
#max_freebusy_occurrence = 10000
|
|
529
|
+
|
|
530
|
+
# Days before and after today in the default free-busy view
|
|
531
|
+
# (?view=freebusy). The window is aligned to midnight UTC.
|
|
532
|
+
# A request can override both with start and end.
|
|
533
|
+
#freebusy_view_past_days = 365
|
|
534
|
+
#freebusy_view_future_days = 365
|
|
@@ -3,7 +3,7 @@ name = "Radicale"
|
|
|
3
3
|
# When the version is updated, a new section in the CHANGELOG.md file must be
|
|
4
4
|
# added too.
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
version = "3.8.
|
|
6
|
+
version = "3.8.2"
|
|
7
7
|
authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}]
|
|
8
8
|
license = {text = "GNU GPL v3"}
|
|
9
9
|
description = "CalDAV and CardDAV Server"
|
|
@@ -46,6 +46,7 @@ argon2 = ["argon2-cffi"]
|
|
|
46
46
|
ldap = ["ldap3"]
|
|
47
47
|
pam = ["pam"]
|
|
48
48
|
dev = ["flake8", "isort", "mypy", "pytest", "pytest-playwright", "html5validator"]
|
|
49
|
+
caldav_test = ["pytest>=7", "caldav>=3.0; python_version >= '3.10'", "caldav-server-tester>=1.2,<2; python_version >= '3.10'"]
|
|
49
50
|
|
|
50
51
|
[project.scripts]
|
|
51
52
|
radicale = "radicale.__main__:run"
|
|
@@ -56,9 +57,10 @@ build-backend = "setuptools.build_meta"
|
|
|
56
57
|
|
|
57
58
|
[tool.tox]
|
|
58
59
|
min_version = "4.0"
|
|
59
|
-
envlist = ["flake8", "isort", "mypy", "py_loglevel_info", "py_radicale_loglevel_info", "py_radicale_loglevel_trace", "py", "integ_test", "html5validator"]
|
|
60
|
+
envlist = ["flake8", "isort", "mypy", "py_loglevel_info", "py_radicale_loglevel_info", "py_radicale_loglevel_trace", "py", "integ_test", "caldav_compat_test", "html5validator"]
|
|
60
61
|
|
|
61
62
|
[tool.tox.env.py]
|
|
63
|
+
description = "Server: run 'pytest' (incl. coverage report)"
|
|
62
64
|
extras = ["test"]
|
|
63
65
|
deps = [
|
|
64
66
|
"pytest",
|
|
@@ -67,6 +69,7 @@ deps = [
|
|
|
67
69
|
commands = [["pytest", "-r", "s", "--cov", "--cov-report=term", "--cov-report=xml", "."]]
|
|
68
70
|
|
|
69
71
|
[tool.tox.env.py_loglevel_info]
|
|
72
|
+
description = "Server: run 'pytest' with log level 'INFO'"
|
|
70
73
|
extras = ["test"]
|
|
71
74
|
deps = [
|
|
72
75
|
"pytest"
|
|
@@ -74,6 +77,7 @@ deps = [
|
|
|
74
77
|
commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
|
|
75
78
|
|
|
76
79
|
[tool.tox.env.py_radicale_loglevel_info]
|
|
80
|
+
description = "Server: run 'pytest' with radicale on log level 'INFO'"
|
|
77
81
|
extras = ["test"]
|
|
78
82
|
setenv = { PYTEST_RADICALE_LOGLEVEL = "info" }
|
|
79
83
|
deps = [
|
|
@@ -82,6 +86,7 @@ deps = [
|
|
|
82
86
|
commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
|
|
83
87
|
|
|
84
88
|
[tool.tox.env.py_radicale_loglevel_trace]
|
|
89
|
+
description = "Server: run 'pytest' with radicale on log level 'TRACE'"
|
|
85
90
|
extras = ["test"]
|
|
86
91
|
setenv = { PYTEST_RADICALE_LOGLEVEL = "trace" }
|
|
87
92
|
deps = [
|
|
@@ -90,6 +95,7 @@ deps = [
|
|
|
90
95
|
commands = [["pytest", "-r", "s", "--log-level", "5", "."]]
|
|
91
96
|
|
|
92
97
|
[tool.tox.env.py_filesystem_vfat]
|
|
98
|
+
description = "Server: run 'pytest' with collection storage filesystem 'vfat' (on Linux)"
|
|
93
99
|
allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
|
|
94
100
|
extras = ["test"]
|
|
95
101
|
deps = [
|
|
@@ -117,6 +123,7 @@ commands_post = [
|
|
|
117
123
|
]
|
|
118
124
|
|
|
119
125
|
[tool.tox.env.py_filesystem_vfat_utf8]
|
|
126
|
+
description = "Server: run 'pytest' with collection storage filesystem 'vfat+utf8' (on Linux)"
|
|
120
127
|
allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
|
|
121
128
|
extras = ["test"]
|
|
122
129
|
deps = [
|
|
@@ -144,6 +151,7 @@ commands_post = [
|
|
|
144
151
|
]
|
|
145
152
|
|
|
146
153
|
[tool.tox.env.py_filesystem_hfsplus]
|
|
154
|
+
description = "Server: run 'pytest' with collection storage filesystem 'hfsplus' (on Linux)"
|
|
147
155
|
# Fedora
|
|
148
156
|
allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ]
|
|
149
157
|
extras = ["test"]
|
|
@@ -172,6 +180,7 @@ commands_post = [
|
|
|
172
180
|
]
|
|
173
181
|
|
|
174
182
|
[tool.tox.env.py_filesystem_ntfs]
|
|
183
|
+
description = "Server: run 'pytest' with collection storage filesystem 'ntfs' (on Linux)"
|
|
175
184
|
allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ]
|
|
176
185
|
extras = ["test"]
|
|
177
186
|
deps = [
|
|
@@ -199,34 +208,57 @@ commands_post = [
|
|
|
199
208
|
]
|
|
200
209
|
|
|
201
210
|
[tool.tox.env.flake8]
|
|
211
|
+
description = "Server: test code style"
|
|
202
212
|
deps = ["flake8==7.1.0"]
|
|
203
213
|
commands = [["flake8", "."]]
|
|
204
214
|
skip_install = true
|
|
205
215
|
|
|
206
216
|
[tool.tox.env.isort]
|
|
217
|
+
description = "Server: test import sort order"
|
|
207
218
|
deps = ["isort==5.13.2"]
|
|
208
219
|
commands = [["isort", "--check", "--diff", "."]]
|
|
209
220
|
skip_install = true
|
|
210
221
|
|
|
211
222
|
[tool.tox.env.mypy]
|
|
223
|
+
description = "Server: test static types"
|
|
212
224
|
deps = ["mypy==1.11.0"]
|
|
213
225
|
commands = [["mypy", "--install-types", "--non-interactive", "."]]
|
|
214
226
|
skip_install = true
|
|
215
227
|
|
|
216
228
|
[tool.tox.env.integ_test]
|
|
229
|
+
description = "Server+WebUI: run 'playwright' integration tests (with 'chromium')"
|
|
217
230
|
deps = ["pytest", "pytest-playwright"]
|
|
218
231
|
# Store Playwright browsers inside .tox for better caching
|
|
219
232
|
setenv = { PLAYWRIGHT_BROWSERS_PATH = "{toxworkdir}/.playwright" }
|
|
220
233
|
commands_pre = [["playwright", "install", "chromium"]]
|
|
221
234
|
commands = [["pytest", "integ_tests"]]
|
|
222
235
|
|
|
236
|
+
[tool.tox.env.caldav_compat_test]
|
|
237
|
+
description = "Server: run caldav compatibility tests"
|
|
238
|
+
extras = ["caldav_test"]
|
|
239
|
+
deps = [
|
|
240
|
+
"caldav",
|
|
241
|
+
"caldav_server_tester==1.2.0", # synchronize version with .github/workflows/test.yml#caldav-compat-test
|
|
242
|
+
"pytest"
|
|
243
|
+
]
|
|
244
|
+
commands = [["pytest", "caldav_compat_tests"]]
|
|
245
|
+
|
|
246
|
+
[tool.tox.env.js]
|
|
247
|
+
# for optional local test as of now, copied from .github/workflows/test.yml
|
|
248
|
+
# Fedora (will install nodejs): yum install npx
|
|
249
|
+
description = "WebUI: validate JavaScript"
|
|
250
|
+
allowlist_externals = [ "npx" ]
|
|
251
|
+
commands = [["npx", "-y", "-p", "typescript@7.0.2", "tsc", "-p", "radicale/web/jsconfig.json"]]
|
|
252
|
+
skip_install = true
|
|
253
|
+
|
|
223
254
|
[tool.tox.env.html5validator]
|
|
255
|
+
description = "WebUI: validate HTML"
|
|
224
256
|
deps = ["html5validator-2"]
|
|
225
257
|
commands = [["html5validator", "--root", "radicale/web/internal_data/", "--also-check-css"]]
|
|
226
258
|
|
|
227
259
|
|
|
228
260
|
[tool.pytest.ini_options]
|
|
229
|
-
norecursedirs = ["integ_tests"]
|
|
261
|
+
norecursedirs = ["integ_tests", "caldav_compat_tests"]
|
|
230
262
|
|
|
231
263
|
|
|
232
264
|
[tool.setuptools]
|
|
@@ -234,7 +266,7 @@ platforms = ["Any"]
|
|
|
234
266
|
include-package-data = false
|
|
235
267
|
|
|
236
268
|
[tool.setuptools.packages.find]
|
|
237
|
-
exclude = ["*.tests", "integ_tests"] # *.tests.*; tests.*; tests
|
|
269
|
+
exclude = ["*.tests", "integ_tests", "caldav_compat_tests"] # *.tests.*; tests.*; tests
|
|
238
270
|
namespaces = false
|
|
239
271
|
|
|
240
272
|
[tool.setuptools.package-data]
|
|
@@ -266,7 +298,7 @@ exclude = "(^|/)build($|/)"
|
|
|
266
298
|
[tool.coverage.run]
|
|
267
299
|
branch = true
|
|
268
300
|
source = ["radicale"]
|
|
269
|
-
omit = ["tests/*", "*/tests/*", "integ_tests/*", "*/integ_tests/*"]
|
|
301
|
+
omit = ["tests/*", "*/tests/*", "integ_tests/*", "*/integ_tests/*", "caldav_compat_tests/*", "*/caldav_compat_tests/*"]
|
|
270
302
|
|
|
271
303
|
[tool.coverage.report]
|
|
272
304
|
# Regexes for lines to exclude from consideration
|
|
@@ -45,7 +45,7 @@ from radicale import config, httputils, log, pathutils, types, utils
|
|
|
45
45
|
from radicale.app import base as app_base
|
|
46
46
|
from radicale.app.base import ApplicationBase
|
|
47
47
|
from radicale.app.delete import ApplicationPartDelete
|
|
48
|
-
from radicale.app.get import ApplicationPartGet
|
|
48
|
+
from radicale.app.get import ApplicationPartGet, requests_freebusy_view
|
|
49
49
|
from radicale.app.head import ApplicationPartHead
|
|
50
50
|
from radicale.app.mkcalendar import ApplicationPartMkcalendar
|
|
51
51
|
from radicale.app.mkcol import ApplicationPartMkcol
|
|
@@ -305,7 +305,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|
|
305
305
|
condition=self._response_content_on_notice_condition,
|
|
306
306
|
value=request_info,
|
|
307
307
|
):
|
|
308
|
-
logger.
|
|
308
|
+
if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
|
|
309
|
+
logger.notice("Response content (nonXML, log condition passed):\n%s", utils.textwrap_str(answer, self._limit_content))
|
|
309
310
|
else:
|
|
310
311
|
if logger.isEnabledFor(logging.DEBUG):
|
|
311
312
|
logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
|
|
@@ -340,7 +341,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|
|
340
341
|
condition=self._response_header_on_notice_condition,
|
|
341
342
|
value=request_info,
|
|
342
343
|
):
|
|
343
|
-
logger.
|
|
344
|
+
if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
|
|
345
|
+
logger.notice("Response header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(headers), self._limit_content))
|
|
344
346
|
else:
|
|
345
347
|
if logger.isEnabledFor(logging.DEBUG):
|
|
346
348
|
logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
|
|
@@ -474,8 +476,9 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|
|
474
476
|
request_method, unsafe_path, depthinfo,
|
|
475
477
|
remote_host, remote_useragent_txt, https_info)
|
|
476
478
|
if self._request_header_on_debug:
|
|
477
|
-
logger.
|
|
478
|
-
|
|
479
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
480
|
+
logger.debug("Request header:\n%s",
|
|
481
|
+
utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
|
|
479
482
|
else:
|
|
480
483
|
if not self._request_header_on_notice_condition != {}:
|
|
481
484
|
# conditional request header logging is later
|
|
@@ -570,7 +573,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|
|
570
573
|
condition=self._request_header_on_notice_condition,
|
|
571
574
|
value=request_info,
|
|
572
575
|
):
|
|
573
|
-
logger.
|
|
576
|
+
if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
|
|
577
|
+
logger.notice("Request header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
|
|
574
578
|
|
|
575
579
|
if user and login == user:
|
|
576
580
|
logger.info("Successful login: %r (%s)", user, info)
|
|
@@ -668,8 +672,12 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|
|
668
672
|
profiler_active = True
|
|
669
673
|
|
|
670
674
|
try:
|
|
671
|
-
|
|
672
|
-
|
|
675
|
+
if (requests_freebusy_view(environ) and
|
|
676
|
+
request_method not in ("GET", "HEAD")):
|
|
677
|
+
status, headers, answer, xml_request = httputils.METHOD_NOT_ALLOWED
|
|
678
|
+
else:
|
|
679
|
+
status, headers, answer, xml_request = function(
|
|
680
|
+
environ, base_prefix, path, user, request_info)
|
|
673
681
|
except PermissionError as e:
|
|
674
682
|
logger.error("PermissionError: %s", e)
|
|
675
683
|
status, headers, answer, xml_request = httputils.INTERNAL_SERVER_ERROR
|
|
@@ -215,7 +215,8 @@ class ApplicationBase:
|
|
|
215
215
|
condition=self._request_content_on_notice_condition,
|
|
216
216
|
value=request_info,
|
|
217
217
|
):
|
|
218
|
-
logger.
|
|
218
|
+
if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
|
|
219
|
+
logger.notice("Request content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
|
|
219
220
|
else:
|
|
220
221
|
if logger.isEnabledFor(logging.DEBUG):
|
|
221
222
|
logger.debug("Request content (XML, log condition skipped): suppressed")
|
|
@@ -236,7 +237,8 @@ class ApplicationBase:
|
|
|
236
237
|
condition=self._response_content_on_notice_condition,
|
|
237
238
|
value=request_info,
|
|
238
239
|
):
|
|
239
|
-
logger.
|
|
240
|
+
if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
|
|
241
|
+
logger.notice("Response content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
|
|
240
242
|
else:
|
|
241
243
|
if logger.isEnabledFor(logging.DEBUG):
|
|
242
244
|
logger.debug("Response content (XML, log condition skipped): suppressed")
|
|
@@ -255,6 +257,12 @@ class ApplicationBase:
|
|
|
255
257
|
return status, headers, content, None
|
|
256
258
|
|
|
257
259
|
|
|
260
|
+
def _with_implied_freebusy(permissions: str) -> str:
|
|
261
|
+
if "r" in permissions and "f" not in permissions:
|
|
262
|
+
return permissions + "f"
|
|
263
|
+
return permissions
|
|
264
|
+
|
|
265
|
+
|
|
258
266
|
class Access:
|
|
259
267
|
"""Helper class to check access rights of an item"""
|
|
260
268
|
|
|
@@ -275,8 +283,8 @@ class Access:
|
|
|
275
283
|
self.permissions = self._rights.authorization(self.user, self.path)
|
|
276
284
|
if permissions_filter is not None:
|
|
277
285
|
self._permissions_filter = permissions_filter
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
self.permissions = intersect(
|
|
287
|
+
_with_implied_freebusy(self.permissions), permissions_filter)
|
|
280
288
|
self._parent_permissions = None
|
|
281
289
|
|
|
282
290
|
@property
|
|
@@ -287,8 +295,9 @@ class Access:
|
|
|
287
295
|
self._parent_permissions = self._rights.authorization(
|
|
288
296
|
self.user, self.parent_path)
|
|
289
297
|
if self._permissions_filter is not None:
|
|
290
|
-
|
|
291
|
-
|
|
298
|
+
self._parent_permissions = intersect(
|
|
299
|
+
_with_implied_freebusy(self._parent_permissions),
|
|
300
|
+
self._permissions_filter)
|
|
292
301
|
return self._parent_permissions
|
|
293
302
|
|
|
294
303
|
def check(self, permission: str,
|
|
@@ -310,3 +319,11 @@ class Access:
|
|
|
310
319
|
return bool(rights.intersect(self.permissions, permissions) or (
|
|
311
320
|
self.path != self.parent_path and
|
|
312
321
|
rights.intersect(self.parent_permissions, parent_permissions)))
|
|
322
|
+
|
|
323
|
+
def allows_freebusy(self, item: Optional[types.CollectionOrItem] = None
|
|
324
|
+
) -> bool:
|
|
325
|
+
if item is not None and (
|
|
326
|
+
not isinstance(item, storage.BaseCollection) or
|
|
327
|
+
item.tag != "VCALENDAR"):
|
|
328
|
+
return False
|
|
329
|
+
return "r" in self.permissions or "f" in self.permissions
|
|
@@ -79,6 +79,9 @@ class ApplicationPartDelete(ApplicationBase):
|
|
|
79
79
|
user_lookup += sharing.SHARING_SEPARATOR_GROUP + ','.join(self._rights._user_groups)
|
|
80
80
|
share = self._sharing.sharing_collection_resolver(path, user_lookup)
|
|
81
81
|
if share:
|
|
82
|
+
if path.endswith('/'):
|
|
83
|
+
logger.notice("delete of a mapped collection is never permitted: %r -> %r", share['PathOrToken'], path)
|
|
84
|
+
return httputils.NOT_ALLOWED
|
|
82
85
|
# overwrite and run through extended permission check
|
|
83
86
|
path = share['PathMapped']
|
|
84
87
|
user = share['Owner']
|
|
@@ -86,6 +89,12 @@ class ApplicationPartDelete(ApplicationBase):
|
|
|
86
89
|
access = Access(self._rights, user, path, permissions_filter)
|
|
87
90
|
if not access.check("w"):
|
|
88
91
|
return httputils.NOT_ALLOWED
|
|
92
|
+
if self._sharing._enabled:
|
|
93
|
+
if not share:
|
|
94
|
+
collections_share_list = self._sharing.database_list_sharing(PathMapped=path)
|
|
95
|
+
if collections_share_list is not None and len(collections_share_list) > 0:
|
|
96
|
+
logger.notice("delete of a mapped collection in use (%d) is not permitted: %r", len(collections_share_list), path)
|
|
97
|
+
return httputils.CONFLICT
|
|
89
98
|
with self._storage.acquire_lock("w", user, path=path, request="DELETE"):
|
|
90
99
|
item = next(iter(self._storage.discover(path)), None)
|
|
91
100
|
if not item:
|
|
@@ -100,11 +109,11 @@ class ApplicationPartDelete(ApplicationBase):
|
|
|
100
109
|
if isinstance(item, storage.BaseCollection):
|
|
101
110
|
if self._permit_delete_collection:
|
|
102
111
|
if access.check("d", item):
|
|
103
|
-
logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %
|
|
112
|
+
logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %r", path)
|
|
104
113
|
return httputils.NOT_ALLOWED
|
|
105
114
|
else:
|
|
106
115
|
if not access.check("D", item):
|
|
107
|
-
logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %
|
|
116
|
+
logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %r", path)
|
|
108
117
|
return httputils.NOT_ALLOWED
|
|
109
118
|
if self._hook.enabled:
|
|
110
119
|
for i in item.get_all():
|