Skip to content

[ADD] base_dav: CalDAV and CardDAV support (port from 11.0) - #470

Draft
rossigee wants to merge 6 commits into
OCA:17.0from
rossigee:17.0
Draft

[ADD] base_dav: CalDAV and CardDAV support (port from 11.0)#470
rossigee wants to merge 6 commits into
OCA:17.0from
rossigee:17.0

Conversation

@rossigee

@rossigee rossigee commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Ports base_dav (CalDAV/CardDAV access to Odoo data via Radicale) from the 11.0 branch to 17.0, and brings it up to this repo's current packaging conventions.

  • Adapted to 17.0 (no @api.one/@api.multi, no attrs/states in views).
  • Added pyproject.toml, readme/ fragments (generated README.rst and static/description/index.html), development_status/website manifest keys, and a tests/ suite (previously absent).
  • Regenerated root requirements.txt and the addons table in README.md.

While reviewing the port I found and fixed a few real bugs, not just formatting:

  • radicale/collection.py: models/dav_collection.py imports a class named Item, but the module defined ItemWrapper — an ImportError on load. Renamed to Item.
  • radicale/auth.py, radicale/collection.py: the ImportError fallback for a missing radicale package set the base classes to None, but Auth/Collection/Storage subclass them directly, which raises TypeError at import time. Fall back to object instead so the addon can at least install without radicale present (matching the controller's existing "radicale not installed" runtime message).
  • radicale/auth.py: switched from writing the private request._env attribute to the public request.update_env(user=uid) API.
  • __manifest__.py: declared the dateutil external dependency that dav_collection_field_mapping.py already imports.
  • dav_get() set result.logger = self.logger for files-type collections, but dav.collection has no logger attribute — any DAV request against a "Files" collection raised AttributeError. Removed.
  • dav_delete() was a no-op for files-type collections, so DELETE requests against a file appeared to succeed without removing the underlying ir.attachment. Implemented deletion mirroring the lookup already used by dav_get().
  • from_vobject() only accepted VEVENT components even though get_meta() advertises VTODO/VJOURNAL support too; an incoming VTODO/VJOURNAL item returned None, which then crashed dav_upload() with an unhandled TypeError. Now accepts all three component types on import, and dav_upload() raises a clear ValueError instead of crashing when an item still can't be parsed.

Marked as draft because the new tests, while written against the actual code paths (including Auth._login), have not yet actually been executed — no Odoo 17 runtime or radicale/vobject packages were available in the environment where this was prepared, so this only got a static lint/pre-commit pass, not a real test run.

Known limitations

These were identified during review but intentionally not touched in this PR, because implementing them correctly requires verifying against Radicale's actual internal API contract (return types, expected exceptions, calling conventions), which isn't possible without a live radicale install to test against — guessing at the shape risks landing plausible-looking but silently-wrong code:

  • File upload (dav_upload for dav_type == "files") always returns None — no file upload support via WebDAV for "Files" collections, only listing/download/delete.
  • Storage.create_collection() and Storage.move() in radicale/collection.py are no-ops — a DAV client's native "create calendar" or "move/rename item" actions won't do anything; collections can only be created through the Odoo backend UI.
  • Collection.set_meta() is a no-op — a client renaming a calendar or changing its color via PROPPATCH won't have it persisted.
  • Storage.acquire_lock() yields without acquiring anything — no protection against concurrent writes from multiple DAV clients.
  • sync() raises if a client passes back a previous sync token, so sync-collection REPORT (used by most modern CalDAV/CardDAV clients for efficient incremental sync) always falls back to a full resync.
  • to_vobject() always exports VEVENT; VTODO/VJOURNAL export isn't modeled (there's no per-collection "component type" field), even though import now accepts them.

Test plan

  • Run tests/test_dav_collection.py via odoo --test-enable -i base_dav -d <db> in an Odoo 17.0 environment with radicale, vobject, python-dateutil pip-installed (a single Odoo test DB is all that's needed — Radicale itself is stateless and stores nothing of its own; it delegates entirely to the dav.collection Odoo model via our Storage/Collection glue classes)
  • Manually verify CalDAV/CardDAV access via a client (e.g. /.dav/{username}/{collection_id}/) against the demo collections
  • pre-commit run clean (ruff, ruff-format, pylint_odoo, oca-checks-odoo-module, prettier, oca-gen-addon-readme, oca-gen-external-dependencies)

rossigee added 2 commits July 25, 2026 10:47
Port base_dav from the 11.0 branch, adapting it to Odoo 17.0 and this
repo's packaging conventions (pyproject.toml, generated README, tests).

Fixes made during the port review:
- radicale/collection.py: ItemWrapper was renamed to Item to match the
  name models/dav_collection.py actually imports; the mismatch caused
  an ImportError on load.
- radicale/auth.py, radicale/collection.py: the ImportError fallbacks
  for missing radicale set base classes to None, but Auth/Collection/
  Storage subclass them directly, which raises TypeError when radicale
  isn't installed. Fall back to object instead.
- radicale/auth.py: use the public request.update_env() instead of
  setting the private request._env attribute.
- __manifest__.py: declare the dateutil external dependency that
  dav_collection_field_mapping.py already imports.
- dav_get() set result.logger = self.logger for files-type
  collections, but dav.collection has no logger attribute: any DAV
  request against a "Files" collection raised AttributeError.
- dav_delete() was a no-op for files-type collections, so DELETE
  requests against a file appeared to succeed without removing the
  underlying ir.attachment. Implemented deletion mirroring the lookup
  already used by dav_get().
- from_vobject() only accepted VEVENT components even though get_meta()
  advertises VTODO and VJOURNAL support too; incoming VTODO/VJOURNAL
  items returned None which then crashed dav_upload() with an
  unhandled TypeError. Accept all three component types on import, and
  raise a clear ValueError from dav_upload() instead of crashing when
  an item still can't be parsed.

Added tests covering the files collection get/delete flow and the
vtodo/vjournal/invalid-item from_vobject paths.
rossigee added 4 commits July 25, 2026 11:52
… comodel

Required Many2one fields default to ondelete='restrict', but ir.model
does not support that mode as a comodel, causing registry setup to
fail on install.
….fields comodel

Same issue as model_id: required Many2one defaults to ondelete='restrict',
which ir.model.fields does not support as a comodel.
tools.safe_eval referenced the odoo.tools.safe_eval submodule instead
of the safe_eval function within it, breaking domain evaluation and
demo data loading. Import safe_eval directly instead.

res.users._login requires a user_agent_env argument in Odoo 17; the
missing argument raised a TypeError that was silently swallowed by
the broad except clause, making DAV authentication always fail.
- Add TestDavCollectionDelete for dav_delete on non-files
- Add TestDavRights with 6 authorization test methods
- Add TestDavCollectionList for dav_list variants
- Add TestRadicaleStorage (5 methods)
- Add TestRadicaleCollection (15 methods)
- Add TestFieldMappingCode for code-type mappings
- Add test_login_raises_exception and test_get_external_login
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants