Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions base_dav/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
==========================
Caldav and Carddav support
==========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:6cc5b91f1cff865b4527a2097534adb50c8bade6eaed9f3b820275b7b8ab19d3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--backend-lightgray.png?logo=github
:target: https://github.com/OCA/server-backend/tree/18.0/base_dav
:alt: OCA/server-backend
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-backend-18-0/server-backend-18-0-base_dav
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-backend&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds WebDAV support to Odoo, specifically CalDAV and
CardDAV.

You can configure arbitrary objects as a calendar or an address book,
thus make arbitrary information accessible in external systems or your
mobile.

**Table of contents**

.. contents::
:local:

Configuration
=============

To configure this module, you need to:

1. Go to Settings / WebDAV Collections and create or edit your
collections. There, you'll also see the URL to point your clients to.

Note that you need to configure a dbfilter if you use multiple
databases.

Known issues / Roadmap
======================

- Much better UX for configuring collections (probably provide a group
that sees the current fully flexible field mappings, and by default
show some dumbed down version where you can select some preselected
vobject fields);
- Support todo lists and journals;
- Support configuring default field mappings per model;
- Support plain WebDAV collections to make some model's records
accessible as folders, and the records' attachments as files (r/w);
- Support configuring lists of calendars so that you can have a calendar
for every project and appointments are tasks, or a calendar for every
sales team and appointments are sale orders. Lots of possibilities.

Backporting this to <=v10 will be tricky because radicale only supports
python3. Probably it will be quite a hassle to backport the relevant
code, so it might be more sensible to just backport the configuration
part, and implement the rest as radicale auth/storage plugin that talks
to Odoo via odoorpc. It should be possible to recycle most of the code
from this addon, which actually implements those plugins, but then
within Odoo.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-backend/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-backend/issues/new?body=module:%20base_dav%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* initOS GmbH
* Therp BV

Contributors
------------

- Holger Brunn <hbrunn@therp.nl>
- Florian Kantelberg <florian.kantelberg@initos.com>
- `Cetmix <https://cetmix.com/>`__

- Ivan Sokolov
- George Smirnov
- Dmitry Meita

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/server-backend <https://github.com/OCA/server-backend/tree/18.0/base_dav>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
5 changes: 5 additions & 0 deletions base_dav/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models
from . import controllers
from . import radicale
25 changes: 25 additions & 0 deletions base_dav/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2018 Therp BV <https://therp.nl>
# Copyright 2019-2020 initOS GmbH <https://initos.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Caldav and Carddav support",
"version": "18.0.1.0.0",
"author": "initOS GmbH,Therp BV,Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Extra Tools",
"summary": "Access Odoo data as calendar or address book",
"website": "https://github.com/OCA/server-backend",
"depends": [
"base",
],
"demo": [
"demo/dav_collection.xml",
],
"data": [
"views/dav_collection.xml",
"security/ir.model.access.csv",
],
"external_dependencies": {
"python": ["radicale"],
},
}
3 changes: 3 additions & 0 deletions base_dav/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright 2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import main
135 changes: 135 additions & 0 deletions base_dav/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright 2018 Therp BV <https://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

import io
import sys

import werkzeug
from radicale import config as radicale_config
from radicale.app import Application
from werkzeug.wrappers.response import Response as WerkzeugResponse

from odoo import http
from odoo.http import request

PREFIX = "/.dav"


class Main(http.Controller):
@http.route(
["/.well-known/carddav", "/.well-known/caldav", "/.well-known/webdav"],
type="http",
auth="none",
csrf=False,
)
def handle_well_known_request(self) -> WerkzeugResponse:
"""
Redirect well-known CalDAV/CardDAV/WebDAV endpoints to the Radicale mount point.

This endpoint exists for client compatibility: many CalDAV/CardDAV clients
probe `/.well-known/caldav` or `/.well-known/carddav` and expect a redirect.

:return: HTTP 301 redirect response to ``/.dav``.
:rtype: werkzeug.wrappers.response.Response
"""
return werkzeug.utils.redirect(PREFIX, 301)

@http.route(
[PREFIX, f"{PREFIX}/<path:davpath>"],
type="http",
auth="none",
csrf=False,
)
def handle_dav_request(self, davpath=None, **kwargs):
"""Handle WebDAV/CalDAV/CardDAV requests by proxying them to Radicale 3.x.

The controller builds a WSGI environ from the current Odoo/Werkzeug request,
configures Radicale to use Odoo-backed plugins (auth/storage/rights),
executes the Radicale WSGI application, and returns an Odoo HTTP response.

:param davpath: Path relative to the DAV mount point (``/.dav``),
e.g. ``"admin/2/123"``; if ``None`` the root path is used.
:type davpath: str, optional
:param kwargs: Extra keyword arguments passed by the routing layer (unused).
:type kwargs: Any

:raises Exception: Any unexpected Radicale or Odoo/Werkzeug error
during request processing will propagate as an Odoo HTTP 500.

:return: Response produced by Radicale, including status and headers.
:rtype: odoo.http.Response
"""
configuration = radicale_config.load()
configuration.update(
{
"auth": {"type": "odoo.addons.base_dav.radicale.auth"},
"storage": {"type": "odoo.addons.base_dav.radicale.collection"},
"rights": {"type": "odoo.addons.base_dav.radicale.rights"},
"web": {"type": "none"},
"hook": {"type": "none"},
},
"odoo",
)

app = Application(configuration)

# Let's take WSGI environ from werkzeug/odoo
environ = dict(request.httprequest.environ)

# Radicale 3.x requires wsgi.errors and wsgi.input
environ.setdefault("wsgi.errors", sys.stderr)
method = environ.get("REQUEST_METHOD") or request.httprequest.method
raw_body = request.httprequest.get_data(cache=False) or b""

if method == "PROPFIND" and len(raw_body) == 0:
raw_body = (
b'<?xml version="1.0" encoding="utf-8"?>'
b'<D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>'
)

# Force Radicale to read the body we provide
environ["wsgi.input"] = io.BytesIO(raw_body)
environ["CONTENT_LENGTH"] = str(len(raw_body))

# Ensure content type is present for XML parsing
environ.setdefault("CONTENT_TYPE", "application/xml; charset=utf-8")

# Radicale should know that it is mounted under /.dav
environ["SCRIPT_NAME"] = PREFIX
environ["HTTP_X_SCRIPT_NAME"] = PREFIX

# PATH_INFO must be absolute (with "/")
path_info = "/" + (davpath or "")
environ["PATH_INFO"] = path_info

status_headers = {"status": "500 Internal Server Error", "headers": []}

def start_response(status, headers, exc_info=None):
"""WSGI start_response callback used by Radicale.

:param status: HTTP status line, e.g. ``"207 Multi-Status"``.
:type status: str
:param headers: Sequence of ``(header_name, header_value)``.
:type headers: Sequence[tuple[str, str]]
:param exc_info: Optional exception info as per WSGI spec (unused).
:type exc_info: Any, optional
"""
status_headers["status"] = status
status_headers["headers"] = headers

result_iter = app(environ, start_response)
try:
response_body = b"".join(result_iter) if result_iter else b""
finally:
if hasattr(result_iter, "close"):
result_iter.close()

headers = status_headers["headers"]
if isinstance(headers, dict):
headers = list(headers.items())

return http.Response(
response=response_body,
status=status_headers["status"],
headers=headers,
)
39 changes: 39 additions & 0 deletions base_dav/demo/dav_collection.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<odoo>
<record id="collection_addressbook" model="dav.collection">
<field name="name">Addressbook</field>
<field name="dav_type">addressbook</field>
<field name="model_id" ref="base.model_res_partner" />
<field name="domain">[]</field>
<field name="rights">authenticated</field>
</record>

<record id="field_mapping_addressbook_n" model="dav.collection.field_mapping">
<field name="name">N</field>
<field name="field_id" ref="base.field_res_partner__name" />
<field name="collection_id" ref="collection_addressbook" />
</record>

<record id="field_mapping_addressbook_fn" model="dav.collection.field_mapping">
<field name="name">FN</field>
<field name="field_id" ref="base.field_res_partner__name" />
<field name="collection_id" ref="collection_addressbook" />
</record>

<record id="field_mapping_addressbook_photo" model="dav.collection.field_mapping">
<field name="name">photo</field>
<field name="field_id" ref="base.field_res_partner__image_1920" />
<field name="collection_id" ref="collection_addressbook" />
</record>

<record id="field_mapping_addressbook_email" model="dav.collection.field_mapping">
<field name="name">email</field>
<field name="field_id" ref="base.field_res_partner__email" />
<field name="collection_id" ref="collection_addressbook" />
</record>

<record id="field_mapping_addressbook_tel" model="dav.collection.field_mapping">
<field name="name">tel</field>
<field name="field_id" ref="base.field_res_partner__phone" />
<field name="collection_id" ref="collection_addressbook" />
</record>
</odoo>
Loading
Loading