Compare commits
11 commits
18.0
...
17.0-calda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e537a7f7f4 | ||
|
|
5797f8efca | ||
|
|
992778e353 | ||
|
|
90d05d75a2 | ||
|
|
5ca1d56e54 | ||
|
|
ce7b27cc65 | ||
|
|
421d609abd | ||
|
|
0e07d42825 | ||
|
|
14a8b6acf0 | ||
|
|
068d1fb29f | ||
|
|
0104e3897c |
8 changed files with 922 additions and 363 deletions
|
|
@ -3,7 +3,7 @@ CalDAV Synchronization
|
|||
|
||||
Bemade Inc.
|
||||
|
||||
Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
Copyright (C) 2023-June Bemade Inc. (https://www.bemade.org).
|
||||
Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
|
||||
This program is under the terms of the GNU Lesser General Public License (LGPL-3)
|
||||
|
|
@ -44,13 +44,44 @@ Usage
|
|||
Technical Details
|
||||
-----------------
|
||||
|
||||
- The module extends the `calendar.event` model to add CalDAV synchronization
|
||||
* The module extends the `calendar.event` model to add CalDAV synchronization
|
||||
functionality.
|
||||
- It uses the `icalendar` library to format events and the `caldav` library to
|
||||
* It uses the `icalendar` library to format events and the `caldav` library to
|
||||
interact with CalDAV servers.
|
||||
- Polling for changes on the CalDAV server can be triggered manually by
|
||||
* Polling for changes on the CalDAV server can be triggered manually by
|
||||
triggering the scheduled action in Odoo.
|
||||
|
||||
Change Log
|
||||
----------
|
||||
|
||||
17.0.0.6.0
|
||||
^^^^^^^^^^
|
||||
|
||||
* Fixed an issue where synchronizing events created duplicate events on every sync.
|
||||
* Completely revamped and synchronization of recurring events in both directions.
|
||||
|
||||
* Making a recurring event in Odoo correctly creates the recurring event on the server.
|
||||
* Modifying the base event of a recurrence with "all events" or "future events" in
|
||||
Odoo reflects correctly on the server.
|
||||
* Modifying a non-base event correctly updates on the server in all 3 modes (this
|
||||
event only, all events, future events).
|
||||
* Modifying a base recurring event on the CalDAV server correctly updates the events
|
||||
on Odoo after a synchronization.
|
||||
* Deleting a whole recurring sequence from Odoo correctly deletes the sequence from
|
||||
the CalDAV server.
|
||||
* Deleting a single event or a whole recurring sequence on the CalDAV server
|
||||
correctly synchronizes to Odoo after a synchronization.
|
||||
|
||||
* CalDAV (iCalendar) UIDs are now correctly shared among events of a same recurrence in
|
||||
Odoo. This corrects a number of issues around updating and deleting events from both
|
||||
the Odoo and CalDAV server side.
|
||||
|
||||
Issues & Requests
|
||||
-----------------
|
||||
|
||||
Please submit issues on Bemade's Gitlab at https://git.bemade.org/bemade/bemade-addons
|
||||
or via our website at https://www.bemade.org.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
{
|
||||
"name": "CalDAV Synchronization",
|
||||
"version": "17.0.0.5.9",
|
||||
"version": "17.0.0.6.0",
|
||||
"license": "LGPL-3",
|
||||
"category": "Productivity",
|
||||
"summary": "Synchronize Odoo Calendar Events with CalDAV Servers",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"website": "https://www.bemade.org",
|
||||
"depends": ["base", "calendar"],
|
||||
"external_dependencies": {
|
||||
"python": ["caldav", "icalendar", "bs4"],
|
||||
"python": ["caldav", "icalendar", "markdownify", "markdown2"],
|
||||
},
|
||||
"images": ["static/description/images/main_screenshot.png"],
|
||||
"data": [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import calendar_event
|
||||
from . import res_users
|
||||
from . import calendar_recurrence
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
39
caldav_sync/models/calendar_recurrence.py
Normal file
39
caldav_sync/models/calendar_recurrence.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from odoo import models, fields, api
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecurrenceRule(models.Model):
|
||||
_inherit = "calendar.recurrence"
|
||||
|
||||
caldav_uid = fields.Char(
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
_sql_constraints = [
|
||||
("caldav_uid_unique", "UNIQUE (caldav_uid)", "caldav_uid must be unique")
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if not self._context.get("caldav_keep_ids"):
|
||||
for vals in vals_list:
|
||||
vals["caldav_uid"] = str(uuid.uuid4())
|
||||
else:
|
||||
for vals in vals_list:
|
||||
base_event = self.env["calendar.event"].browse(vals["base_event_id"])
|
||||
vals.update(caldav_uid=base_event.caldav_uid)
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.model
|
||||
def _detach_events(self, events):
|
||||
"""When events are detached from a recurrence, their CalDAV UID and recurrence-id
|
||||
are no longer going to be valid, so we remove them from the server. They may then
|
||||
be re-written to the server with their new IDs later, but we don't care about
|
||||
that here."""
|
||||
detached_events = super()._detach_events(events)
|
||||
for event in detached_events:
|
||||
event._sync_unlink_to_caldav()
|
||||
return detached_events
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
caldav==1.3.9
|
||||
icalendar==5.0.13
|
||||
bs4==0.0.2
|
||||
markdownify==0.13.1
|
||||
markdown2==2.5.1
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@
|
|||
<p><strong>License:</strong> GNU Lesser General Public License (LGPL-3)</p>
|
||||
<h2>Overview</h2>
|
||||
<p>The CalDAV Synchronization module for Odoo allows users to synchronize their calendar events with CalDAV servers. This enables seamless integration of Odoo calendar with external applications like Apple Calendar or Thunderbird.</p>
|
||||
<h2>Warning</h2>
|
||||
<p>This module is in early development stages and should not be considered stable.
|
||||
Please test it on an unimportant calendar before using it with your important data.
|
||||
We are working hard to make improvements based on user feedback, so please reach
|
||||
out if you run into any issues or need a specific feature.</p>
|
||||
<h2>Features</h2>
|
||||
<ul>
|
||||
<li>Synchronize Odoo calendar events with CalDAV servers.</li>
|
||||
|
|
@ -64,6 +69,17 @@
|
|||
</ol>
|
||||
<h2>Technical Details</h2>
|
||||
<p>The module extends the <code>calendar.event</code> model to add CalDAV synchronization functionality. It uses the <code>icalendar</code> library to format events and the <code>caldav</code> library to interact with CalDAV servers. Polling for changes on the CalDAV server can be triggered manually by triggering the scheduled action in Odoo.</p>
|
||||
<h2>Support</h2>
|
||||
<p>If you have any issues with the module or feature requests, please reach out
|
||||
to us by creating an issue on our Gitlab at
|
||||
<a href="https://git.bemade.org/bemade/bemade-addons">
|
||||
https://git.bemade.org/bemade/bemade-addons
|
||||
</a>
|
||||
or by submitting a request on our website at
|
||||
<a href="https://www.bemade.org">
|
||||
https://www.bemade.org</a>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from collections.abc import Iterable
|
||||
from odoo.tests import TransactionCase
|
||||
from odoo import Command
|
||||
from unittest.mock import patch, MagicMock, PropertyMock
|
||||
from unittest.mock import patch, MagicMock, DEFAULT
|
||||
import icalendar
|
||||
from pathlib import Path
|
||||
from .common import CaldavTestCommon
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, UTC, timedelta
|
||||
import caldav
|
||||
|
||||
WEEKDAY_MAP = {
|
||||
0: "SUN",
|
||||
|
|
@ -28,7 +29,6 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None):
|
|||
with (
|
||||
patch("caldav.DAVClient") as MockDAVClient,
|
||||
patch("caldav.Calendar") as MockCalendar,
|
||||
patch("caldav.Event") as MockEvent,
|
||||
):
|
||||
mock_client = MockDAVClient.return_value
|
||||
mock_calendar = MockCalendar.return_value
|
||||
|
|
@ -43,6 +43,13 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None):
|
|||
raise Exception("Calendar does not exist.")
|
||||
|
||||
mock_calendar.side_effect = calendar_side_effect
|
||||
|
||||
def event_by_uid_side_effect(self, uid):
|
||||
for event in self.events():
|
||||
if str(event.icalendar_component.get("uid")) == uid:
|
||||
return event
|
||||
return DEFAULT
|
||||
|
||||
ical_events = []
|
||||
if ics_paths:
|
||||
if not isinstance(ics_paths, Iterable):
|
||||
|
|
@ -51,18 +58,27 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None):
|
|||
with ics_path.open("rb") as file:
|
||||
ical_content = file.read()
|
||||
ical_events.append(icalendar.Calendar.from_ical(ical_content))
|
||||
mock_caldav_events = []
|
||||
for ical_event in ical_events:
|
||||
mock_event = MockEvent()
|
||||
mock_event.icalendar_instance = ical_event
|
||||
if last_modified:
|
||||
for component in ical_event.walk():
|
||||
if component.name == "VEVENT":
|
||||
component["last-modified"] = last_modified.strftime(
|
||||
"%Y%m%dT%H%M%SZ"
|
||||
)
|
||||
mock_caldav_events.append(mock_event)
|
||||
mock_calendar.events.return_value = mock_caldav_events
|
||||
if last_modified:
|
||||
for event in ical_events:
|
||||
for subcomponent in event.subcomponents:
|
||||
if subcomponent.name == "VEVENT":
|
||||
subcomponent["last-modified"] = icalendar.vDate(last_modified)
|
||||
subcomponent["dtstamp"] = icalendar.vDate(last_modified)
|
||||
|
||||
base_events = [event for event in ical_events if not event.get("recurrence-id")]
|
||||
for base_event in base_events:
|
||||
child_events = [
|
||||
event
|
||||
for event in ical_events
|
||||
if event.get("recurrence-id")
|
||||
and event.get("uid") == base_event.get("uid")
|
||||
]
|
||||
for child_event in child_events:
|
||||
base_event.add_component(child_event)
|
||||
mock_calendar.add_event(base_event)
|
||||
caldav_events = [caldav.Event(data=event) for event in base_events]
|
||||
mock_calendar.events.return_value = caldav_events
|
||||
mock_calendar.event_by_uid.side_effect = event_by_uid_side_effect
|
||||
user._compute_is_caldav_enabled()
|
||||
yield
|
||||
|
||||
|
|
@ -258,35 +274,6 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
|||
)
|
||||
)
|
||||
|
||||
def test_multiple_user_attendees_event_to_server_create(self):
|
||||
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||
self._create_multi_user_test_event()
|
||||
self.assertEqual(mock_calendar.add_event.call_count, 3)
|
||||
|
||||
def test_event_to_server_delete(self):
|
||||
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||
self._create_multi_user_test_event().unlink()
|
||||
self.assertEqual(
|
||||
mock_calendar.event_by_uid.return_value.delete.call_count, 3
|
||||
)
|
||||
|
||||
def test_event_to_server_update(self):
|
||||
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||
self._create_multi_user_test_event().write(
|
||||
{"start": datetime.now() + timedelta(days=14)}
|
||||
)
|
||||
self.assertEqual(mock_calendar.save_event.call_count, 3)
|
||||
|
||||
def test_recurrent_event_to_server(self):
|
||||
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||
self._create_multi_user_test_event().write(
|
||||
{
|
||||
"recurrency": True,
|
||||
}
|
||||
)
|
||||
args = mock_calendar.save_event.call_args
|
||||
self.assertEqual(mock_calendar.save_event.call_count, 3)
|
||||
|
||||
@contextmanager
|
||||
def _patch_all_3_users_davclients(self):
|
||||
with patch("caldav.DAVClient") as MockDAVClient:
|
||||
|
|
|
|||
Loading…
Reference in a new issue