Merge pull request #2182 from TowyTowy/fix/bday-empty-fn-placeholder

Fix: bday conversion clobbers NICKNAME when FN is empty
This commit is contained in:
Peter Bieringer
2026-07-15 21:37:13 +03:00
committed by GitHub
3 changed files with 39 additions and 3 deletions

View File

@@ -4,6 +4,7 @@
* Fix: time-range filter treated a VEVENT with a whole-day DURATION (e.g. P1D, P2D) as zero-length (timedelta.seconds instead of total_seconds), so such events were missing from calendar-query REPORT results
* Fix: calendar-data expand (REPORT) left recurrence properties (e.g. RDATE) on the expanded single-occurrence VEVENTs; a single try/except around the sequential delattr() calls stopped at the first absent property (e.g. missing EXDATE), so later ones were never removed
* Fix: text-match filter on a structured property (e.g. vCard N or ADR) crashed with HTTP 500 (AttributeError: 'Name'/'Address' object has no attribute 'lower') because vobject parses these into non-string objects; their text representation is now used
* Fix: sharing bday-to-ICS conversion assigned the empty-FN fallback marker to the {nickname} placeholder instead of {fn}, so a VCARD with an empty FN got its NICKNAME overwritten with "!fn!" in the generated SUMMARY/DESCRIPTION and the {fn} fallback never resolved
## 3.7.6
* Extension: item verification on commandline

View File

@@ -717,7 +717,7 @@ class Item:
if hasattr(self.vobject_item, "fn") and self.vobject_item.fn.value != "":
placeholder_mapping['{fn}'] = self.vobject_item.fn.value
else:
placeholder_mapping['{nickname}'] = '!fn!'
placeholder_mapping['{fn}'] = '!fn!'
# rfc6350#6.2 FamilyName;GivenName;AdditionalNames;HonorificPrefixes;HonorificSuffixes
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.family != "":

View File

@@ -28,11 +28,13 @@ import re
import sys
import tempfile
import urllib
from typing import Dict, Sequence, Tuple, Union
from typing import Dict, Sequence, Tuple, Union, cast
import pytest
import vobject
from radicale import pathutils, sharing, xmlutils
from radicale import pathutils, sharing, storage, xmlutils
from radicale.item import Item
from radicale.tests import BaseTest
from radicale.tests.helpers import get_file_content
@@ -5328,6 +5330,39 @@ permissions: RrWw""")
assert "Test-FN-C3 (1990/5)" in answer
assert "Test-FN-C3 (1990/6)" in answer
def test_sharing_bday_conversion_empty_fn(self) -> None:
"""BDAY-to-ICS conversion of a VCARD with an empty FN property.
vCard mandates FN, but some clients emit it empty. The fallback for
an empty ``{fn}`` must be assigned to the ``{fn}`` placeholder, not to
``{nickname}``: otherwise a present NICKNAME gets clobbered with the
``!fn!`` marker and the ``{fn}`` fallback never triggers.
"""
vcard = vobject.readOne(
"BEGIN:VCARD\r\n"
"VERSION:3.0\r\n"
"UID:contact-empty-fn\r\n"
"N:FamilyTest;GivenTest;;;\r\n"
"FN:\r\n"
"NICKNAME:Test-NICKNAME\r\n"
"BDAY:1990-05-06\r\n"
"END:VCARD\r\n")
class _StubCollection:
path = "test"
item = Item(collection=cast(storage.BaseCollection, _StubCollection()),
vobject_item=vcard, href="contact-empty-fn.vcf")
converted = item.convert_vcf_to_ics(ShareActions={"config": {
"conversion_bday_summary_template": "{nickname} (BDAY)",
"conversion_bday_description_template": "[{fn}|no-fn]"}})
assert converted is not None
serialized = converted.serialize()
# NICKNAME must survive, not be overwritten by the '!fn!' marker
assert "SUMMARY:Test-NICKNAME (BDAY)" in serialized
# empty {fn} must expose the '!fn!' marker so the fallback resolves
assert "DESCRIPTION:no-fn" in serialized
def test_sharing_api_map_vcf_bday_age_template(self) -> None:
"""share-by-map with conversion=bday template tests with age."""
self.configure({"auth": {"type": "htpasswd",