Skip to content

Commit f54546e

Browse files
authored
add calendar_ics_url (#239)
1 parent 23a88ac commit f54546e

File tree

7 files changed

+200
-0
lines changed

7 files changed

+200
-0
lines changed

calendar_ics_url/README.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
================
2+
Calendar ICS URL
3+
================
4+
5+
This module enhances calendar event ICS file generation by adding the
6+
videocall_location as a URL property in the ICS format.
7+
8+
This makes meeting URLs clickable in calendar applications (Outlook, Google
9+
Calendar, Apple Calendar, etc.).
10+
11+
Usage
12+
=====
13+
14+
The module works automatically. When a calendar event has a videocall_location
15+
set, the ICS file generated will include a URL field that recipients can click
16+
in their calendar applications.

calendar_ics_url/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

calendar_ics_url/__manifest__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "Calendar ICS URL",
3+
"summary": "Add URL field to calendar event ICS files",
4+
"category": "Productivity/Calendar",
5+
"version": "18.0.1.0.0",
6+
"depends": ["calendar"],
7+
"data": [],
8+
"author": "Nitrokey GmbH",
9+
"license": "AGPL-3",
10+
"installable": True,
11+
"auto_install": False,
12+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import calendar_event
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import vobject
2+
3+
from odoo import models
4+
5+
6+
class CalendarEvent(models.Model):
7+
_inherit = "calendar.event"
8+
9+
def _get_ics_file(self):
10+
"""Override to add URL field to ICS files from videocall_location."""
11+
result = super()._get_ics_file()
12+
13+
for meeting in self:
14+
if meeting.id not in result:
15+
continue
16+
17+
# Only add URL if videocall_location is set
18+
if not meeting.videocall_location:
19+
continue
20+
21+
# Parse the existing ICS content
22+
cal = vobject.readOne(result[meeting.id].decode("utf-8"))
23+
24+
# Add URL property to the event
25+
if hasattr(cal, "vevent"):
26+
cal.vevent.add("url").value = meeting.videocall_location
27+
28+
# Re-serialize the calendar
29+
result[meeting.id] = cal.serialize().encode("utf-8")
30+
31+
return result

calendar_ics_url/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_calendar_ics_url
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from datetime import datetime, timedelta
2+
3+
from odoo.tests import tagged
4+
from odoo.tests.common import TransactionCase
5+
6+
try:
7+
import vobject
8+
except ImportError:
9+
vobject = None
10+
11+
12+
@tagged("post_install", "-at_install")
13+
class TestCalendarIcsUrl(TransactionCase):
14+
"""Test calendar event ICS file generation with URL field."""
15+
16+
@classmethod
17+
def setUpClass(cls):
18+
super().setUpClass()
19+
cls.user = cls.env.ref("base.user_admin")
20+
cls.partner = cls.env.ref("base.partner_admin")
21+
22+
def test_ics_file_with_url(self):
23+
"""Test that URL is added to ICS file when videocall_location is set."""
24+
if not vobject:
25+
self.skipTest("vobject module not available")
26+
27+
# Create a calendar event with videocall_location
28+
event = self.env["calendar.event"].create(
29+
{
30+
"name": "Test Meeting with URL",
31+
"start": datetime.now(),
32+
"stop": datetime.now() + timedelta(hours=1),
33+
"user_id": self.user.id,
34+
"partner_ids": [(6, 0, [self.partner.id])],
35+
"videocall_location": "https://meet.example.com/test-meeting",
36+
}
37+
)
38+
39+
# Generate ICS file
40+
ics_files = event._get_ics_file()
41+
42+
# Verify ICS file was generated
43+
self.assertIn(event.id, ics_files)
44+
45+
# Parse the ICS content
46+
ics_content = ics_files[event.id].decode("utf-8")
47+
cal = vobject.readOne(ics_content)
48+
49+
# Verify URL property exists and has correct value
50+
self.assertTrue(hasattr(cal.vevent, "url"))
51+
self.assertEqual(cal.vevent.url.value, "https://meet.example.com/test-meeting")
52+
53+
def test_ics_file_without_url(self):
54+
"""Test that ICS file is generated correctly without videocall_location."""
55+
if not vobject:
56+
self.skipTest("vobject module not available")
57+
58+
# Create a calendar event without videocall_location
59+
event = self.env["calendar.event"].create(
60+
{
61+
"name": "Test Meeting without URL",
62+
"start": datetime.now(),
63+
"stop": datetime.now() + timedelta(hours=1),
64+
"user_id": self.user.id,
65+
"partner_ids": [(6, 0, [self.partner.id])],
66+
}
67+
)
68+
69+
# Generate ICS file
70+
ics_files = event._get_ics_file()
71+
72+
# Verify ICS file was generated
73+
self.assertIn(event.id, ics_files)
74+
75+
# Parse the ICS content
76+
ics_content = ics_files[event.id].decode("utf-8")
77+
cal = vobject.readOne(ics_content)
78+
79+
# Verify URL property does not exist
80+
self.assertFalse(hasattr(cal.vevent, "url"))
81+
82+
def test_ics_file_preserves_other_fields(self):
83+
"""Test that adding URL preserves other ICS fields like LOCATION."""
84+
if not vobject:
85+
self.skipTest("vobject module not available")
86+
87+
# Create a calendar event with both location and videocall_location
88+
event = self.env["calendar.event"].create(
89+
{
90+
"name": "Test Meeting with Location and URL",
91+
"start": datetime.now(),
92+
"stop": datetime.now() + timedelta(hours=1),
93+
"user_id": self.user.id,
94+
"partner_ids": [(6, 0, [self.partner.id])],
95+
"location": "Conference Room A",
96+
"videocall_location": "https://meet.example.com/room-a",
97+
}
98+
)
99+
100+
# Generate ICS file
101+
ics_files = event._get_ics_file()
102+
103+
# Parse the ICS content
104+
ics_content = ics_files[event.id].decode("utf-8")
105+
cal = vobject.readOne(ics_content)
106+
107+
# Verify both LOCATION and URL exist
108+
self.assertTrue(hasattr(cal.vevent, "location"))
109+
self.assertEqual(cal.vevent.location.value, "Conference Room A")
110+
self.assertTrue(hasattr(cal.vevent, "url"))
111+
self.assertEqual(cal.vevent.url.value, "https://meet.example.com/room-a")
112+
113+
def test_ics_file_with_empty_videocall_location(self):
114+
"""Test that empty videocall_location doesn't add URL field."""
115+
if not vobject:
116+
self.skipTest("vobject module not available")
117+
118+
# Create a calendar event with empty videocall_location
119+
event = self.env["calendar.event"].create(
120+
{
121+
"name": "Test Meeting with Empty URL",
122+
"start": datetime.now(),
123+
"stop": datetime.now() + timedelta(hours=1),
124+
"user_id": self.user.id,
125+
"partner_ids": [(6, 0, [self.partner.id])],
126+
"videocall_location": "",
127+
}
128+
)
129+
130+
# Generate ICS file
131+
ics_files = event._get_ics_file()
132+
133+
# Parse the ICS content
134+
ics_content = ics_files[event.id].decode("utf-8")
135+
cal = vobject.readOne(ics_content)
136+
137+
# Verify URL property does not exist
138+
self.assertFalse(hasattr(cal.vevent, "url"))

0 commit comments

Comments
 (0)